// Consolidated decompiled source — Wubarrk-Njord v1.3.0 // Generated by Hexium's decompiled-source browser. Best-effort concatenation of every type in this // version's manifest — decompiler output isn't guaranteed to compile as-is. using System; using System.Runtime.CompilerServices; using Microsoft.CodeAnalysis; using Njord.Core; using UnityEngine; using System.Collections.Generic; using System.Collections; using System.Reflection; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Njord.HUD; using UnityEngine.Rendering; using UnityEngine.UI; using Njord.Patches; // ---- Njord/plugins/Njord.dll :: Microsoft.CodeAnalysis.EmbeddedAttribute ---- namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } // ---- Njord/plugins/Njord.dll :: System.Runtime.CompilerServices.RefSafetyRulesAttribute ---- namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } // ---- Njord/plugins/Njord.dll :: Njord.NjordBoatController ---- namespace Njord { public static class NjordBoatController { public static string GetShipPrefabName(object ship) { if (!(ship is Component component)) { return "Unknown"; } return component.gameObject.name.Replace("(Clone)", "").Trim(); } public static float GetBaseline(NjordSailState state, string shipName) { NjordRuntimeConfig runtimeConfig = NjordSync.RuntimeConfig; if (runtimeConfig == null) { return 0f; } return state switch { NjordSailState.Forward => runtimeConfig.BaseForwardForce, NjordSailState.Half => runtimeConfig.BaseForwardForce, NjordSailState.Full => runtimeConfig.BaseForwardForce, _ => 0f, }; } public static float ApplyAcceleration(float baseline, float dt) { NjordRuntimeConfig runtimeConfig = NjordSync.RuntimeConfig; if (runtimeConfig == null) { return 0f; } float num = baseline * runtimeConfig.AccelerationMultiplier; if (float.IsNaN(num) || float.IsInfinity(num)) { return 0f; } return num; } public static float GetSailMult(NjordSailState state) { NjordRuntimeConfig runtimeConfig = NjordSync.RuntimeConfig; if (runtimeConfig == null) { return 1f; } return state switch { NjordSailState.Forward => runtimeConfig.SailForwardForce, NjordSailState.Half => runtimeConfig.HalfSailForce, NjordSailState.Full => runtimeConfig.FullSailForce, _ => 1f, }; } public static float GetReverseForce(float dt) { NjordRuntimeConfig runtimeConfig = NjordSync.RuntimeConfig; if (runtimeConfig == null) { return 0f; } float num = runtimeConfig.BaseReverseForce * runtimeConfig.ReverseKick; if (float.IsNaN(num) || float.IsInfinity(num)) { return 0f; } return num; } } } // ---- Njord/plugins/Njord.dll :: Njord.Njord_ShipControlTracker ---- namespace Njord { public static class Njord_ShipControlTracker { public const float READY_DELAY = 0.35f; private static Dictionary _controlMap; private static Dictionary _windAngleMap; private static Dictionary _windGraceTimerMap; private static Dictionary _wasInDeadZoneMap; private static float _lastLookupLogTime = -10f; private const float LOOKUP_LOG_INTERVAL = 2f; public static long CurrentController { get; private set; } = 0L; public static bool ShouldShowHud { get; private set; } = false; public static float ReadyTimer { get; private set; } = 0f; public static bool SteeringGateReady { get; private set; } = false; private static Dictionary ControlMap { get { if (_controlMap == null) { _controlMap = new Dictionary(); } return _controlMap; } } private static Dictionary WindAngleMap { get { if (_windAngleMap == null) { _windAngleMap = new Dictionary(); } return _windAngleMap; } } private static Dictionary WindGraceTimerMap { get { if (_windGraceTimerMap == null) { _windGraceTimerMap = new Dictionary(); } return _windGraceTimerMap; } } private static Dictionary WasInDeadZoneMap { get { if (_wasInDeadZoneMap == null) { _wasInDeadZoneMap = new Dictionary(); } return _wasInDeadZoneMap; } } public static bool GetShipWasInDeadZone(Ship ship, bool defaultValue) { if (ship == null) { return defaultValue; } ZNetView component = ship.GetComponent(); if (component == null || !component.IsValid()) { return defaultValue; } ZDOID uid = component.GetZDO().m_uid; if (WasInDeadZoneMap.TryGetValue(uid, out var value)) { return value; } return defaultValue; } public static void SetShipWasInDeadZone(Ship ship, bool val) { if (!(ship == null)) { ZNetView component = ship.GetComponent(); if (!(component == null) && component.IsValid()) { ZDOID uid = component.GetZDO().m_uid; WasInDeadZoneMap[uid] = val; } } } public static float GetShipGraceTimer(Ship ship) { if (ship == null) { return 0f; } ZNetView component = ship.GetComponent(); if (component == null || !component.IsValid()) { return 0f; } ZDOID uid = component.GetZDO().m_uid; if (WindGraceTimerMap.TryGetValue(uid, out var value)) { return value; } return 0f; } public static void SetShipGraceTimer(Ship ship, float timer) { if (!(ship == null)) { ZNetView component = ship.GetComponent(); if (!(component == null) && component.IsValid()) { ZDOID uid = component.GetZDO().m_uid; WindGraceTimerMap[uid] = timer; } } } public static float GetShipWindAngle(Ship ship) { if (ship == null) { return 0f; } ZNetView component = ship.GetComponent(); if (component == null || !component.IsValid()) { return 0f; } ZDOID uid = component.GetZDO().m_uid; if (WindAngleMap.TryGetValue(uid, out var value)) { return value; } float num = Mathf.DeltaAngle(0f, ship.GetWindAngle()); WindAngleMap[uid] = num; return num; } public static void SetShipWindAngle(Ship ship, float angle) { if (!(ship == null)) { ZNetView component = ship.GetComponent(); if (!(component == null) && component.IsValid()) { ZDOID uid = component.GetZDO().m_uid; WindAngleMap[uid] = angle; } } } public static long GetControllerForShip(object shipObj) { if (shipObj == null) { return 0L; } if (!(shipObj is Ship ship)) { return 0L; } ZNetView component = ship.GetComponent(); if (component == null || !component.IsValid()) { return 0L; } ZDO zDO = component.GetZDO(); if (zDO == null) { return 0L; } ZDOID uid = zDO.m_uid; if (ControlMap.TryGetValue(uid, out var value)) { if (NjordConfig.Debug_Enable.Value && NjordConfig.Debug_AuthControl.Value && Time.time - _lastLookupLogTime >= 2f) { NjordDebug.Debug($"[Tracker] Lookup: {ship.name} ({uid}) → controller={value}"); _lastLookupLogTime = Time.time; } return value; } if (NjordConfig.Debug_Enable.Value && NjordConfig.Debug_AuthControl.Value && Time.time - _lastLookupLogTime >= 2f) { NjordDebug.Debug($"[Tracker] Lookup: {ship.name} ({uid}) → no entry"); _lastLookupLogTime = Time.time; } return 0L; } private static bool LocalPlayerHasControl() { if (Player.m_localPlayer == null) { return false; } long playerID = Player.m_localPlayer.GetPlayerID(); if (playerID == 0L) { return false; } foreach (KeyValuePair item in ControlMap) { if (item.Value == playerID) { if (NjordConfig.Debug_Enable.Value && NjordConfig.Debug_AuthControl.Value) { NjordDebug.Debug($"[Tracker] LocalPlayerHasControl → TRUE (ship {item.Key})"); } return true; } } if (NjordConfig.Debug_Enable.Value && NjordConfig.Debug_AuthControl.Value) { NjordDebug.Debug("[Tracker] LocalPlayerHasControl → FALSE"); } return false; } private static void RecalculateHudState() { bool flag = Player.m_localPlayer != null; bool flag2 = LocalPlayerHasControl(); bool value = Plugin.HudEnabledByUser.Value; bool flag3 = flag && flag2 && value; if (ShouldShowHud != flag3 && NjordConfig.Debug_Enable.Value && NjordConfig.Debug_AuthControl.Value) { NjordDebug.Debug($"[Tracker] HUD state changed → {flag3} (hasPlayer={flag}, hasControl={flag2}, hudEnabled={value})"); } ShouldShowHud = flag3; } public static void SetControl(Ship ship, long playerID) { if (ship == null) { return; } ZNetView component = ship.GetComponent(); if (component == null || !component.IsValid()) { return; } ZDO zDO = component.GetZDO(); if (zDO != null) { ZDOID uid = zDO.m_uid; ControlMap[uid] = playerID; CurrentController = playerID; Njord_Telemetry.ClearErrors(); if (NjordConfig.Debug_Enable.Value && NjordConfig.Debug_AuthControl.Value) { NjordDebug.Debug($"[Tracker] SetControl: ship={ship.name} ({uid}) → player={playerID}"); } RecalculateHudState(); } } public static void ClearControl(Ship ship) { if (ship == null) { return; } ZNetView component = ship.GetComponent(); if (component == null || !component.IsValid()) { return; } ZDO zDO = component.GetZDO(); if (zDO == null) { return; } ZDOID uid = zDO.m_uid; ControlMap.Remove(uid); WindAngleMap.Remove(uid); long num = Player.m_localPlayer?.GetPlayerID() ?? 0; if (num == CurrentController) { if (NjordConfig.Debug_Enable.Value && NjordConfig.Debug_AuthControl.Value) { NjordDebug.Debug($"[Tracker] ClearControl: local player lost control of {ship.name} ({uid})"); } CurrentController = 0L; } else if (NjordConfig.Debug_Enable.Value && NjordConfig.Debug_AuthControl.Value) { NjordDebug.Debug($"[Tracker] ClearControl: removed entry for {ship.name} ({uid}), local player unaffected"); } RecalculateHudState(); } public static void UpdateController(long controller, float dt) { if (NjordConfig.Debug_Enable.Value && NjordConfig.Debug_AuthControl.Value && Time.time - _lastLookupLogTime >= 2f) { NjordDebug.Debug($"[AuthoritySync] UpdateController: controller={controller}, dt={dt:F4}"); } long num = ((Player.m_localPlayer != null) ? Player.m_localPlayer.GetPlayerID() : 0); if (controller == 0L || (num != 0L && controller == num)) { CurrentController = controller; } if (controller != 0L) { ReadyTimer += dt; if (ReadyTimer >= 0.35f) { SteeringGateReady = true; } } else { ReadyTimer = 0f; SteeringGateReady = false; } RecalculateHudState(); } } } // ---- Njord/plugins/Njord.dll :: Njord.NjordDebug ---- namespace Njord { public static class NjordDebug { private static bool _startupPrinted; private static bool IsEnabled() { try { return NjordConfig.Debug_Enable != null && NjordConfig.Debug_Enable.Value; } catch { return false; } } public static void Startup() { if (!_startupPrinted) { _startupPrinted = true; if (Plugin.Log != null) { Plugin.Log.LogInfo("⚓ [Njord] The longship wakes. The sea remembers your hand."); } } } public static void Info(string msg) { if (IsEnabled()) { Plugin.Log?.LogInfo("[Njord] " + msg); } } public static void Debug(string msg) { if (IsEnabled()) { Plugin.Log?.LogDebug("[Njord] " + msg); } } public static void Warn(string msg) { if (IsEnabled()) { Plugin.Log?.LogWarning("[Njord] " + msg); } } public static void Log(string msg) { Plugin.Log?.LogInfo("[Njord] " + msg); } public static void Error(string msg) { Plugin.Log?.LogError("[Njord] " + msg); } } } // ---- Njord/plugins/Njord.dll :: Njord.Plugin ---- namespace Njord { [BepInPlugin("wubarrk.njord", "Njord", "1.3.0")] public class Plugin : BaseUnityPlugin { internal static Plugin Instance; internal static Harmony Harmony; internal static ManualLogSource Log; public static ConfigEntry HudPositionX; public static ConfigEntry HudPositionY; public static ConfigEntry HudEnabledByUser; private bool _hudCreated; public static bool OdinShipDetected { get; private set; } private void Awake() { Instance = this; Log = base.Logger; NjordDebug.Startup(); Log.LogInfo("The sea stirs. The runes awaken."); OdinShipDetected = Chainloader.PluginInfos.ContainsKey("marlthon.OdinShip"); if (OdinShipDetected) { Log.LogInfo("Njord smiles upon the shipwrights: OdinShip detected! Taking the helm."); } else { Log.LogInfo("Njord watches the waves: OdinShip not detected. We sail on."); } NjordConfig.Bind(base.Config); HudPositionX = base.Config.Bind("HUD", "HudPositionX", 40f, "Njord HUD X position."); HudPositionY = base.Config.Bind("HUD", "HudPositionY", 200f, "Njord HUD Y position."); HudEnabledByUser = base.Config.Bind("HUD", "HudEnabled", defaultValue: true, "If false, Njord HUD is hidden (F7 toggles)."); NjordSync.Initialize(); Harmony = new Harmony("wubarrk.njord"); NjordPatchCaller.ApplyPatches(); NjordHudManager.Initialize(); RegisterConsoleCommands(); if (NjordConfig.Debug_Enable.Value) { Log.LogInfo("Njord console commands registered (including njord.dumpvfx)."); try { Version version = Assembly.GetExecutingAssembly().GetName().Version; Log.LogInfo($"Njord DLL version: {version}"); } catch { } } Log.LogInfo("Njord, god of the sea, obeys your command."); } private void Update() { if (!_hudCreated && ZNet.instance != null) { _hudCreated = true; NjordHudManager.CreateHud(); } if (Input.GetKeyDown(KeyCode.F8)) { if (ZNet.instance != null && ZNet.instance.IsServer()) { NjordSync.ServerReloadAndBroadcast(); NjordDebug.Info("Njord config reloaded (server) and broadcast to all clients."); MessageHud.instance?.ShowMessage(MessageHud.MessageType.TopLeft, "Njord config reloaded (server)"); } else { NjordSync.Client_RequestConfigUpdate(); NjordDebug.Info("Njord config update request sent to server."); MessageHud.instance?.ShowMessage(MessageHud.MessageType.TopLeft, "Njord config update request sent"); } } if (Input.GetKeyDown(KeyCode.F7)) { HudEnabledByUser.Value = !HudEnabledByUser.Value; } } private void OnGUI() { NjordHudManager.OnGUI(); } private void RegisterConsoleCommands() { ConsoleCommand("njord.reload", delegate { if (ZNet.instance == null || !ZNet.instance.IsServer()) { Log.LogWarning("njord.reload can only be run on the server."); } else { NjordSync.ServerReloadAndBroadcast(); Log.LogInfo("Njord config reloaded from disk and broadcast to all clients."); } }); ConsoleCommand("njord.push", delegate { if (ZNet.instance == null || !ZNet.instance.IsServer()) { Log.LogWarning("njord.push can only be run on the server."); } else { typeof(NjordSync).GetMethod("ServerReloadAndBroadcast", BindingFlags.Static | BindingFlags.Public)?.Invoke(null, null); Log.LogInfo("Njord runtime config pushed to all clients."); } }); ConsoleCommand("njord.dumpvfx", delegate { try { NjordRuneSpriteTrail.DumpEmitters(); try { NjordRunePiggyback.DumpEmitters(); } catch { } Log.LogInfo("Njord: dumped VFX emitter state to Njord debug log."); try { MessageHud.instance?.ShowMessage(MessageHud.MessageType.TopLeft, "Njord: VFX dump written to log."); } catch { } } catch (Exception ex) { Log.LogWarning("njord.dumpvfx failed: " + ex.Message); } }); ConsoleCommand("njord.vfx.orientation", delegate(string[] args) { try { if (args.Length < 1) { Log.LogInfo("Usage: njord.vfx.orientation "); } else { string text = args[0].ToLowerInvariant(); switch (text) { case "flat": NjordRunePiggyback_SetOrientation("Flat"); break; case "flatinv": NjordRunePiggyback_SetOrientation("FlatInverted"); break; case "upright": NjordRunePiggyback_SetOrientation("Upright"); break; case "camera": NjordRunePiggyback_SetOrientation("CameraFacing"); break; default: Log.LogInfo("Unknown orientation. Valid: flat, flatinv, upright, camera"); return; } Log.LogInfo("Njord: set VFX spawn orientation -> " + text); } } catch (Exception ex) { Log.LogWarning("njord.vfx.orientation failed: " + ex.Message); } }); ConsoleCommand("njord.testtrail", delegate { try { Player localPlayer = Player.m_localPlayer; if (localPlayer == null) { Log.LogWarning("njord.testtrail: no local player"); } else { Transform transform = localPlayer.transform; NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); Color vFX_Color = njordRuntimeConfig.VFX_Color; float num = 0.6f; Vector3 vector = transform.position - transform.forward * 1f + Vector3.up * 0.5f; for (int i = 0; i < 8; i++) { Vector3 pos = vector - transform.forward * ((float)i * num); NjordRunePiggyback.SpawnAtWorldPosition(pos, vFX_Color); } Log.LogInfo("njord.testtrail: spawned test trail behind player"); } } catch (Exception ex) { Log.LogWarning("njord.testtrail failed: " + ex.Message); } }); if (NjordConfig.Debug_Enable.Value) { Log.LogInfo("Registered console command 'njord.dumpvfx'."); } } public static void NjordRunePiggyback_SetOrientation(string name) { try { Type typeFromHandle = typeof(NjordRunePiggyback); Type nestedType = typeFromHandle.GetNestedType("OrientationMode", BindingFlags.Public | BindingFlags.NonPublic); if (!(nestedType == null)) { object value = Enum.Parse(nestedType, name); FieldInfo field = typeFromHandle.GetField("_spawnOrientation", BindingFlags.Static | BindingFlags.NonPublic); if (field != null) { field.SetValue(null, value); } } } catch { } } private void ConsoleCommand(string name, Action action) { try { new Terminal.ConsoleCommand(name, "", delegate(Terminal.ConsoleEventArgs args) { action(args.Args); }); if (NjordConfig.Debug_Enable.Value) { Log.LogInfo("Registered console command '" + name + "' (immediate)."); } return; } catch (Exception ex) { if (NjordConfig.Debug_Enable.Value) { Log.LogInfo("Immediate registration of console command '" + name + "' failed, will wait for Terminal: " + ex.Message); } } Type typeFromHandle = typeof(Terminal); object obj = null; FieldInfo field = typeFromHandle.GetField("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { obj = field.GetValue(null); } else { PropertyInfo property = typeFromHandle.GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { obj = property.GetValue(null); } } if (obj != null) { try { new Terminal.ConsoleCommand(name, "", delegate(Terminal.ConsoleEventArgs args) { action(args.Args); }); if (NjordConfig.Debug_Enable.Value) { Log.LogInfo("Registered console command '" + name + "' after Terminal detected via reflection."); } return; } catch (Exception ex2) { Log.LogWarning("Failed to register console command '" + name + "' after reflection detect: " + ex2.Message); return; } } StartCoroutine(WaitAndRegister(name, action)); } private IEnumerator WaitAndRegister(string name, Action action) { float timeout = 10f; float start = Time.time; while (Time.time - start < timeout) { Type typeFromHandle = typeof(Terminal); FieldInfo field = typeFromHandle.GetField("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); object obj = null; if (field != null) { obj = field.GetValue(null); } else { PropertyInfo property = typeFromHandle.GetProperty("instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null) { obj = property.GetValue(null); } } if (obj != null) { try { new Terminal.ConsoleCommand(name, "", delegate(Terminal.ConsoleEventArgs args) { action(args.Args); }); if (NjordConfig.Debug_Enable.Value) { Log.LogInfo("Registered console command '" + name + "' after Terminal became available."); } yield break; } catch (Exception ex) { Log.LogWarning("Failed to register console command '" + name + "' after wait: " + ex.Message); yield break; } } yield return null; } Log.LogWarning("Timed out waiting for Terminal to register console command '" + name + "'."); } } } // ---- Njord/plugins/Njord.dll :: Njord.NjordRunePiggyback ---- namespace Njord { public static class NjordRunePiggyback { public enum OrientationMode { Flat, FlatInverted, Upright, CameraFacing } private class Emitter : MonoBehaviour { private Transform _stern; private Color _color = Color.white; private float _spacing = 0.6f; private Vector3 _lastSpawnPos = Vector3.zero; private Action _returnToPool; private Sprite _sprite; private Texture2D _tex; private bool _active = true; private ParticleSystem _wakePS; private ParticleSystem.Particle[] _buffer = new ParticleSystem.Particle[512]; private float _wakeInterval = 0.1f; private float _lastWake; private float _lastSternTime; private Vector3 _lastSternPos = Vector3.zero; private float _minSpeed = 0.5f; private float _lastLogTime = -10f; public void Initialize(Transform stern, Color color, float rateMultiplier, Action returnToPool, Sprite sprite, Texture2D tex) { _stern = stern; _color = color; _returnToPool = returnToPool; _sprite = sprite; _tex = tex; _spacing = 0.6f; _wakeInterval = 0.1f; _minSpeed = 0.5f; try { if (NjordConfig.Debug_Enable != null && NjordConfig.Debug_Enable.Value) { _minSpeed = Mathf.Min(_minSpeed, 0.1f); } } catch { } _lastSpawnPos = ((_stern != null) ? (_stern.TransformPoint(new Vector3(0f, -0.25f, -1f)) - Vector3.one * 1000f) : Vector3.zero); _lastSternPos = ((_stern != null) ? _stern.position : Vector3.zero); _lastSternTime = Time.time; _active = true; try { ParticleSystem[] componentsInChildren = _stern.GetComponentsInChildren(includeInactive: true); ParticleSystem particleSystem = null; ParticleSystem[] array = componentsInChildren; foreach (ParticleSystem particleSystem2 in array) { if (!(particleSystem2 == null)) { string text = particleSystem2.gameObject.name.ToLowerInvariant(); if (text.Contains("wake") || text.Contains("trail") || text.Contains("speed") || text.Contains("mask")) { particleSystem = particleSystem2; break; } } } if (particleSystem == null && componentsInChildren.Length != 0) { particleSystem = componentsInChildren[0]; } _wakePS = particleSystem; if (IsDebugEnabled()) { try { Plugin.Log.LogInfo("[VFX-Piggyback] Emitter init: wakePS=" + ((_wakePS != null) ? _wakePS.gameObject.name : "(none)")); } catch { } } if (!(_wakePS == null)) { return; } _active = false; if (IsDebugEnabled()) { try { Plugin.Log.LogInfo("[VFX-Piggyback] No wake PS found; emitter disabled (sprite trail used instead)"); return; } catch { return; } } } catch { } } public void UpdateParams(Transform stern, Color color, float rateMultiplier) { _stern = stern ?? _stern; _color = color; _spacing = 0.6f; } public void SetActive(bool v) { _active = v; } private void Update() { if (!_active || _stern == null) { return; } NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); float time = Time.time; float num = Mathf.Max(1E-06f, time - _lastSternTime); float num2 = Vector3.Distance(_stern.position, _lastSternPos) / num; _lastSternPos = _stern.position; _lastSternTime = time; if (_wakePS != null) { if (Time.time - _lastWake < _wakeInterval) { return; } _lastWake = Time.time; int particles = _wakePS.GetParticles(_buffer); if (IsDebugEnabled()) { try { Plugin.Log.LogDebug($"[VFX-Piggyback] Wake sample: count={particles} speed={num2} minSpeed={_minSpeed}"); } catch { } } if (particles <= 0) { return; } int num3 = Mathf.Min(4, particles); int num4 = Mathf.Max(1, particles / num3); for (int i = 0; i < particles; i += num4) { Vector3 worldPos = _wakePS.transform.TransformPoint(_buffer[i].position); if (num2 < _minSpeed) { if (!(Time.time - _lastLogTime > 1f)) { continue; } if (IsDebugEnabled()) { try { Plugin.Log.LogInfo($"[VFX-Piggyback] Skipping wake spawn: speed {num2} < min {_minSpeed}"); } catch { } } _lastLogTime = Time.time; } else { TrySpawnAt(worldPos); } } return; } Vector3 vector = _stern.TransformPoint(new Vector3(0f, -0.25f, -1f)); Vector3 vector2 = vector; int num5 = 0; try { num5 = ((LayerMask.NameToLayer("Water") >= 0) ? (1 << LayerMask.NameToLayer("Water")) : 0); } catch { num5 = 0; } if (num5 == 0) { num5 = -1; } if (num5 != 0) { Vector3 origin = vector + Vector3.up * 2f; if (Physics.Raycast(origin, Vector3.down, out var hitInfo, 10f, num5)) { vector2.y = hitInfo.point.y + 0.02f; } } Vector3 lhs = vector2 - _lastSpawnPos; for (float num6 = Vector3.Dot(lhs, _stern.forward); num6 >= _spacing; num6 = Vector3.Dot(lhs, _stern.forward)) { _lastSpawnPos += _stern.forward * _spacing; Vector3 lastSpawnPos = _lastSpawnPos; lastSpawnPos.y = vector2.y; if (num2 >= _minSpeed) { TrySpawnAt(lastSpawnPos); } else if (Time.time - _lastLogTime > 1f) { if (IsDebugEnabled()) { try { Plugin.Log.LogInfo($"[VFX-Piggyback] Skipping trail spawn: speed {num2} < min {_minSpeed}"); } catch { } } _lastLogTime = Time.time; } lhs = vector2 - _lastSpawnPos; } } public bool TrySpawnAt(Vector3 worldPos) { if (!_active || _stern == null) { return false; } float num = Vector3.Distance(worldPos, _lastSpawnPos); if (num < _spacing) { if (Time.time - _lastLogTime > 1f) { if (IsDebugEnabled()) { try { Plugin.Log.LogInfo($"[VFX-Piggyback] TrySpawnAt rejected: dist {num} < spacing {_spacing}"); } catch { } } _lastLogTime = Time.time; } return false; } _lastSpawnPos = worldPos; SpawnVisual(worldPos, _color, _sprite, _tex, null, _stern); return true; } private void DoSpawn(Vector3 pos) { SpawnVisual(pos, _color, _sprite, _tex, null, _stern); } } private class RuneFader : MonoBehaviour { private SpriteRenderer _sr; private Vector3 _vel; private float _life; private float _age; private float _startScale; private Color _startColor; private Action _onFinish; public void Init(Action onFinish) { _sr = GetComponent(); _onFinish = onFinish; } public void Play(Vector3 velocity, float life, float startScale, Color color) { _vel = velocity; _life = Mathf.Max(0.01f, life); _age = 0f; _startScale = Mathf.Max(0.01f, startScale); _startColor = color; if (_sr != null) { _sr.color = color; _sr.transform.localScale = Vector3.one * _startScale; } } private void Update() { float deltaTime = Time.deltaTime; _age += deltaTime; base.transform.position += _vel * deltaTime; float t = Mathf.Clamp01(_age / _life); if (_sr != null) { Color startColor = _startColor; startColor.a = Mathf.Lerp(_startColor.a, 0f, t); _sr.color = startColor; float num = Mathf.Lerp(_startScale, _startScale * 0.4f, t); _sr.transform.localScale = Vector3.one * num; } if (_age >= _life) { _onFinish?.Invoke(base.gameObject); } } } private const int POOL_INITIAL = 64; private const int POOL_MAX = 1024; private static Queue _pool = new Queue(); private static GameObject _poolRoot; private static Texture2D _runeTex; private static Sprite _runeSprite; private static Mesh _debugQuadMesh; private static bool _inited = false; private static OrientationMode _spawnOrientation = OrientationMode.Flat; private static readonly Dictionary _emitters = new Dictionary(); private static bool IsDebugEnabled() { try { NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); if (njordRuntimeConfig != null && njordRuntimeConfig.Debug_Enable) { return true; } } catch { } try { if (NjordConfig.Debug_Enable != null && NjordConfig.Debug_Enable.Value) { return true; } } catch { } return false; } private static void EnsureInit() { if (_inited) { return; } _inited = true; _runeTex = NjordRuneTextureGenerator.GenerateRune(NjordConfig.VFX_Color?.Value ?? new Color(0.3f, 0.6f, 1f, 1f)); if (_runeTex != null) { _runeSprite = Sprite.Create(_runeTex, new Rect(0f, 0f, _runeTex.width, _runeTex.height), new Vector2(0.5f, 0.5f), 100f); } else { Texture2D texture2D = new Texture2D(16, 16, TextureFormat.RGBA32, mipChain: false); Color[] array = new Color[256]; for (int i = 0; i < array.Length; i++) { array[i] = Color.white; } texture2D.SetPixels(array); texture2D.Apply(updateMipmaps: false); _runeTex = texture2D; _runeSprite = Sprite.Create(_runeTex, new Rect(0f, 0f, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f), 100f); } _poolRoot = new GameObject("NjordRunePiggybackPool"); UnityEngine.Object.DontDestroyOnLoad(_poolRoot); for (int j = 0; j < 64; j++) { _pool.Enqueue(CreatePooledGO()); } } private static Mesh GetOrCreateDebugQuadMesh() { if (_debugQuadMesh != null) { return _debugQuadMesh; } Mesh mesh = new Mesh(); mesh.name = "Njord_DebugQuadMesh"; Vector3[] vertices = new Vector3[4] { new Vector3(-0.5f, 0f, -0.5f), new Vector3(0.5f, 0f, -0.5f), new Vector3(0.5f, 0f, 0.5f), new Vector3(-0.5f, 0f, 0.5f) }; int[] triangles = new int[6] { 0, 1, 2, 0, 2, 3 }; Vector2[] uv = new Vector2[4] { new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(0f, 1f) }; mesh.vertices = vertices; mesh.triangles = triangles; mesh.uv = uv; mesh.RecalculateNormals(); mesh.RecalculateBounds(); _debugQuadMesh = mesh; return _debugQuadMesh; } private static GameObject CreatePooledGO() { GameObject gameObject = new GameObject("NjordRune"); gameObject.transform.SetParent(_poolRoot.transform, worldPositionStays: false); SpriteRenderer spriteRenderer = gameObject.AddComponent(); spriteRenderer.sprite = _runeSprite; spriteRenderer.sortingLayerName = "Default"; spriteRenderer.sortingOrder = 5000; spriteRenderer.shadowCastingMode = ShadowCastingMode.Off; try { Shader shader = Shader.Find("Sprites/Default"); if (shader != null) { Material material = new Material(shader); if (material.HasProperty("_MainTex") && _runeTex != null) { material.SetTexture("_MainTex", _runeTex); } spriteRenderer.material = material; } } catch { } RuneFader runeFader = gameObject.AddComponent(); runeFader.Init(ReturnToPool); try { GameObject gameObject2 = new GameObject("Njord_DebugQuad"); gameObject2.transform.SetParent(gameObject.transform, worldPositionStays: false); gameObject2.transform.localPosition = Vector3.zero; gameObject2.transform.localRotation = Quaternion.identity; MeshFilter meshFilter = gameObject2.AddComponent(); meshFilter.mesh = GetOrCreateDebugQuadMesh(); MeshRenderer meshRenderer = gameObject2.AddComponent(); meshRenderer.shadowCastingMode = ShadowCastingMode.Off; meshRenderer.receiveShadows = false; meshRenderer.enabled = false; } catch { } gameObject.SetActive(value: false); return gameObject; } private static GameObject GetFromPool() { if (!_inited) { EnsureInit(); } if (_pool.Count > 0) { return _pool.Dequeue(); } if (_pool.Count + 1 <= 1024) { return CreatePooledGO(); } return null; } private static void ReturnToPool(GameObject go) { if (!(go == null)) { go.SetActive(value: false); go.transform.SetParent(_poolRoot.transform, worldPositionStays: false); if (_pool.Count < 1024) { _pool.Enqueue(go); } else { UnityEngine.Object.Destroy(go); } } } public static void SpawnBurst(Vector3 pos, Color color, int count) { if (!NjordConfig.VFX_Enable.Value) { return; } EnsureInit(); float life = 2.5f; float startScale = 1.125f; for (int i = 0; i < Mathf.Max(1, count); i++) { GameObject fromPool = GetFromPool(); if (!(fromPool == null)) { SpriteRenderer component = fromPool.GetComponent(); if (component != null) { component.sprite = _runeSprite; component.color = color; } fromPool.transform.position = pos + UnityEngine.Random.insideUnitSphere * 0.35f; fromPool.transform.rotation = Quaternion.Euler(90f, UnityEngine.Random.Range(0f, 360f), 0f); fromPool.SetActive(value: true); RuneFader component2 = fromPool.GetComponent(); if (component2 != null) { Vector3 velocity = (UnityEngine.Random.onUnitSphere + Vector3.back * 1.5f) * 0.4f; component2.Play(velocity, life, startScale, color); } } } } public static void StartTrailForShip(object ship, Transform stern, Color color, float rateMultiplier = 1f) { if (ship == null || stern == null || !NjordConfig.VFX_Enable.Value) { return; } EnsureInit(); if (_emitters.TryGetValue(ship, out var value) && value != null) { value.SetActive(v: true); value.UpdateParams(stern, color, rateMultiplier); return; } GameObject gameObject = new GameObject($"NjordRuneEmitter_{ship.GetHashCode()}"); gameObject.transform.SetParent(stern, worldPositionStays: true); gameObject.transform.position = stern.TransformPoint(new Vector3(0f, -0.25f, -1f)); Emitter emitter = gameObject.AddComponent(); try { emitter.Initialize(stern, color, rateMultiplier, ReturnToPool, _runeSprite, _runeTex); _emitters[ship] = emitter; if (IsDebugEnabled()) { try { NjordDebug.Info($"[VFX-Piggyback] Created emitter for ship {ship} (stern={stern?.name})"); } catch { } } if (IsDebugEnabled()) { try { Plugin.Log.LogInfo($"[VFX-Piggyback] Created emitter for ship {ship} (stern={stern?.name})"); return; } catch { return; } } } catch (Exception ex) { if (IsDebugEnabled()) { try { Plugin.Log.LogWarning($"[VFX-Piggyback] Failed to initialize emitter for ship {ship}: {ex.Message}"); } catch { } } if (IsDebugEnabled()) { try { NjordDebug.Warn($"[VFX-Piggyback] Failed to initialize emitter for ship {ship}: {ex.Message}"); } catch { } } try { if (emitter != null && emitter.gameObject != null) { UnityEngine.Object.Destroy(emitter.gameObject); } } catch { } } } public static void StopTrailForShip(object ship) { if (ship != null && _emitters.TryGetValue(ship, out var value) && value != null) { value.SetActive(v: false); } } public static void RemoveEmitterForShip(object ship) { if (ship == null) { return; } if (_emitters.TryGetValue(ship, out var value) && value != null) { try { if (value.gameObject != null) { UnityEngine.Object.Destroy(value.gameObject); } } catch { } } _emitters.Remove(ship); } public static void DumpEmitters() { try { if (IsDebugEnabled()) { try { Plugin.Log.LogInfo($"[VFX-Piggyback] Emitters: count={_emitters.Count} pool={_pool.Count}"); } catch { } } foreach (KeyValuePair emitter in _emitters) { object key = emitter.Key; Emitter value = emitter.Value; if (value == null) { if (IsDebugEnabled()) { try { Plugin.Log.LogInfo($"[VFX-Piggyback] Emitter for {key}: null"); } catch { } } } else if (IsDebugEnabled()) { try { Plugin.Log.LogInfo($"[VFX-Piggyback] Emitter for {key}: active={value.isActiveAndEnabled} go={value.gameObject?.name}"); } catch { } } } } catch { } } public static void SpawnAtWorldPosition(Vector3 pos, Color color) { SpawnVisual(pos, color, _runeSprite, _runeTex, null, null); } private static void SpawnVisual(Vector3 pos, Color color, Sprite sprite, Texture2D tex, Vector3? velocityOverride, Transform stern) { if (!NjordConfig.VFX_Enable.Value) { return; } EnsureInit(); GameObject fromPool = GetFromPool(); if (fromPool == null) { return; } SpriteRenderer component = fromPool.GetComponent(); if (component != null) { component.sprite = sprite ?? _runeSprite; component.color = color; component.sortingLayerName = "Default"; component.sortingOrder = 32767; } fromPool.transform.position = pos; try { switch (_spawnOrientation) { case OrientationMode.Flat: fromPool.transform.rotation = Quaternion.Euler(90f, UnityEngine.Random.Range(0f, 360f), 0f); break; case OrientationMode.FlatInverted: fromPool.transform.rotation = Quaternion.Euler(-90f, UnityEngine.Random.Range(0f, 360f), 0f); break; case OrientationMode.Upright: fromPool.transform.rotation = Quaternion.Euler(0f, UnityEngine.Random.Range(0f, 360f), 0f); break; case OrientationMode.CameraFacing: if (Camera.main != null) { fromPool.transform.rotation = Quaternion.LookRotation(Camera.main.transform.forward); } else { fromPool.transform.rotation = Quaternion.Euler(90f, UnityEngine.Random.Range(0f, 360f), 0f); } break; default: fromPool.transform.rotation = Quaternion.Euler(90f, UnityEngine.Random.Range(0f, 360f), 0f); break; } } catch { fromPool.transform.rotation = Quaternion.Euler(90f, UnityEngine.Random.Range(0f, 360f), 0f); } fromPool.SetActive(value: true); float life = 2.5f; float startScale = 0.75f; Vector3 velocity = (velocityOverride.HasValue ? velocityOverride.Value : ((!(stern != null)) ? (Vector3.back * 0.6f + UnityEngine.Random.insideUnitSphere * 0.2f) : (-stern.forward * UnityEngine.Random.Range(0.6f, 1.2f) + stern.right * UnityEngine.Random.Range(-0.25f, 0.25f) + stern.up * UnityEngine.Random.Range(-0.05f, 0.05f)))); RuneFader component2 = fromPool.GetComponent(); if (component2 != null) { try { component2.Play(velocity, life, startScale, color); } catch { try { component2.Play(velocity, life, startScale, color); } catch { } } } try { fromPool.layer = 0; SpriteRenderer component3 = fromPool.GetComponent(); if (component3 != null) { component3.sortingOrder = 32767; } } catch { } try { if (IsDebugEnabled()) { Transform transform = fromPool.transform.Find("Njord_DebugQuad"); if (transform != null) { MeshRenderer component4 = transform.GetComponent(); if (component4 != null) { if (component4.sharedMaterial == null) { Shader shader = Shader.Find("Unlit/Texture") ?? Shader.Find("Unlit/Color") ?? Shader.Find("Sprites/Default"); if (shader != null) { try { Material material = new Material(shader); if (material.HasProperty("_MainTex") && tex != null) { material.SetTexture("_MainTex", tex); } if (material.HasProperty("_Color")) { material.SetColor("_Color", color); } component4.material = material; } catch { } } } transform.localScale = Vector3.one * 0.3f; component4.enabled = true; try { component4.material.renderQueue = 5000; } catch { } if (IsDebugEnabled()) { try { Plugin.Log.LogInfo($"[VFX-Piggyback] Spawned debug quad at {pos}"); } catch { } } } } } } catch { } if (!IsDebugEnabled()) { return; } try { Plugin.Log.LogInfo($"[VFX-Piggyback] DoSpawn at {pos}"); } catch { } } } } // ---- Njord/plugins/Njord.dll :: Njord.NjordRuneSpriteTrail ---- namespace Njord { public static class NjordRuneSpriteTrail { private class Emitter : MonoBehaviour { private Transform _stern; private Color _color = Color.white; private float _life = 1.2f; private float _scale = 0.7f; private float _spacing = 0.6f; private Vector3 _lastSpawnPos; private Action _returnToPool; private Sprite _sprite; private bool _active = true; private bool _useTelemetry; private float _cachedWaterY = float.MinValue; private float _lastWaterSampleTime = -10f; private const float WATER_SAMPLE_INTERVAL = 0.1f; private const float WATER_SURFACE_OFFSET = 0.02f; private int _waterMask; private bool _usedAllLayersFallback; private bool _loggedLayerResolution; private float _lastSpawnDebugLog = -10f; private const float SPAWN_DEBUG_LOG_INTERVAL = 1f; private bool _debugNotified; public void Initialize(Transform stern, Color color, float rateMultiplier, Action returnToPool, Sprite sprite, bool useTelemetry) { _stern = stern; _useTelemetry = useTelemetry; _color = color; _sprite = sprite; _returnToPool = returnToPool; _active = true; NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); _life = 2.5f; _scale = 0.41343752f; _spacing = 0.49980003f; _lastSpawnPos = ((_stern != null) ? _stern.position : Vector3.zero); if (_useTelemetry && _stern != null) { try { NjordDebug.Debug($"[VFX-Sprite] Telemetry emitter seeded cachedY={_cachedWaterY = GetRealisticTelemetryWaterY():F2}"); } catch { } } string text = null; string text2 = null; List list = new List(); if (!string.IsNullOrEmpty(text)) { list.Add(text); } list.AddRange(new string[7] { "Water", "water", "WaterSurface", "Ocean", "Sea", "SeaWater", "WaterPlane" }); foreach (string item in list) { if (!string.IsNullOrEmpty(item)) { int num = LayerMask.NameToLayer(item); if (num >= 0) { _waterMask = 1 << num; text2 = item; _usedAllLayersFallback = false; break; } } } if (text2 == null) { ConfigEntry debug_Enable = NjordConfig.Debug_Enable; if (debug_Enable != null && debug_Enable.Value) { _waterMask = -1; _usedAllLayersFallback = true; } else { _waterMask = 0; _usedAllLayersFallback = false; } } if (!_loggedLayerResolution) { string arg = text2 ?? (_usedAllLayersFallback ? "(AllLayers)" : "(none)"); NjordDebug.Debug($"[VFX-Sprite] Resolved water layer -> '{arg}' mask={_waterMask}"); _loggedLayerResolution = true; } if (_useTelemetry || !(_stern != null)) { return; } try { int num2 = _waterMask; if (num2 == 0) { ConfigEntry debug_Enable2 = NjordConfig.Debug_Enable; if (debug_Enable2 != null && debug_Enable2.Value) { num2 = -1; } } if (num2 != 0 && (Physics.Raycast(_stern.position, Vector3.down, out var hitInfo, 10f, num2) || Physics.Raycast(_stern.position, Vector3.up, out hitInfo, 10f, num2))) { _cachedWaterY = hitInfo.point.y; NjordDebug.Debug($"[VFX-Sprite] Immediate water sample cachedY={_cachedWaterY:F2}"); } } catch { } } public void UpdateParams(Transform stern, Color color, float rateMultiplier) { _stern = stern ?? _stern; _color = color; _spacing = 0.6f; } public void SetActive(bool v) { _active = v; } private float GetRealisticTelemetryWaterY() { float num = ((_stern != null) ? _stern.position.y : 0f); float waterLevel = Njord_Telemetry.WaterLevel; float boatY = Njord_Telemetry.BoatY; if (float.IsNaN(waterLevel) || float.IsInfinity(waterLevel)) { return num; } float value = waterLevel; if (_stern != null && !float.IsNaN(boatY) && !float.IsInfinity(boatY) && Mathf.Abs(boatY) > 0.0001f) { value = waterLevel + (num - boatY); } return Mathf.Clamp(value, num - 3f, num + 3f); } private void Update() { if (!_active) { return; } if (_stern == null) { _active = false; NjordDebug.Warn("[VFX-Sprite] Emitter stern destroyed; deactivating."); return; } if (_useTelemetry) { try { float waterLevel = Njord_Telemetry.WaterLevel; float boatY = Njord_Telemetry.BoatY; if (!float.IsNaN(waterLevel) && !float.IsInfinity(waterLevel)) { float value = waterLevel; if (!float.IsNaN(boatY) && Mathf.Abs(boatY) > 0.0001f) { value = waterLevel + (_stern.position.y - boatY); } _cachedWaterY = Mathf.Clamp(value, _stern.position.y - 3f, _stern.position.y + 3f); } } catch { } } else if (Time.time - _lastWaterSampleTime > 0.1f) { _lastWaterSampleTime = Time.time; int num = _waterMask; if (num == 0) { ConfigEntry debug_Enable = NjordConfig.Debug_Enable; if (debug_Enable != null && debug_Enable.Value) { num = -1; } } if (num != 0) { try { if (Physics.Raycast(_stern.position, Vector3.down, out var hitInfo, 10f, num) || Physics.Raycast(_stern.position, Vector3.up, out hitInfo, 10f, num)) { _cachedWaterY = hitInfo.point.y; } } catch { } } } if (_cachedWaterY == float.MinValue) { _cachedWaterY = _stern.position.y; } float num2 = _stern.position.x - _lastSpawnPos.x; float num3 = _stern.position.z - _lastSpawnPos.z; if (!(num2 * num2 + num3 * num3 < _spacing * _spacing)) { Vector3 vector = new Vector3(_stern.position.x, _cachedWaterY + 0.02f, _stern.position.z); Vector3 normalized = new Vector3(_stern.right.x, 0f, _stern.right.z).normalized; SpawnOneAt(vector + normalized * UnityEngine.Random.Range(-0.15f, 0.15f), 1.1475f); SpawnOneAt(vector - normalized * 0.9f + normalized * UnityEngine.Random.Range(-0.15f, 0.15f), 0.837f); SpawnOneAt(vector + normalized * 0.9f + normalized * UnityEngine.Random.Range(-0.15f, 0.15f), 0.837f); SpawnOneAt(vector - normalized * 1.8f + normalized * UnityEngine.Random.Range(-0.15f, 0.15f), 1.1475f); SpawnOneAt(vector + normalized * 1.8f + normalized * UnityEngine.Random.Range(-0.15f, 0.15f), 1.1475f); _lastSpawnPos = _stern.position; } } public bool TrySpawnAt(Vector3 worldPos) { if (!_active || _stern == null) { return false; } float num = worldPos.x - _lastSpawnPos.x; float num2 = worldPos.z - _lastSpawnPos.z; if (num * num + num2 * num2 < _spacing * _spacing) { return false; } if (_useTelemetry) { try { worldPos.y = GetRealisticTelemetryWaterY() + 0.02f; } catch { } } _lastSpawnPos = worldPos; SpawnOneAt(worldPos); return true; } private void SpawnOneAt(Vector3 spawnPos, float scaleMultiplier = 1f) { if (_useTelemetry) { try { spawnPos.y = GetRealisticTelemetryWaterY() + 0.02f; } catch { } } GameObject fromPool = GetFromPool(); if (fromPool == null) { return; } SpriteRenderer component = fromPool.GetComponent(); if (component != null) { component.sprite = ((_runeSprites != null && _runeSprites.Length != 0) ? _runeSprites[UnityEngine.Random.Range(0, _runeSprites.Length)] : _sprite); component.color = _color; if (component.sprite == null) { NjordDebug.Warn($"[VFX-Sprite] SpawnOneAt: sprite is null for spawned GO at {spawnPos}"); } try { NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); if (njordRuntimeConfig != null && njordRuntimeConfig.Debug_Enable) { Shader shader = Shader.Find("Sprites/Default"); if (shader != null) { try { component.material = new Material(shader); } catch { } } } } catch { } } fromPool.transform.position = spawnPos; fromPool.transform.rotation = Quaternion.Euler(UnityEngine.Random.Range(0f, 360f), UnityEngine.Random.Range(0f, 360f), UnityEngine.Random.Range(0f, 360f)); fromPool.SetActive(value: true); try { NjordRuntimeConfig njordRuntimeConfig2 = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); if (njordRuntimeConfig2 != null && njordRuntimeConfig2.Debug_Enable) { if (Time.time - _lastSpawnDebugLog > 1f) { string text = ((component == null || component.sprite == null) ? "(sprite-null)" : "(sprite-ok)"); NjordDebug.Info($"[VFX-Sprite] SpawnOneAt: pos={spawnPos} cachedY={_cachedWaterY} {text} goLayer={fromPool.layer} sortingOrder={((component != null) ? component.sortingOrder : (-1))} active={fromPool.activeSelf}"); _lastSpawnDebugLog = Time.time; } if (!_debugNotified) { try { MessageHud.instance?.ShowMessage(MessageHud.MessageType.TopLeft, "Njord VFX debug spawns active"); } catch { } _debugNotified = true; } try { if (component != null) { Shader shader2 = Shader.Find("Sprites/Default"); if (shader2 != null) { try { component.material = new Material(shader2); } catch { } } component.sortingOrder = 32767; } fromPool.layer = 0; try { Transform transform = fromPool.transform.Find("Njord_DebugQuad"); if (transform != null) { MeshRenderer component2 = transform.GetComponent(); MeshFilter component3 = transform.GetComponent(); if (component2 != null && component3 != null) { if (component2.sharedMaterial == null) { Shader shader3 = Shader.Find("Unlit/Texture") ?? Shader.Find("Unlit/Color") ?? Shader.Find("Sprites/Default"); if (shader3 != null) { try { Material material = new Material(shader3); if (material.HasProperty("_MainTex") && _runeTex != null) { material.SetTexture("_MainTex", _runeTex); } if (material.HasProperty("_Color")) { material.SetColor("_Color", _color); } material.renderQueue = 5000; component2.material = material; } catch { } } } try { NjordRuntimeConfig njordRuntimeConfig3 = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); float num = Mathf.Max(0.25f, njordRuntimeConfig3.Debug_Enable ? 1.5f : 0.6f); transform.localScale = Vector3.one * num; } catch { transform.localScale = Vector3.one * 0.6f; } component2.enabled = true; try { component2.material.renderQueue = 5000; } catch { } } } } catch { } } catch { } } } catch { } RuneSprite_Fader component4 = fromPool.GetComponent(); if (component4 != null) { float num2 = UnityEngine.Random.Range(1.5f, 3.5f); float num3 = UnityEngine.Random.Range(0.2f, 0.7f); float num4 = UnityEngine.Random.Range(-0.4f, 0.4f); Vector3 velocity = Vector3.up * num2 + -_stern.forward * num3 + _stern.right * num4; component4.Play(velocity, _life, _scale * scaleMultiplier, _color); } } } private class RuneSprite_Fader : MonoBehaviour { private SpriteRenderer _sr; private Vector3 _vel; private Vector3 _angularVelocity; private float _life; private float _age; private float _startScale; private Color _startColor; private Action _onFinish; private const float GRAVITY = -0.35f; private const float DRAG = 2.8f; public void Init(Action onFinish) { _sr = GetComponent(); _onFinish = onFinish; } public void Play(Vector3 velocity, float life, float startScale, Color color) { _vel = velocity; _life = Mathf.Max(0.01f, life); _age = 0f; _startScale = Mathf.Max(0.01f, startScale); _startColor = color; _angularVelocity = new Vector3(UnityEngine.Random.Range(-240f, 240f), UnityEngine.Random.Range(-240f, 240f), UnityEngine.Random.Range(-240f, 240f)); if (_sr != null) { _sr.color = color; _sr.transform.localScale = Vector3.one * _startScale; } } private void Update() { float deltaTime = Time.deltaTime; _age += deltaTime; _vel *= Mathf.Exp(-2.8f * deltaTime); _vel.y += -0.35f * deltaTime; base.transform.position += _vel * deltaTime; base.transform.Rotate(_angularVelocity * deltaTime, Space.World); float num = Mathf.Clamp01(_age / _life); if (_sr != null) { float num2; if (num < 0.3f) { num2 = 1f; } else { float num3 = (num - 0.3f) / 0.7f; num3 *= num3; num2 = 1f - num3; num2 *= 1f + 0.28f * Mathf.Sin(_age * 18f); num2 = Mathf.Clamp01(num2); } Color startColor = _startColor; startColor.a = _startColor.a * num2; _sr.color = startColor; float num4 = ((num < 0.3f) ? _startScale : Mathf.Lerp(_startScale, 0f, (num - 0.3f) / 0.7f)); _sr.transform.localScale = Vector3.one * Mathf.Max(0f, num4); _sr.transform.localScale = Vector3.one * num4; } if (_age >= _life) { _onFinish?.Invoke(base.gameObject); } } } private class WakeSampler : MonoBehaviour { private ParticleSystem _ps; private ParticleSystem.Particle[] _buffer = new ParticleSystem.Particle[512]; private float _interval = 0.1f; private float _last; private Color _color = Color.white; private Emitter _emitterRef; public void Init(ParticleSystem ps, Color color, float rate, Emitter emitterRef) { _ps = ps; _color = color; _interval = Mathf.Max(0.05f, 1f / Mathf.Max(1f, rate)); _last = Time.time - _interval; _emitterRef = emitterRef; } private void Update() { try { NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); if (!njordRuntimeConfig.VFX_Enable || _ps == null || _emitterRef == null || Time.time - _last < _interval) { return; } _last = Time.time; int particles = _ps.GetParticles(_buffer); if (particles <= 0) { return; } int num = Mathf.Min(4, particles); int num2 = Mathf.Max(1, particles / num); for (int i = 0; i < particles; i += num2) { Vector3 worldPos = _ps.transform.TransformPoint(_buffer[i].position); try { _emitterRef.TrySpawnAt(worldPos); } catch { } } } catch { } } } private const int POOL_INITIAL = 64; private const int POOL_MAX = 1024; private const float VISIBILITY_MULT = 5f; private static Queue _pool = new Queue(); private static GameObject _poolRoot; private static Sprite[] _runeSprites; private static Sprite _runeSprite; private static Texture2D _runeTex; private static Mesh _debugQuadMesh; private static readonly Dictionary _emitters = new Dictionary(); private static readonly Dictionary _wakeSamplers = new Dictionary(); private static bool _inited = false; private static void EnsureInit(Color color) { if (_inited) { return; } _inited = true; _runeSprites = new Sprite[16]; for (int i = 0; i < 16; i++) { Texture2D texture2D = NjordRuneTextureGenerator.GenerateRune(Color.white, i); if (texture2D == null) { texture2D = new Texture2D(16, 16, TextureFormat.RGBA32, mipChain: false); Color[] array = new Color[256]; for (int j = 0; j < array.Length; j++) { array[j] = Color.white; } texture2D.SetPixels(array); texture2D.Apply(updateMipmaps: false); } _runeSprites[i] = Sprite.Create(texture2D, new Rect(0f, 0f, texture2D.width, texture2D.height), new Vector2(0.5f, 0.5f), 100f); } _runeTex = NjordRuneTextureGenerator.GenerateRune(Color.white, 0); _runeSprite = _runeSprites[0]; NjordDebug.Debug($"[VFX-Sprite] Generated {16} rune sprites."); _poolRoot = new GameObject("NjordRuneSpritePool"); UnityEngine.Object.DontDestroyOnLoad(_poolRoot); for (int k = 0; k < 64; k++) { _pool.Enqueue(CreatePooledGO()); } NjordDebug.Log("[VFX-Sprite] Initialized rune sprite trail pool."); } private static GameObject CreatePooledGO() { GameObject gameObject = new GameObject("NjordRuneSprite"); gameObject.transform.SetParent(_poolRoot.transform, worldPositionStays: false); SpriteRenderer spriteRenderer = gameObject.AddComponent(); spriteRenderer.sprite = _runeSprite; spriteRenderer.sortingLayerName = "Default"; spriteRenderer.sortingOrder = 5000; spriteRenderer.shadowCastingMode = ShadowCastingMode.Off; RuneSprite_Fader runeSprite_Fader = gameObject.AddComponent(); runeSprite_Fader.Init(ReturnToPool); try { GameObject gameObject2 = new GameObject("Njord_DebugQuad"); gameObject2.transform.SetParent(gameObject.transform, worldPositionStays: false); gameObject2.transform.localPosition = Vector3.zero; gameObject2.transform.localRotation = Quaternion.identity; MeshFilter meshFilter = gameObject2.AddComponent(); meshFilter.mesh = GetOrCreateDebugQuadMesh(); MeshRenderer meshRenderer = gameObject2.AddComponent(); meshRenderer.shadowCastingMode = ShadowCastingMode.Off; meshRenderer.receiveShadows = false; meshRenderer.enabled = false; gameObject2.SetActive(value: true); } catch { } gameObject.SetActive(value: false); return gameObject; } private static GameObject GetFromPool() { if (_pool.Count > 0) { return _pool.Dequeue(); } if (_pool.Count + 1 <= 1024) { return CreatePooledGO(); } return null; } private static void ReturnToPool(GameObject go) { if (!(go == null)) { go.SetActive(value: false); go.transform.SetParent(_poolRoot.transform, worldPositionStays: false); if (_pool.Count < 1024) { _pool.Enqueue(go); } else { UnityEngine.Object.Destroy(go); } } } private static Mesh GetOrCreateDebugQuadMesh() { if (_debugQuadMesh != null) { return _debugQuadMesh; } Mesh mesh = new Mesh(); mesh.name = "Njord_DebugQuadMesh"; Vector3[] vertices = new Vector3[4] { new Vector3(-0.5f, 0f, -0.5f), new Vector3(0.5f, 0f, -0.5f), new Vector3(0.5f, 0f, 0.5f), new Vector3(-0.5f, 0f, 0.5f) }; int[] triangles = new int[6] { 0, 1, 2, 0, 2, 3 }; Vector2[] uv = new Vector2[4] { new Vector2(0f, 0f), new Vector2(1f, 0f), new Vector2(1f, 1f), new Vector2(0f, 1f) }; mesh.vertices = vertices; mesh.triangles = triangles; mesh.uv = uv; mesh.RecalculateNormals(); mesh.RecalculateBounds(); _debugQuadMesh = mesh; return _debugQuadMesh; } public static void SpawnBurst(Vector3 pos, Color color, int count) { NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); if (!njordRuntimeConfig.VFX_Enable) { return; } EnsureInit(color); float life = 12.5f; float startScale = 1.125f; for (int i = 0; i < Mathf.Max(1, count); i++) { GameObject fromPool = GetFromPool(); if (!(fromPool == null)) { SpriteRenderer component = fromPool.GetComponent(); if (component != null) { component.sprite = ((_runeSprites != null) ? _runeSprites[UnityEngine.Random.Range(0, _runeSprites.Length)] : _runeSprite); component.color = color; } fromPool.transform.position = pos + UnityEngine.Random.insideUnitSphere * 0.35f; fromPool.transform.rotation = Quaternion.Euler(90f, UnityEngine.Random.Range(0f, 360f), 0f); fromPool.SetActive(value: true); RuneSprite_Fader component2 = fromPool.GetComponent(); if (component2 != null) { Vector3 velocity = (UnityEngine.Random.onUnitSphere + Vector3.back * 1.5f) * 0.4f; component2.Play(velocity, life, startScale, color); } } } } public static void SpawnAtWorldPosition(Vector3 pos, Color color) { NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); if (!njordRuntimeConfig.VFX_Enable) { return; } EnsureInit(color); GameObject fromPool = GetFromPool(); if (!(fromPool == null)) { SpriteRenderer component = fromPool.GetComponent(); if (component != null) { component.sprite = ((_runeSprites != null) ? _runeSprites[UnityEngine.Random.Range(0, _runeSprites.Length)] : _runeSprite); component.color = color; } fromPool.transform.position = pos; fromPool.transform.rotation = Quaternion.Euler(90f, UnityEngine.Random.Range(0f, 360f), 0f); fromPool.SetActive(value: true); RuneSprite_Fader component2 = fromPool.GetComponent(); if (component2 != null) { Vector3 velocity = Vector3.back * 0.6f + UnityEngine.Random.insideUnitSphere * 0.2f; float life = 2.5f; float startScale = 0.3f; component2.Play(velocity, life, startScale, color); } } } public static void StartTrailForShip(object ship, Transform stern, Color color, float rateMultiplier = 1f) { if (ship == null || stern == null) { return; } NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); if (!njordRuntimeConfig.VFX_Enable) { return; } EnsureInit(color); if (_emitters.TryGetValue(ship, out var value) && value != null) { value.SetActive(v: true); value.UpdateParams(stern, color, rateMultiplier); return; } Transform parent = stern; try { if (ship is Component component && component.transform != null) { parent = component.transform; } } catch { } GameObject gameObject = new GameObject($"NjordRuneSpriteEmitter_{ship.GetHashCode()}"); gameObject.transform.SetParent(parent, worldPositionStays: true); long controllerForShip = Njord_ShipControlTracker.GetControllerForShip(ship); long num = Player.m_localPlayer?.GetPlayerID() ?? 0; bool flag = controllerForShip != 0L && num != 0L && controllerForShip == num; Emitter emitter = gameObject.AddComponent(); emitter.Initialize(stern, color, rateMultiplier, ReturnToPool, _runeSprite, flag); try { if (flag) { gameObject.transform.position = stern.position - stern.forward * 1f + stern.up * 0.5f; } else { float num2 = 0.25f; float num3 = 1f; float x = 0f; if (ship is Component component2) { string text = component2.gameObject.name.Replace("(Clone)", ""); if (text.Contains("VikingShip_Ashlands")) { x = 0.55f; num3 = 2.2f; } else if (text.Contains("VikingShip")) { num3 = 2.2f; } } gameObject.transform.position = stern.TransformPoint(new Vector3(x, 0f - num2, 0f - num3)); } } catch { } _emitters[ship] = emitter; NjordDebug.Log("[VFX-Sprite] Created sprite emitter for ship."); } public static void StopTrailForShip(object ship) { if (ship != null && _emitters.TryGetValue(ship, out var value) && value != null) { value.SetActive(v: false); } } public static void RemoveEmitterForShip(object ship) { if (ship == null) { return; } if (_emitters.TryGetValue(ship, out var value) && value != null) { try { if (value.gameObject != null) { UnityEngine.Object.Destroy(value.gameObject); } } catch { } } _emitters.Remove(ship); if (!_wakeSamplers.TryGetValue(ship, out var value2) || !(value2 != null)) { return; } try { if (value2.gameObject != null) { UnityEngine.Object.Destroy(value2.gameObject); } } catch { } _wakeSamplers.Remove(ship); } public static void DumpEmitters() { NjordDebug.Info($"[VFX-Sprite] Emitters: count={_emitters.Count} pool={_pool.Count}"); foreach (KeyValuePair emitter in _emitters) { object key = emitter.Key; Emitter value = emitter.Value; if (value == null) { NjordDebug.Info($"[VFX-Sprite] Emitter for {key}: null"); continue; } Type type = value.GetType(); object obj = type.GetField("_active", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(value); object obj2 = type.GetField("_cachedWaterY", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(value); object obj3 = type.GetField("_lastWaterSampleTime", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(value); object obj4 = type.GetField("_waterMask", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(value); object obj5 = type.GetField("_useTelemetry", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(value); object obj6 = type.GetField("_lastSpawnPos", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(value); object obj7 = type.GetField("_stern", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(value); bool flag = ((obj7 is UnityEngine.Object obj8) ? (obj8 != null) : (obj7 != null)); NjordDebug.Info($"[VFX-Sprite] Emitter for {key}: active={obj} enabled={value.isActiveAndEnabled} tele={obj5} cachedY={obj2} lastSpawnPos={obj6} lastSample={obj3} mask={obj4} sternAlive={flag}"); } } } } // ---- Njord/plugins/Njord.dll :: Njord.NjordRuneVFX ---- namespace Njord { public static class NjordRuneVFX { private static bool _initialized; private static GameObject _prefab; private static Material _material; private static Texture2D _runeTex; internal static ParticleSystem _burstSystem; internal static readonly Dictionary _emitters = new Dictionary(); private const float DefaultTrailRate = 30f; private const int DefaultMaxParticles = 400; internal static readonly Dictionary _lastActive = new Dictionary(); public static void DiscoverShaderIfNeeded() { if (_initialized) { return; } _initialized = true; if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug("[VFX] Initializing NjordRuneVFX…"); } NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); string text = "blend"; Texture2D texture2D = text.ToLowerInvariant() switch { "soft" => NjordRuneTextureGenerator.GenerateSoftTrail(njordRuntimeConfig.VFX_Color), "rune" => NjordRuneTextureGenerator.GenerateRune(njordRuntimeConfig.VFX_Color), "streak" => NjordRuneTextureGenerator.GenerateStreak(njordRuntimeConfig.VFX_Color), _ => NjordRuneTextureGenerator.GenerateBlend(njordRuntimeConfig.VFX_Color), }; if (texture2D == null) { NjordDebug.Error("[VFX] Rune texture generation failed."); return; } _runeTex = texture2D; _material = NjordRuneVFX_Helper.GetOrCreateRuneMaterial(texture2D, njordRuntimeConfig.VFX_Color); if (_material == null) { NjordDebug.Error("[VFX] Failed to create rune material (no suitable shader)."); return; } if (_material.HasProperty("_EmissionColor")) { Color vFX_Color = njordRuntimeConfig.VFX_Color; _material.SetColor("_EmissionColor", vFX_Color * 3f); } _material.SetInt("_SrcBlend", 1); _material.SetInt("_DstBlend", 1); _material.SetInt("_ZWrite", 0); _material.DisableKeyword("_ALPHATEST_ON"); _material.EnableKeyword("_ALPHABLEND_ON"); _material.EnableKeyword("_ALPHAPREMULTIPLY_ON"); _material.renderQueue = 3000; _prefab = BuildPrefab(_material); if (_prefab == null) { NjordDebug.Error("[VFX] Failed to build rune prefab."); return; } GameObject gameObject = Object.Instantiate(_prefab); gameObject.name = "NjordRuneVFX_BurstSystem"; gameObject.SetActive(value: true); _burstSystem = gameObject.GetComponent(); if (_burstSystem != null) { ParticleSystem.EmissionModule emission = _burstSystem.emission; emission.rateOverTime = 0f; _burstSystem.Stop(); } GameObject gameObject2 = new GameObject("NjordRuneVFX_Manager"); gameObject2.AddComponent(); Object.DontDestroyOnLoad(gameObject2); if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug("[VFX] NjordRuneVFX initialized successfully."); } } public static void Spawn(Vector3 pos, Color color) { SpawnBurst(pos, color, 6); } public static void Spawn(Vector3 pos) { NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); Spawn(pos, njordRuntimeConfig.VFX_Color); } private static GameObject BuildPrefab(Material mat) { GameObject gameObject = new GameObject("NjordRuneVFX_Particle"); ParticleSystem particleSystem = gameObject.AddComponent(); ParticleSystem.MainModule main = particleSystem.main; main.playOnAwake = false; main.loop = true; main.simulationSpace = ParticleSystemSimulationSpace.World; main.startLifetime = new ParticleSystem.MinMaxCurve(0.8f, 1.4f); main.startSpeed = 0.6f; main.startSize = new ParticleSystem.MinMaxCurve(0.18f, 0.42f); main.startColor = Color.white; main.maxParticles = 400; ParticleSystem.EmissionModule emission = particleSystem.emission; emission.rateOverTime = 0f; ParticleSystem.ShapeModule shape = particleSystem.shape; shape.enabled = true; shape.shapeType = ParticleSystemShapeType.Cone; shape.angle = 10f; shape.radius = 0.15f; shape.position = Vector3.zero; shape.rotation = Vector3.zero; ParticleSystem.VelocityOverLifetimeModule velocityOverLifetime = particleSystem.velocityOverLifetime; velocityOverLifetime.enabled = true; velocityOverLifetime.space = ParticleSystemSimulationSpace.Local; velocityOverLifetime.x = new ParticleSystem.MinMaxCurve(0f); velocityOverLifetime.y = new ParticleSystem.MinMaxCurve(-0.05f, 0.05f); velocityOverLifetime.z = new ParticleSystem.MinMaxCurve(-1.6f, -0.6f); ParticleSystem.ColorOverLifetimeModule colorOverLifetime = particleSystem.colorOverLifetime; colorOverLifetime.enabled = true; Gradient gradient = new Gradient(); gradient.SetKeys(new GradientColorKey[2] { new GradientColorKey(Color.white, 0f), new GradientColorKey(Color.white, 1f) }, new GradientAlphaKey[2] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(0f, 1f) }); colorOverLifetime.color = new ParticleSystem.MinMaxGradient(gradient); ParticleSystem.SizeOverLifetimeModule sizeOverLifetime = particleSystem.sizeOverLifetime; sizeOverLifetime.enabled = true; ParticleSystem.MinMaxCurve size = new ParticleSystem.MinMaxCurve(1f, AnimationCurve.EaseInOut(0f, 1f, 1f, 0.2f)); sizeOverLifetime.size = size; ParticleSystemRenderer component = gameObject.GetComponent(); component.renderMode = ParticleSystemRenderMode.Billboard; component.material = mat; component.shadowCastingMode = ShadowCastingMode.Off; component.receiveShadows = false; gameObject.SetActive(value: false); if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug("[VFX] Rune particle prefab constructed."); } return gameObject; } private static ParticleSystem GetOrCreateEmitterForShip(object ship, Transform stern, Color color) { if (ship == null || stern == null) { return null; } if (!_initialized) { DiscoverShaderIfNeeded(); } if (_prefab == null) { return null; } if (_emitters.TryGetValue(ship, out var value) && value != null) { return value; } GameObject gameObject = Object.Instantiate(_prefab, stern.position, stern.rotation); gameObject.name = $"NjordRuneVFX_Emitter_{ship.GetHashCode()}"; Vector3 position = stern.TransformPoint(new Vector3(0f, -0.6f, -0.6f)); gameObject.transform.position = position; gameObject.transform.rotation = stern.rotation; gameObject.transform.SetParent(stern, worldPositionStays: true); gameObject.SetActive(value: true); value = gameObject.GetComponent(); if (value == null) { return null; } ParticleSystemRenderer component = gameObject.GetComponent(); if (component != null && _material != null) { component.material = new Material(_material); Material material = component.material; if (material.HasProperty("_Color")) { material.SetColor("_Color", color); } if (material.HasProperty("_TintColor")) { material.SetColor("_TintColor", color * 2f); } if (material.HasProperty("_EmissionColor")) { material.SetColor("_EmissionColor", color * 3f); } try { Shader shader = Shader.Find("Particles/Standard Unlit") ?? Shader.Find("Legacy Shaders/Particles/Alpha Blended") ?? Shader.Find("Particles/Alpha Blended"); if (shader != null) { Material material2 = new Material(shader); Texture2D texture2D = _runeTex; if (texture2D == null) { try { texture2D = _material?.GetTexture("_MainTex") as Texture2D; } catch { } try { if (texture2D == null) { texture2D = _material?.mainTexture as Texture2D; } } catch { } } if (texture2D != null) { if (material2.HasProperty("_MainTex")) { material2.SetTexture("_MainTex", texture2D); } if (material2.HasProperty("_BaseMap")) { material2.SetTexture("_BaseMap", texture2D); } try { material2.mainTexture = texture2D; } catch { } } if (material2.HasProperty("_Color")) { material2.SetColor("_Color", color); } if (material2.HasProperty("_EmissionColor")) { material2.SetColor("_EmissionColor", color * 3f); } try { material2.SetInt("_SrcBlend", 5); material2.SetInt("_DstBlend", 10); material2.SetInt("_ZWrite", 0); material2.DisableKeyword("_ALPHATEST_ON"); material2.EnableKeyword("_ALPHABLEND_ON"); material2.DisableKeyword("_ALPHAPREMULTIPLY_ON"); material2.renderQueue = 3000; } catch { } component.material = material2; material = component.material; } } catch { } } ParticleSystem.MainModule main = value.main; main.startColor = color; main.startLifetime = new ParticleSystem.MinMaxCurve(0.5f, 1.6f); main.startSize = new ParticleSystem.MinMaxCurve(0.12f, 1f); main.maxParticles = 400; value.Stop(withChildren: true, ParticleSystemStopBehavior.StopEmitting); _emitters[ship] = value; _lastActive[ship] = Time.time; if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug($"[VFX] Created emitter for ship {ship}."); } return value; } public static void StartTrailForShip(object ship, Transform stern, Color color, float rateMultiplier = 1f) { try { NjordRuneSpriteTrail.StartTrailForShip(ship, stern, color, rateMultiplier); } catch { } } public static void StopTrailForShip(object ship) { try { NjordRuneSpriteTrail.StopTrailForShip(ship); } catch { } } public static void SpawnBurst(Vector3 pos, Color color, int count) { NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); if (!njordRuntimeConfig.VFX_Enable) { return; } try { NjordRuneSpriteTrail.SpawnBurst(pos, color, count); } catch { } } public static void RemoveEmitterForShip(object ship) { if (ship == null) { return; } try { NjordRuneSpriteTrail.RemoveEmitterForShip(ship); } catch { } if (_emitters.TryGetValue(ship, out var value) && value != null) { try { if (value.gameObject != null) { Object.Destroy(value.gameObject); } } catch { } } _emitters.Remove(ship); _lastActive.Remove(ship); if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug($"[VFX] Removed emitter for ship {ship} (destroy hook)."); } } } } // ---- Njord/plugins/Njord.dll :: Njord.NjordRuneVFX_Manager ---- namespace Njord { public class NjordRuneVFX_Manager : MonoBehaviour { private float _timer; private const float CHECK_INTERVAL = 5f; private void Update() { _timer += Time.deltaTime; if (_timer < 5f) { return; } _timer = 0f; NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); float num = 15f; List list = new List(NjordRuneVFX._emitters.Keys); foreach (object item in list) { if (!NjordRuneVFX._emitters.TryGetValue(item, out var value) || value == null) { NjordRuneVFX._emitters.Remove(item); NjordRuneVFX._lastActive.Remove(item); continue; } if (!NjordRuneVFX._lastActive.TryGetValue(item, out var value2)) { value2 = Time.time; } if (!(Time.time - value2 > num)) { continue; } try { if (value.gameObject != null) { Object.Destroy(value.gameObject); } } catch { } NjordRuneVFX._emitters.Remove(item); NjordRuneVFX._lastActive.Remove(item); if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug($"[VFX] Cleaned up emitter for ship {item} after timeout."); } } } } } // ---- Njord/plugins/Njord.dll :: Njord.NjordRuneVFX_Helper ---- namespace Njord { public static class NjordRuneVFX_Helper { private static readonly Dictionary _materialCache = new Dictionary(); private static bool _initialized = false; private static void Init() { if (!_initialized) { _initialized = true; NjordDebug.Log("\ud83d\udf01 VFX‑Forge: Seeking glowing, additive, unlit shaders worthy of Njord."); } } private static long MakeCacheKey(Shader shader, Texture2D tex, Color color) { int num = ((shader != null) ? shader.GetInstanceID() : 0); int num2 = ((tex != null) ? tex.GetInstanceID() : 0); int hashCode = color.GetHashCode(); return (long)((ulong)((long)num << 32) ^ ((ulong)(uint)num2 << 16) ^ (uint)hashCode); } public static Material GetOrCreateRuneMaterial(Texture2D runeTex, Color color) { Init(); Shader shader = Shader.Find("Particles/Standard Unlit") ?? Shader.Find("Legacy Shaders/Particles/Alpha Blended") ?? Shader.Find("Particles/Alpha Blended") ?? Shader.Find("Particles/Additive"); if (shader == null) { float num = float.MinValue; Material[] array = Resources.FindObjectsOfTypeAll(); foreach (Material material in array) { if (!(material == null) && !(material.shader == null)) { Shader shader2 = material.shader; string text = shader2.name.ToLower(); float num2 = 0f; if (text.Contains("particle")) { num2 += 3f; } if (text.Contains("add")) { num2 += 3f; } if (text.Contains("unlit")) { num2 += 2f; } if (text.Contains("transparent")) { num2 += 1.5f; } if (material.HasProperty("_Color") && material.HasProperty("_EmissionColor") && num2 > num) { num = num2; shader = shader2; } } } } if (shader == null) { NjordDebug.Error("\ud83d\udf01 VFX‑Forge: No glowing shader found. The runes refuse to shine."); return null; } long key = MakeCacheKey(shader, runeTex, color); if (_materialCache.TryGetValue(key, out var value) && value != null) { return value; } Material material2 = new Material(shader); if (runeTex != null) { if (material2.HasProperty("_MainTex")) { material2.SetTexture("_MainTex", runeTex); } if (material2.HasProperty("_BaseMap")) { material2.SetTexture("_BaseMap", runeTex); } try { material2.mainTexture = runeTex; } catch { } } if (material2.HasProperty("_Color")) { material2.SetColor("_Color", color); } if (material2.HasProperty("_TintColor")) { material2.SetColor("_TintColor", color); } if (material2.HasProperty("_EmissionColor")) { material2.SetColor("_EmissionColor", color * 3f); } material2.SetInt("_SrcBlend", 1); material2.SetInt("_DstBlend", 1); material2.SetInt("_ZWrite", 0); material2.DisableKeyword("_ALPHATEST_ON"); material2.EnableKeyword("_ALPHABLEND_ON"); material2.DisableKeyword("_ALPHAPREMULTIPLY_ON"); material2.renderQueue = 3000; _materialCache[key] = material2; NjordDebug.Log("\ud83d\udf01 VFX‑Forge: Forged glowing additive rune material from '" + shader.name + "'."); return material2; } public static void ClearCache() { _materialCache.Clear(); _initialized = false; } } } // ---- Njord/plugins/Njord.dll :: Njord.NjordSeaDogEngineVFXTriggers ---- namespace Njord { public static class NjordSeaDogEngineVFXTriggers { public static readonly Dictionary LastController = new Dictionary(); public static readonly Dictionary LastSpeed = new Dictionary(); public static readonly Dictionary LastVfxTime = new Dictionary(); public static readonly Dictionary LastTrailLog = new Dictionary(); private static readonly Dictionary _rudderCache = new Dictionary(); public static Transform GetStern(Ship ship) { if (ship == null) { return null; } if (_rudderCache.TryGetValue(ship, out var value) && value != null) { return value; } try { ShipControlls componentInChildren = ship.GetComponentInChildren(includeInactive: true); if (componentInChildren != null) { _rudderCache[ship] = componentInChildren.transform; return componentInChildren.transform; } } catch { } _rudderCache[ship] = ship.transform; return ship.transform; } public static void Process(Ship ship, NjordSailState state, string speedStr, long controller, float now, Transform tf, bool debug, NjordRuntimeConfig cfg) { if (!cfg.VFX_Enable) { return; } if (!LastVfxTime.TryGetValue(ship, out var value)) { value = 0f; } if (!LastController.TryGetValue(ship, out var value2)) { value2 = 0L; } if (!LastSpeed.TryGetValue(ship, out var value3)) { value3 = "Stop"; } if (controller != value2) { LastController[ship] = controller; if (controller != 0L) { if (debug) { NjordDebug.Debug("[SeaDog][VFX] Control change: spawning control burst"); } LastVfxTime[ship] = now; Transform stern = GetStern(ship); Vector3 pos = stern.position + Vector3.up * 1.2f; NjordRuneVFX.SpawnBurst(pos, cfg.VFX_Color, 8); } else { try { NjordRuneVFX.StopTrailForShip(ship); } catch { } } } if (speedStr != value3) { LastSpeed[ship] = speedStr; if (speedStr == "Half" || speedStr == "Full") { if (debug) { NjordDebug.Debug("[SeaDog][VFX] Sail state change to " + speedStr + ": spawning burst"); } LastVfxTime[ship] = now; Transform stern2 = GetStern(ship); Vector3 vector = stern2.position + Vector3.up * 1.5f; for (int i = 0; i < 4; i++) { Vector3 insideUnitSphere = Random.insideUnitSphere; insideUnitSphere.y = Mathf.Abs(insideUnitSphere.y) + 0.3f; Vector3 pos2 = vector + insideUnitSphere * 0.2f; NjordRuneVFX.SpawnBurst(pos2, cfg.VFX_Color, 1); } } } float num = 0f; try { num = ship.GetSpeed(); } catch { num = 0f; } Transform stern3 = GetStern(ship); if (speedStr != "Stop" && speedStr != "Back" && num > 0.5f) { float maxSpeedForShip = NjordShipSpeed.GetMaxSpeedForShip(ship); float num2 = ((maxSpeedForShip > 0f) ? (num / maxSpeedForShip) : Mathf.Clamp01(num / 10f)); float num3 = Mathf.Clamp(0.5f + num2 * 2f, 0.2f, 4f); if (debug) { LastTrailLog.TryGetValue(ship, out var value4); if (now - value4 >= 2f) { NjordDebug.Debug($"[SeaDog][VFX] Starting trail for ship {ship} at stern {stern3?.name} rateMul={num3:F2}"); LastTrailLog[ship] = now; } } NjordRuneVFX.StartTrailForShip(ship, stern3, cfg.VFX_Color, num3); LastVfxTime[ship] = now; return; } if (debug) { LastTrailLog.TryGetValue(ship, out var value5); if (now - value5 >= 2f) { NjordDebug.Debug($"[SeaDog][VFX] Stopping trail for ship {ship}"); LastTrailLog[ship] = now; } } NjordRuneVFX.StopTrailForShip(ship); } } } // ---- Njord/plugins/Njord.dll :: Njord.Patches.NjordDoodadControlRune ---- namespace Njord.Patches { [HarmonyPatch(typeof(Player), "StartDoodadControl")] public static class NjordDoodadControlRune { private static void Postfix(Player __instance, IDoodadController shipControl) { if (__instance == null || shipControl == null || !(shipControl is ShipControlls { m_ship: var ship }) || ship == null) { return; } long playerID = __instance.GetPlayerID(); if (!NjordAuthorityEngine.UseLegacyFallback && ZNet.instance != null) { if (ZNet.instance.IsServer()) { NjordAuthorityEngine.Server_OnHelmClaim(ship, playerID); } else { NjordAuthorityEngine.Client_RequestControl(ship); } } Njord_ShipControlTracker.SetControl(ship, playerID); ZNetView component = ship.GetComponent(); if (component != null && component.IsValid()) { component.ClaimOwnership(); if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug("[Helm‑Bind] Claimed ZDO ownership for " + ship.name + "."); } } if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug($"[Helm‑Bind] Player {playerID} takes helm of {ship.name}."); } } } } // ---- Njord/plugins/Njord.dll :: Njord.Patches.NjordDoodadReleaseRune ---- namespace Njord.Patches { [HarmonyPatch(typeof(ShipControlls), "OnUseStop")] public static class NjordDoodadReleaseRune { private static void Postfix(ShipControlls __instance, Player player) { if (__instance == null || player == null) { return; } Ship ship = __instance.m_ship; if (ship == null) { return; } long playerID = player.GetPlayerID(); long controllerForShip = Njord_ShipControlTracker.GetControllerForShip(ship); if (controllerForShip != playerID) { return; } if (!NjordAuthorityEngine.UseLegacyFallback && ZNet.instance != null) { if (ZNet.instance.IsServer()) { NjordAuthorityEngine.Server_OnHelmRelease(ship); } else { NjordAuthorityEngine.Client_ReleaseControl(ship); } } Njord_ShipControlTracker.ClearControl(ship); if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug($"[Helm‑Release] Player {playerID} released helm of {ship.name}."); } } } } // ---- Njord/plugins/Njord.dll :: Njord.Patches.NjordSeaDogEngine ---- namespace Njord.Patches { [HarmonyPatch] public static class NjordSeaDogEngine { private static float _telemetryTimer = 0f; private const float TELEMETRY_INTERVAL = 0.2f; private static float _prevNjordSpeed = 0f; private static float _prevVanillaSpeed = 0f; private static float _lastTickLog = -10f; private const float TICK_LOG_INTERVAL = 2f; private static readonly Dictionary _lastDebugLog = new Dictionary(); private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("Ship"); if (type == null) { NjordDebug.Error("NjordSeaDogEngine: Ship type not found."); return null; } MethodInfo methodInfo = AccessTools.Method(type, "CustomFixedUpdate"); if (methodInfo == null) { NjordDebug.Error("NjordSeaDogEngine: CustomFixedUpdate not found on Ship."); } return methodInfo; } public static float Safe(float v) { if (!float.IsNaN(v) && !float.IsInfinity(v)) { return v; } return 0f; } private static void Prefix(object __instance, float fixedDeltaTime) { if (__instance == null || fixedDeltaTime <= 0f) { return; } bool value = NjordConfig.Debug_Enable.Value; bool flag = value && Time.fixedTime - _lastTickLog >= 2f; if (flag) { NjordDebug.Debug($"[SeaDog] Tick dt={fixedDeltaTime:0.000}"); _lastTickLog = Time.fixedTime; } Type type = __instance.GetType(); object obj = AccessTools.Field(type, "m_nview")?.GetValue(__instance); if (obj == null) { return; } MethodInfo methodInfo = AccessTools.Method(obj.GetType(), "IsValid"); if ((methodInfo != null && !(bool)methodInfo.Invoke(obj, null)) || !(AccessTools.Field(type, "m_body")?.GetValue(__instance) is Rigidbody rigidbody) || rigidbody == null || float.IsNaN(rigidbody.mass) || rigidbody.mass <= 0f) { return; } FieldInfo fieldInfo = AccessTools.Field(type, "m_lastDepth"); if (fieldInfo != null && (float)fieldInfo.GetValue(__instance) <= -9000f) { return; } long controllerForShip = Njord_ShipControlTracker.GetControllerForShip(__instance); long num = ((Player.m_localPlayer != null) ? Player.m_localPlayer.GetPlayerID() : 0); bool flag2 = controllerForShip != 0L && num != 0L && controllerForShip == num; Njord_ShipControlTracker.UpdateController(controllerForShip, fixedDeltaTime); if (flag2 && obj is ZNetView zNetView && zNetView.IsValid() && !zNetView.IsOwner()) { zNetView.ClaimOwnership(); if (flag) { NjordDebug.Debug("[SeaDog] ZDO ownership reclaimed for locally controlled ship."); } } float num2 = 0f; float num3 = 0f; float num4 = 1f; float num5 = 0f; float num6 = 1f; if (__instance is Ship ship) { NjordRuntimeConfig njordRuntimeConfig = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); float shipWindAngle = Njord_ShipControlTracker.GetShipWindAngle(ship); if (njordRuntimeConfig.Wind_SystemEnable) { num2 = Mathf.DeltaAngle(0f, ship.GetWindAngle()); num3 = num2; if (njordRuntimeConfig.Wind_AlwaysFull) { num3 = 0f; } else { float num7 = Mathf.Abs(num2); bool flag3 = num7 > 130f; bool shipWasInDeadZone = Njord_ShipControlTracker.GetShipWasInDeadZone(ship, flag3); float shipGraceTimer = Njord_ShipControlTracker.GetShipGraceTimer(ship); if (flag3 != shipWasInDeadZone) { shipGraceTimer = 0f; Njord_ShipControlTracker.SetShipWasInDeadZone(ship, flag3); } else { shipGraceTimer += fixedDeltaTime; } Njord_ShipControlTracker.SetShipGraceTimer(ship, shipGraceTimer); float num8 = num2; if (flag3 && njordRuntimeConfig.Wind_NoDeadZone) { FieldInfo fieldInfo2 = AccessTools.Field(type, "m_rudder") ?? AccessTools.Field(type, "m_rudderValue"); float num9 = ((fieldInfo2 != null) ? ((float)fieldInfo2.GetValue(__instance)) : 0f); float num10 = 115f; num8 = ((num9 > 0.05f) ? (0f - num10) : ((!(num9 < -0.05f)) ? ((num2 > 0f) ? num10 : (0f - num10)) : num10)); } else if (!flag3 && njordRuntimeConfig.Wind_BlendToFull) { num8 = 0f; } num3 = ((!(shipGraceTimer < njordRuntimeConfig.Wind_GracePeriodDelay)) ? num8 : num2); } num5 = Mathf.MoveTowardsAngle(shipWindAngle, num3, njordRuntimeConfig.Wind_BlendRate * fixedDeltaTime); Njord_ShipControlTracker.SetShipWindAngle(ship, num5); Njord_Telemetry.WindUIBlend = num5; float num11 = Mathf.Abs(num5); num4 = ((num11 > 130f) ? 0f : ((!(num11 > 90f)) ? Mathf.Lerp(1f, 0.75f, num11 / 90f) : Mathf.Lerp(0.75f, 0f, (num11 - 90f) / 40f))); } else { num6 = ship.GetWindAngleFactor(); } } AccessTools.Field(type, "m_sailForceFactor")?.SetValue(__instance, 0f); AccessTools.Field(type, "m_sailForceOffset")?.SetValue(__instance, 0f); AccessTools.Field(type, "m_backwardForce")?.SetValue(__instance, 0f); AccessTools.Field(type, "m_sailForce")?.SetValue(__instance, Vector3.zero); string text = AccessTools.Method(type, "GetSpeedSetting")?.Invoke(__instance, null)?.ToString() ?? "Stop"; NjordSailState njordSailState = text switch { "Back" => NjordSailState.Backward, "Half" => NjordSailState.Half, "Full" => NjordSailState.Full, "Slow" => NjordSailState.Forward, "Stop" => NjordSailState.Forward, _ => NjordSailState.Forward, }; string shipPrefabName = NjordBoatController.GetShipPrefabName(__instance); float v = NjordBoatController.GetBaseline(njordSailState, shipPrefabName); if (text == "Stop") { v = 0f; } v = Safe(v); float v2 = NjordBoatController.ApplyAcceleration(v, fixedDeltaTime); v2 = Safe(v2); float sailMult = NjordBoatController.GetSailMult(njordSailState); sailMult = Safe(sailMult); if (text == "Half" || text == "Full") { NjordRuntimeConfig njordRuntimeConfig2 = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); sailMult = ((!njordRuntimeConfig2.Wind_SystemEnable) ? (sailMult * num6) : (sailMult * num4)); } float value2 = Safe(v2 * sailMult * 0.85f); float value3 = Safe(NjordBoatController.GetReverseForce(fixedDeltaTime)); value2 = Mathf.Clamp(value2, 0f, 2000f); value3 = Mathf.Clamp(value3, 0f, 2000f); if (__instance is Ship key) { NjordRuntimeConfig njordRuntimeConfig3 = NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig(); if (njordRuntimeConfig3.Debug_Enable && njordRuntimeConfig3.Debug_PhysicsWind && (!_lastDebugLog.TryGetValue(key, out var value4) || Time.time - value4 > 1f)) { _lastDebugLog[key] = Time.time; string text2 = (njordRuntimeConfig3.Wind_SystemEnable ? "ON " : "OFF"); NjordDebug.Info($"[Physics] {shipPrefabName} | mode={text} | baseline={v:F1} | accel={v2:F1} | mult={sailMult:F2} | fwForce={value2:F1}"); if (njordRuntimeConfig3.Wind_SystemEnable) { NjordDebug.Info($"[Wind] Sys:{text2} | AlwaysFull:{njordRuntimeConfig3.Wind_AlwaysFull} | NatAngle:{num2:F1}° | TgtAngle:{num3:F1}° | CurAngle:{num5:F1}° | Throttle:{num4:F2}"); } } } Transform transform = AccessTools.Property(type, "transform")?.GetValue(__instance) as Transform; if (transform == null) { return; } if (flag2) { Vector3 vector; switch (njordSailState) { case NjordSailState.Backward: vector = -transform.forward * value3; break; case NjordSailState.Forward: case NjordSailState.Half: case NjordSailState.Full: vector = transform.forward * value2; break; default: vector = Vector3.zero; break; } Vector3 vector2 = vector; if (vector2.sqrMagnitude > 0.0001f) { rigidbody.AddForce(vector2 * rigidbody.mass * fixedDeltaTime, ForceMode.Impulse); } } if (__instance is Ship ship2) { float maxSpeedForShip = NjordShipSpeed.GetMaxSpeedForShip(ship2); if (maxSpeedForShip > 0f) { Vector3 linearVelocity = rigidbody.linearVelocity; float magnitude = linearVelocity.magnitude; if (magnitude > maxSpeedForShip && magnitude > 0.0001f) { Vector3 linearVelocity2 = linearVelocity.normalized * maxSpeedForShip; rigidbody.linearVelocity = linearVelocity2; if (value && flag) { NjordDebug.Debug($"[SeaDog][Limiter] {ship2.name} {magnitude:0.00} → {maxSpeedForShip:0.00}"); } } } } if (!flag2 || !(__instance is Ship ship3)) { return; } if (value && flag) { NjordDebug.Debug($"[SeaDog] Controlled ship tick: speedState={text}, vel={ship3.GetSpeed():0.0}"); } _telemetryTimer += fixedDeltaTime; if (_telemetryTimer >= 0.2f) { _telemetryTimer = 0f; Vector3 position = ship3.transform.position; WaterVolume previousAndOut = null; float waterLevel = Floating.GetWaterLevel(position, ref previousAndOut); float num12 = position.y - waterLevel - ship3.m_waterLevelOffset; bool floating = num12 <= ship3.m_disableLevel; WearNTear component = ship3.GetComponent(); if (component != null) { Type typeFromHandle = typeof(WearNTear); ZNetView zNetView2 = AccessTools.Field(typeFromHandle, "m_nview")?.GetValue(component) as ZNetView; if (zNetView2 != null && zNetView2.IsValid()) { float num13 = Mathf.Max(1f, Safe(component.m_health)); float v3 = zNetView2.GetZDO().GetFloat(ZDOVars.s_health, num13); v3 = Mathf.Max(0f, Safe(v3)); Njord_Telemetry.SetHP(v3, num13); } else { Njord_Telemetry.SetHP(0f, 1f); } } else { Njord_Telemetry.SetHP(0f, 1f); } float num14 = Safe(ship3.GetSpeed()); float num15 = Safe(v); Njord_Telemetry.UpdateBasic(ship3, waterLevel, num12, floating, Njord_ShipControlTracker.ReadyTimer, Njord_ShipControlTracker.SteeringGateReady); Njord_Telemetry.UpdateSpeed(_prevNjordSpeed, num14, _prevVanillaSpeed, num15); _prevNjordSpeed = num14; _prevVanillaSpeed = num15; } NjordSeaDogEngineVFXTriggers.Process(ship3, njordSailState, text, controllerForShip, Time.time, transform, value, NjordSync.RuntimeConfig ?? NjordRuntimeConfig.FromConfig()); } } } // ---- Njord/plugins/Njord.dll :: Njord.Patches.NjordTheWindsCall ---- namespace Njord.Patches { [HarmonyPatch(typeof(Hud), "UpdateShipHud")] public static class NjordTheWindsCall { [HarmonyPostfix] public static void Postfix(Hud __instance, Player player, float dt) { if (__instance == null || player == null) { return; } Ship controlledShip = player.GetControlledShip(); if (controlledShip == null) { return; } NjordRuntimeConfig runtimeConfig = NjordSync.RuntimeConfig; if (runtimeConfig == null || !runtimeConfig.Wind_SystemEnable) { return; } float shipWindAngle = Njord_ShipControlTracker.GetShipWindAngle(controlledShip); FieldInfo fieldInfo = AccessTools.Field(typeof(Hud), "m_shipWindIconRoot"); if (fieldInfo != null) { RectTransform rectTransform = fieldInfo.GetValue(__instance) as RectTransform; if (rectTransform != null) { rectTransform.localRotation = Quaternion.Euler(0f, 0f, shipWindAngle); } } Image shipWindIcon = __instance.m_shipWindIcon; if (shipWindIcon == null) { return; } float num = Mathf.DeltaAngle(0f, controlledShip.GetWindAngle()); float num2 = num; float num3 = Mathf.Abs(num); bool flag = num3 > 130f; if (runtimeConfig.Wind_AlwaysFull) { num2 = 0f; } else { float num4 = num; if (flag && runtimeConfig.Wind_NoDeadZone) { FieldInfo fieldInfo2 = AccessTools.Field(typeof(Ship), "m_rudder") ?? AccessTools.Field(typeof(Ship), "m_rudderValue"); float num5 = ((fieldInfo2 != null) ? ((float)fieldInfo2.GetValue(controlledShip)) : 0f); float num6 = 115f; num4 = ((num5 > 0.05f) ? (0f - num6) : ((!(num5 < -0.05f)) ? ((num > 0f) ? num6 : (0f - num6)) : num6)); } else if (!flag && runtimeConfig.Wind_BlendToFull) { num4 = 0f; } float shipGraceTimer = Njord_ShipControlTracker.GetShipGraceTimer(controlledShip); num2 = ((!(shipGraceTimer < runtimeConfig.Wind_GracePeriodDelay)) ? num4 : num); } float num7 = Mathf.Abs(shipWindAngle); Color white = Color.white; if (Mathf.Abs(Mathf.DeltaAngle(shipWindAngle, num2)) > 2f && (runtimeConfig.Wind_AlwaysFull || runtimeConfig.Wind_NoDeadZone || runtimeConfig.Wind_BlendToFull)) { float t = (Mathf.Sin(Time.time * 6f) + 1f) / 2f; white = ((!flag) ? Color.Lerp(new Color(0.4f, 0.7f, 1f, 1f), new Color(0.1f, 0.3f, 1f, 1f), t) : Color.Lerp(new Color(1f, 0.4f, 0.4f, 1f), new Color(1f, 0.1f, 0.1f, 1f), t)); } else if (flag) { white = new Color(1f, 0.2f, 0.2f, 1f); } else if (num7 <= 45f) { white = Color.green; } else if (num7 <= 90f) { float num8 = (num7 - 45f) / 45f; white = Color.Lerp(new Color(0.3f, 0.6f, 1f, 1f), Color.green, 1f - num8); } else { float num9 = (num7 - 90f) / 40f; white = Color.Lerp(Color.white, new Color(0.3f, 0.6f, 1f, 1f), 1f - num9); } shipWindIcon.color = white; } } } // ---- Njord/plugins/Njord.dll :: Njord.Patches.Njord_Ship_Destroy_Patch ---- namespace Njord.Patches { [HarmonyPatch(typeof(Ship), "OnDestroy")] public static class Njord_Ship_Destroy_Patch { [HarmonyPostfix] public static void Postfix(Ship __instance) { try { if (__instance != null) { NjordRuneVFX.RemoveEmitterForShip(__instance); } } catch { } } } } // ---- Njord/plugins/Njord.dll :: Njord.Patches.Njord_PlayerSpawned_Init ---- namespace Njord.Patches { [HarmonyPatch(typeof(Player), "OnSpawned")] public static class Njord_PlayerSpawned_Init { [HarmonyPostfix] public static void Postfix(Player __instance) { __instance.StartCoroutine(DelayedNjordInit()); } private static IEnumerator DelayedNjordInit() { yield return null; yield return null; yield return null; yield return null; NjordDebug.Info("\ud83c\udf0a NjordSync initializing AFTER player spawn…"); NjordSync.Initialize(); NjordAuthorityEngine.Initialize(); NjordDebug.Info("\ud83c\udf0a NjordSync + Authority fully initialized."); } } } // ---- Njord/plugins/Njord.dll :: Njord.Patches.Patch_Ship_ApplyControlls ---- namespace Njord.Patches { [HarmonyPatch(typeof(Ship), "ApplyControlls")] public static class Patch_Ship_ApplyControlls { [HarmonyPostfix] public static void Postfix(Ship __instance) { if (!(__instance == null)) { float fixedDeltaTime = Time.fixedDeltaTime; Type type = __instance.GetType(); FieldInfo fieldInfo = AccessTools.Field(type, "m_rudder") ?? AccessTools.Field(type, "m_rudderValue"); float num = 0f; if (fieldInfo != null) { num = (float)fieldInfo.GetValue(__instance); } float steeringMultiplier = NjordSync.RuntimeConfig.SteeringMultiplier; float njord = num * steeringMultiplier; Njord_Telemetry.ReportRudder(num, njord, steeringMultiplier); } } } } // ---- Njord/plugins/Njord.dll :: Njord.HUD.NjordHudManager ---- namespace Njord.HUD { public static class NjordHudManager { private static bool _initialized = false; private static bool _hudCreated = false; private static bool _stylesCreated = false; private static Rect _hudRect; private static bool _dragging = false; private static Vector2 _dragStartMouse; private static Vector2 _dragStartHud; private static GUIStyle _headerStyle; private static GUIStyle _sectionStyle; private static GUIStyle _labelStyle; private static GUIStyle _smallLabelStyle; private static GUIStyle _hpBgStyle; private static GUIStyle _hpFillStyle; private static readonly Color _bgColor = new Color(0f, 0f, 0f, 0.7f); private const float WIDTH = 360f; private const float HEIGHT = 570f; public static void Initialize() { if (!_initialized) { _initialized = true; _hudRect = new Rect(Plugin.HudPositionX.Value, Plugin.HudPositionY.Value, 360f, 570f); } } public static void CreateHud() { if (!_hudCreated) { _hudCreated = true; NjordDebug.Info("[HUD] Njord HUD initialized."); } } public static void OnGUI() { if (_initialized && _hudCreated && Njord_ShipControlTracker.ShouldShowHud) { CreateStylesIfNeeded(); GUI.color = _bgColor; GUI.Box(_hudRect, GUIContent.none); GUI.color = Color.white; GUILayout.BeginArea(_hudRect); GUILayout.BeginVertical(); GUILayout.Label("⚓ NJORD READOUT ⚓", _headerStyle); GUILayout.Space(4f); GUILayout.Label("Vessel: " + Njord_Telemetry.VesselName, _labelStyle); string text = "Status: " + (Njord_Telemetry.IsFloating ? "Floating ✓" : "Not Floating ✗") + " Steering Gate: " + (Njord_Telemetry.GateReady ? "Open ✓" : "Closed ✗"); GUILayout.Label(text, _smallLabelStyle); GUILayout.Space(4f); GUILayout.Label("── Waterline ─────────────────────────────", _sectionStyle); string text2 = (Njord_Telemetry.WaterError ? "—" : Njord_Telemetry.WaterLevel.ToString("F2")); string text3 = (Njord_Telemetry.WaterError ? "—" : Njord_Telemetry.BoatY.ToString("F2")); string arg = (Njord_Telemetry.DepthError ? "—" : Njord_Telemetry.Depth.ToString("F3")); GUILayout.Label("Water Level: " + text2, _labelStyle); GUILayout.Label("Boat Y: " + text3, _labelStyle); GUILayout.Label($"Depth: {arg} (Disable: {Njord_Telemetry.DisableLevel:F3})", _labelStyle); GUILayout.Label($"Readiness: {Njord_Telemetry.ReadinessTimer:F2} / {Njord_Telemetry.ReadinessMax:F2}", _labelStyle); GUILayout.Space(4f); GUILayout.Label("── Hull Integrity ───────────────────────", _sectionStyle); float hPCurrent = Njord_Telemetry.HPCurrent; float num = Mathf.Max(Njord_Telemetry.HPMax, 1f); float num2 = Mathf.Clamp01(hPCurrent / num); GUILayout.Label($"HP: {hPCurrent:F0} / {num:F0} ({num2 * 100f:F1}%)", _labelStyle); Rect lastRect = GUILayoutUtility.GetLastRect(); float width = 340f; float height = 10f; Rect position = new Rect(10f, lastRect.yMax + 2f, width, height); GUI.Box(position, GUIContent.none, _hpBgStyle); Rect position2 = new Rect(position.x, position.y, position.width * num2, position.height); GUI.Box(position2, GUIContent.none, _hpFillStyle); GUILayout.Space(22f); GUILayout.Label("── Steering ─────────────────────────────", _sectionStyle); string text4 = (Njord_Telemetry.RudderError ? "—" : Njord_Telemetry.RudderVanilla.ToString("F2")); string text5 = (Njord_Telemetry.RudderError ? "—" : Njord_Telemetry.RudderNjord.ToString("F2")); GUILayout.BeginHorizontal(); GUILayout.Label("Vanilla: " + text4, _labelStyle, GUILayout.Width(150f)); GUILayout.Label("Njord: " + text5, _labelStyle); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); GUILayout.Label($"Mult: {Njord_Telemetry.SteeringMultiplier:F2}x", _labelStyle, GUILayout.Width(150f)); GUILayout.Label($"Gain: {Njord_Telemetry.SteeringGainPerSecond:+0.00;-0.00;0.00}", _labelStyle); GUILayout.EndHorizontal(); GUILayout.Space(4f); GUILayout.Label("── Speed ────────────────────────────────", _sectionStyle); string text6 = (Njord_Telemetry.SpeedError ? "—" : Njord_Telemetry.NjordSpeedCurr.ToString("F1")); string text7 = (Njord_Telemetry.SpeedError ? "—" : Njord_Telemetry.MortalSpeedCurr.ToString("F1")); string text8 = (Njord_Telemetry.SpeedError ? "—" : (Njord_Telemetry.NjordSpeedCurr - Njord_Telemetry.MortalSpeedCurr).ToString("0.0")); GUILayout.BeginHorizontal(); GUILayout.Label("Njord: " + text6 + " m/s", _labelStyle, GUILayout.Width(150f)); GUILayout.Label("Mortal: " + text7 + " m/s", _labelStyle); GUILayout.EndHorizontal(); GUILayout.Label("Gift of the Deep: " + text8 + " m/s", _labelStyle); GUILayout.Space(4f); GUILayout.Label("── Wind ─────────────────────────────────", _sectionStyle); GUILayout.Label($"UI Blend: {Njord_Telemetry.WindUIBlend:F2}", _labelStyle); GUILayout.Label($"UI Color: ({Njord_Telemetry.WindUIColor.r:F2}, {Njord_Telemetry.WindUIColor.g:F2}, {Njord_Telemetry.WindUIColor.b:F2})", _labelStyle); GUILayout.Space(6f); GUILayout.Label("Press F7 to toggle HUD (drag anywhere to move)", _smallLabelStyle); GUILayout.EndVertical(); GUILayout.EndArea(); HandleDragging(); } } private static void HandleDragging() { Event current = Event.current; if (current != null) { if (current.type == EventType.MouseDown && current.button == 0 && _hudRect.Contains(current.mousePosition)) { _dragging = true; _dragStartMouse = current.mousePosition; _dragStartHud = new Vector2(_hudRect.x, _hudRect.y); current.Use(); } if (_dragging && current.type == EventType.MouseDrag && current.button == 0) { Vector2 vector = current.mousePosition - _dragStartMouse; _hudRect.x = _dragStartHud.x + vector.x; _hudRect.y = _dragStartHud.y + vector.y; current.Use(); } if (_dragging && (current.type == EventType.MouseUp || current.rawType == EventType.MouseUp)) { _dragging = false; Plugin.HudPositionX.Value = _hudRect.x; Plugin.HudPositionY.Value = _hudRect.y; current.Use(); } } } private static void CreateStylesIfNeeded() { if (!_stylesCreated) { _stylesCreated = true; _headerStyle = new GUIStyle(GUI.skin.label) { fontSize = 16, fontStyle = FontStyle.Bold, normal = { textColor = new Color(0.9f, 0.8f, 0.5f) } }; _sectionStyle = new GUIStyle(GUI.skin.label) { fontSize = 13, fontStyle = FontStyle.Bold, normal = { textColor = new Color(0.85f, 0.75f, 0.55f) } }; _labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 12, normal = { textColor = new Color(0.95f, 0.9f, 0.8f) } }; _smallLabelStyle = new GUIStyle(GUI.skin.label) { fontSize = 11, normal = { textColor = new Color(0.95f, 0.9f, 0.8f) } }; Texture2D texture2D = new Texture2D(1, 1); texture2D.SetPixel(0, 0, new Color(0.2f, 0.05f, 0.05f, 0.8f)); texture2D.Apply(); _hpBgStyle = new GUIStyle(GUI.skin.box); _hpBgStyle.normal.background = texture2D; Texture2D texture2D2 = new Texture2D(1, 1); texture2D2.SetPixel(0, 0, new Color(0.1f, 0.7f, 0.1f, 0.9f)); texture2D2.Apply(); _hpFillStyle = new GUIStyle(GUI.skin.box); _hpFillStyle.normal.background = texture2D2; } } } } // ---- Njord/plugins/Njord.dll :: Njord.HUD.NjordReleaseIndicator ---- namespace Njord.HUD { public static class NjordReleaseIndicator { private static GUIStyle labelStyle; private static void EnsureStyle() { if (labelStyle == null) { labelStyle = new GUIStyle(GUI.skin.label) { fontSize = 14, fontStyle = FontStyle.Bold, normal = { textColor = Color.white } }; } } public static void Draw() { if (!(Player.m_localPlayer == null) && Njord_ShipControlTracker.CurrentController != 0L && Plugin.HudEnabledByUser.Value) { EnsureStyle(); float x = 40f; float y = (float)Screen.height - 220f; GUI.Label(new Rect(x, y, 400f, 40f), "Release [E] to leave helm", labelStyle); } } } } // ---- Njord/plugins/Njord.dll :: Njord.Core.NjordAuthorityEngine ---- namespace Njord.Core { public static class NjordAuthorityEngine { private static readonly Dictionary ServerAuthorityMap = new Dictionary(); private static bool _initialized = false; public static bool UseLegacyFallback { get; private set; } = false; public static void Initialize() { if (_initialized) { return; } try { if (ZRoutedRpc.instance == null) { NjordDebug.Error("[Authority] ZRoutedRpc not ready; enabling legacy fallback."); UseLegacyFallback = true; return; } ZRoutedRpc.instance.Register("Njord_RequestControl", RPC_RequestControl); ZRoutedRpc.instance.Register("Njord_ReleaseControl", RPC_ReleaseControl); ZRoutedRpc.instance.Register("Njord_RequestAuthorityState", RPC_RequestAuthorityState); ZRoutedRpc.instance.Register("Njord_AuthorityUpdate", RPC_AuthorityUpdate); ZRoutedRpc.instance.Register("Njord_AuthoritySyncAll", RPC_AuthoritySyncAll); _initialized = true; NjordDebug.Info("[Authority] NjordAuthorityEngine initialized (RPC-only, server-authoritative)."); } catch (Exception arg) { NjordDebug.Error($"[Authority] Initialization failed; enabling legacy fallback. Exception: {arg}"); UseLegacyFallback = true; } } public static void Server_OnHelmClaim(Ship ship, long playerID) { if (!UseLegacyFallback && !(ship == null) && IsServer() && TryGetShipZdoID(ship, out var id)) { ServerAuthorityMap[id] = playerID; BroadcastAuthorityUpdate(id, playerID); ZNetView component = ship.GetComponent(); if (component != null && component.IsValid()) { component.ClaimOwnership(); } } } public static void Server_OnHelmRelease(Ship ship) { if (!UseLegacyFallback && !(ship == null) && IsServer() && TryGetShipZdoID(ship, out var id)) { ServerAuthorityMap[id] = 0L; BroadcastAuthorityUpdate(id, 0L); ZNetView component = ship.GetComponent(); if (component != null && component.IsValid() && !component.IsOwner()) { component.ClaimOwnership(); } } } public static void Client_RequestControl(Ship ship) { if (!UseLegacyFallback && !(ship == null) && !IsServer() && TryGetShipZdoID(ship, out var id)) { long num = ((Player.m_localPlayer != null) ? Player.m_localPlayer.GetPlayerID() : 0); if (num != 0L && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC("Njord_RequestControl", id, num); } } } public static void Client_ReleaseControl(Ship ship) { if (!UseLegacyFallback && !(ship == null) && !IsServer() && TryGetShipZdoID(ship, out var id)) { long num = ((Player.m_localPlayer != null) ? Player.m_localPlayer.GetPlayerID() : 0); if (num != 0L && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC("Njord_ReleaseControl", id, num); } } } private static void RPC_RequestControl(long sender, ZDOID shipID, long playerID) { if (UseLegacyFallback || !IsServer() || playerID == 0L) { return; } ServerAuthorityMap[shipID] = playerID; BroadcastAuthorityUpdate(shipID, playerID); ZDO zDO = ZDOMan.instance?.GetZDO(shipID); if (zDO != null) { zDO.SetOwner(sender); if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug($"[Authority] ZDO ownership → peer {sender} for ship {shipID}"); } } } private static void RPC_ReleaseControl(long sender, ZDOID shipID, long playerID) { if (UseLegacyFallback || !IsServer() || (ServerAuthorityMap.TryGetValue(shipID, out var value) && value != playerID)) { return; } ServerAuthorityMap[shipID] = 0L; BroadcastAuthorityUpdate(shipID, 0L); ZDO zDO = ZDOMan.instance?.GetZDO(shipID); if (zDO == null) { return; } ZNetView zNetView = ZNetScene.instance?.FindInstance(zDO); if (!(zNetView != null)) { return; } ZNetView component = zNetView.GetComponent(); if (component != null && component.IsValid()) { component.ClaimOwnership(); if (NjordConfig.Debug_Enable.Value) { NjordDebug.Debug($"[Authority] ZDO ownership reclaimed by server for ship {shipID}"); } } } private static void RPC_RequestAuthorityState(long sender, ZDOID shipID) { if (!UseLegacyFallback && IsServer()) { long value = 0L; ServerAuthorityMap.TryGetValue(shipID, out value); if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(sender, "Njord_AuthorityUpdate", shipID, value); } } } private static void RPC_AuthorityUpdate(long sender, ZDOID shipID, long controllerID) { if (UseLegacyFallback) { return; } ZDO zDO = ZDOMan.instance.GetZDO(shipID); if (zDO == null) { return; } ZNetView zNetView = ZNetScene.instance.FindInstance(zDO); if (zNetView == null) { return; } Ship component = zNetView.GetComponent(); if (!(component == null)) { if (controllerID != 0L) { Njord_ShipControlTracker.SetControl(component, controllerID); } else { Njord_ShipControlTracker.ClearControl(component); } } } private static void RPC_AuthoritySyncAll(long sender, ZPackage pkg) { if (UseLegacyFallback) { return; } int num = pkg.ReadInt(); long num2 = ((Player.m_localPlayer != null) ? Player.m_localPlayer.GetPlayerID() : 0); long controller = 0L; for (int i = 0; i < num; i++) { ZDOID zDOID = pkg.ReadZDOID(); long num3 = pkg.ReadLong(); if (num2 != 0L && num3 == num2) { controller = num3; } } Njord_ShipControlTracker.UpdateController(controller, 0f); } private static bool IsServer() { if (ZNet.instance != null) { return ZNet.instance.IsServer(); } return false; } private static bool TryGetShipZdoID(Ship ship, out ZDOID id) { id = ZDOID.None; if (ship == null) { return false; } ZNetView component = ship.GetComponent(); if (component == null || !component.IsValid()) { return false; } ZDO zDO = component.GetZDO(); if (zDO == null) { return false; } id = zDO.m_uid; return true; } private static void BroadcastAuthorityUpdate(ZDOID shipID, long controllerID) { if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "Njord_AuthorityUpdate", shipID, controllerID); } } } } // ---- Njord/plugins/Njord.dll :: Njord.Core.NjordConfig ---- namespace Njord.Core { public static class NjordConfig { public static ConfigEntry Wind_SystemEnable; public static ConfigEntry Wind_AlwaysFull; public static ConfigEntry Wind_NoDeadZone; public static ConfigEntry Wind_BlendToFull; public static ConfigEntry Wind_BlendRate; public static ConfigEntry Wind_GracePeriodDelay; public static ConfigEntry SteeringMultiplier; public static ConfigEntry AccelerationMultiplier; public static ConfigEntry BaseForwardForce; public static ConfigEntry BaseReverseForce; public static ConfigEntry SailForwardForce; public static ConfigEntry HalfSailForce; public static ConfigEntry FullSailForce; public static ConfigEntry ReverseKick; public static ConfigEntry MaxSpeed_Raft; public static ConfigEntry MaxSpeed_Karve; public static ConfigEntry MaxSpeed_Longship; public static ConfigEntry MaxSpeed_Drakkar; public static ConfigEntry MaxSpeed_MercantShip; public static ConfigEntry MaxSpeed_CargoShip; public static ConfigEntry MaxSpeed_BigCargoShip; public static ConfigEntry MaxSpeed_RowingCanoe; public static ConfigEntry MaxSpeed_DoubleRowingCanoe; public static ConfigEntry MaxSpeed_LittleBoat; public static ConfigEntry MaxSpeed_WarShip; public static ConfigEntry VFX_Enable; public static ConfigEntry VFX_Color; public static ConfigEntry Debug_Enable; public static ConfigEntry Debug_PhysicsWind; public static ConfigEntry Debug_AuthControl; public static void Bind(ConfigFile file) { Wind_SystemEnable = file.Bind("2 - Wind System", "Wind_SystemEnable", defaultValue: true, "Master switch for Njord wind physics and UI."); Wind_AlwaysFull = file.Bind("2 - Wind System", "Wind_AlwaysFull", defaultValue: true, "Always gives perfect tailwind, ignoring all other rules."); Wind_NoDeadZone = file.Bind("2 - Wind System", "Wind_NoDeadZone", defaultValue: false, "[EXPERIMENTAL] Enables nudging the wind just out of the dead zone (to the 130° edge)."); Wind_BlendToFull = file.Bind("2 - Wind System", "Wind_BlendToFull", defaultValue: false, "[EXPERIMENTAL] Enables nudging the wind from the crosswind line to the perfect tailwind line."); Wind_BlendRate = file.Bind("2 - Wind System", "Wind_BlendRate", 30f, "How fast the wind adjusts (degrees per second)."); Wind_GracePeriodDelay = file.Bind("2 - Wind System", "Wind_GracePeriodDelay", 2.5f, "Delay in seconds before the wind system starts assisting."); SteeringMultiplier = file.Bind("3 - Steering", "SteeringMultiplier", 1.85f, "Multiplier applied to rudder steering."); AccelerationMultiplier = file.Bind("4 - Acceleration", "AccelerationMultiplier", 2.2f, "Acceleration multiplier."); BaseForwardForce = file.Bind("4 - Acceleration", "BaseForwardForce", 0.85f, "Base forward force."); BaseReverseForce = file.Bind("4 - Acceleration", "BaseReverseForce", 0.2f, "Base reverse force."); SailForwardForce = file.Bind("5 - Sails", "SailForwardForce", 0.3f, "Forward force when sails are down."); HalfSailForce = file.Bind("5 - Sails", "HalfSailForce", 1.2f, "Forward force at half sail."); FullSailForce = file.Bind("5 - Sails", "FullSailForce", 4.2f, "Forward force at full sail."); ReverseKick = file.Bind("5 - Sails", "ReverseKick", 1.8f, "Reverse kick multiplier."); MaxSpeed_Raft = file.Bind("6 - Speed Limits", "MaxSpeed_Raft", 7f, "Absolute max speed for the Raft (m/s)."); MaxSpeed_Karve = file.Bind("6 - Speed Limits", "MaxSpeed_Karve", 16.8f, "Absolute max speed for the Karve (m/s)."); MaxSpeed_Longship = file.Bind("6 - Speed Limits", "MaxSpeed_Longship", 26f, "Absolute max speed for the Longship (m/s)."); MaxSpeed_Drakkar = file.Bind("6 - Speed Limits", "MaxSpeed_Drakkar", 30f, "Absolute max speed for the Drakkar (m/s)."); if (Plugin.OdinShipDetected) { MaxSpeed_MercantShip = file.Bind("7 - OdinShip Speed Limits", "MaxSpeed_MercantShip", 22f, new ConfigDescription("Max speed for MercantShip (m/s).", new AcceptableValueRange(1f, 100f))); MaxSpeed_CargoShip = file.Bind("7 - OdinShip Speed Limits", "MaxSpeed_CargoShip", 20f, new ConfigDescription("Max speed for CargoShip (m/s).", new AcceptableValueRange(1f, 100f))); MaxSpeed_BigCargoShip = file.Bind("7 - OdinShip Speed Limits", "MaxSpeed_BigCargoShip", 23.5f, new ConfigDescription("Max speed for BigCargoShip (m/s).", new AcceptableValueRange(1f, 100f))); MaxSpeed_RowingCanoe = file.Bind("7 - OdinShip Speed Limits", "MaxSpeed_RowingCanoe", 10f, new ConfigDescription("Max speed for RowingCanoe (m/s).", new AcceptableValueRange(1f, 100f))); MaxSpeed_DoubleRowingCanoe = file.Bind("7 - OdinShip Speed Limits", "MaxSpeed_DoubleRowingCanoe", 12f, new ConfigDescription("Max speed for DoubleRowingCanoe (m/s).", new AcceptableValueRange(1f, 100f))); MaxSpeed_LittleBoat = file.Bind("7 - OdinShip Speed Limits", "MaxSpeed_LittleBoat", 17f, new ConfigDescription("Max speed for LittleBoat (m/s).", new AcceptableValueRange(1f, 100f))); MaxSpeed_WarShip = file.Bind("7 - OdinShip Speed Limits", "MaxSpeed_WarShip", 32f, new ConfigDescription("Max speed for WarShip (m/s).", new AcceptableValueRange(1f, 100f))); } VFX_Enable = file.Bind("8 - VFX", "VFX_Enable", defaultValue: true, "Enable rune VFX."); VFX_Color = file.Bind("8 - VFX", "VFX_Color", new Color(0.3f, 0.6f, 1f, 1f), "Rune VFX color."); Debug_Enable = file.Bind("9 - Debug", "Debug_Enable", defaultValue: false, "Master switch to enable Njord debug logs."); Debug_PhysicsWind = file.Bind("9 - Debug", "Debug_PhysicsWind", defaultValue: false, "Enable logging for Wind calculations and Physics."); Debug_AuthControl = file.Bind("9 - Debug", "Debug_AuthControl", defaultValue: false, "Enable logging for ship ownership and control."); } } } // ---- Njord/plugins/Njord.dll :: Njord.Core.NjordRuntimeConfig ---- namespace Njord.Core { public class NjordRuntimeConfig { public float SteeringMultiplier; public float AccelerationMultiplier; public float BaseForwardForce; public float BaseReverseForce; public float SailForwardForce; public float HalfSailForce; public float FullSailForce; public float ReverseKick; public bool Wind_SystemEnable; public bool Wind_AlwaysFull; public bool Wind_NoDeadZone; public bool Wind_BlendToFull; public float Wind_BlendRate; public float Wind_GracePeriodDelay; public bool VFX_Enable; public Color VFX_Color; public bool Debug_Enable; public bool Debug_PhysicsWind; public bool Debug_AuthControl; public float MaxSpeed_Raft; public float MaxSpeed_Karve; public float MaxSpeed_Longship; public float MaxSpeed_Drakkar; public static NjordRuntimeConfig FromConfig() { return new NjordRuntimeConfig { SteeringMultiplier = NjordConfig.SteeringMultiplier.Value, AccelerationMultiplier = NjordConfig.AccelerationMultiplier.Value, BaseForwardForce = NjordConfig.BaseForwardForce.Value, BaseReverseForce = NjordConfig.BaseReverseForce.Value, SailForwardForce = NjordConfig.SailForwardForce.Value, HalfSailForce = NjordConfig.HalfSailForce.Value, FullSailForce = NjordConfig.FullSailForce.Value, ReverseKick = NjordConfig.ReverseKick.Value, Wind_SystemEnable = NjordConfig.Wind_SystemEnable.Value, Wind_AlwaysFull = NjordConfig.Wind_AlwaysFull.Value, Wind_NoDeadZone = NjordConfig.Wind_NoDeadZone.Value, Wind_BlendToFull = NjordConfig.Wind_BlendToFull.Value, Wind_BlendRate = NjordConfig.Wind_BlendRate.Value, Wind_GracePeriodDelay = NjordConfig.Wind_GracePeriodDelay.Value, VFX_Enable = NjordConfig.VFX_Enable.Value, VFX_Color = NjordConfig.VFX_Color.Value, Debug_Enable = NjordConfig.Debug_Enable.Value, Debug_PhysicsWind = NjordConfig.Debug_PhysicsWind.Value, Debug_AuthControl = NjordConfig.Debug_AuthControl.Value, MaxSpeed_Raft = NjordConfig.MaxSpeed_Raft.Value, MaxSpeed_Karve = NjordConfig.MaxSpeed_Karve.Value, MaxSpeed_Longship = NjordConfig.MaxSpeed_Longship.Value, MaxSpeed_Drakkar = NjordConfig.MaxSpeed_Drakkar.Value }; } public void WriteToPackage(ZPackage pkg) { pkg.Write(SteeringMultiplier); pkg.Write(AccelerationMultiplier); pkg.Write(BaseForwardForce); pkg.Write(BaseReverseForce); pkg.Write(SailForwardForce); pkg.Write(HalfSailForce); pkg.Write(FullSailForce); pkg.Write(ReverseKick); pkg.Write(Wind_SystemEnable); pkg.Write(Wind_AlwaysFull); pkg.Write(Wind_NoDeadZone); pkg.Write(Wind_BlendToFull); pkg.Write(Wind_BlendRate); pkg.Write(VFX_Enable); pkg.Write(VFX_Color.r); pkg.Write(VFX_Color.g); pkg.Write(VFX_Color.b); pkg.Write(VFX_Color.a); pkg.Write(Debug_Enable); pkg.Write(Debug_PhysicsWind); pkg.Write(Debug_AuthControl); } public void ReadFromPackage(ZPackage pkg) { SteeringMultiplier = pkg.ReadSingle(); AccelerationMultiplier = pkg.ReadSingle(); BaseForwardForce = pkg.ReadSingle(); BaseReverseForce = pkg.ReadSingle(); SailForwardForce = pkg.ReadSingle(); HalfSailForce = pkg.ReadSingle(); FullSailForce = pkg.ReadSingle(); ReverseKick = pkg.ReadSingle(); Wind_SystemEnable = pkg.ReadBool(); Wind_AlwaysFull = pkg.ReadBool(); Wind_NoDeadZone = pkg.ReadBool(); Wind_BlendToFull = pkg.ReadBool(); Wind_BlendRate = pkg.ReadSingle(); VFX_Enable = pkg.ReadBool(); float r = pkg.ReadSingle(); float g = pkg.ReadSingle(); float b = pkg.ReadSingle(); float a = pkg.ReadSingle(); VFX_Color = new Color(r, g, b, a); Debug_Enable = pkg.ReadBool(); Debug_PhysicsWind = pkg.ReadBool(); Debug_AuthControl = pkg.ReadBool(); } } } // ---- Njord/plugins/Njord.dll :: Njord.Core.NjordSailState ---- namespace Njord.Core { public enum NjordSailState { Forward, Half, Full, Backward } } // ---- Njord/plugins/Njord.dll :: Njord.Core.NjordShipSpeed ---- namespace Njord.Core { public static class NjordShipSpeed { private static string CleanName(Ship ship) { if (ship == null) { return "unknown"; } string name = ship.name; if (string.IsNullOrEmpty(name)) { name = ship.gameObject.name; } return name.Replace("(Clone)", "").Trim().ToLowerInvariant(); } public static float GetMaxSpeedForShip(Ship ship) { if (ship == null) { return 0f; } string text = CleanName(ship); float num = 0f; if (text.Contains("raft")) { num = NjordConfig.MaxSpeed_Raft.Value; } else if (text.Contains("karve")) { num = NjordConfig.MaxSpeed_Karve.Value; } else if (text.Contains("longship")) { num = NjordConfig.MaxSpeed_Longship.Value; } else if (text.Contains("drakkar")) { num = NjordConfig.MaxSpeed_Drakkar.Value; } if (Plugin.OdinShipDetected) { if (text.Contains("mercantship")) { num = NjordConfig.MaxSpeed_MercantShip.Value; } else if (text.Contains("bigcargoship")) { num = NjordConfig.MaxSpeed_BigCargoShip.Value; } else if (text.Contains("cargoship")) { num = NjordConfig.MaxSpeed_CargoShip.Value; } else if (text.Contains("doublerowingcanoe")) { num = NjordConfig.MaxSpeed_DoubleRowingCanoe.Value; } else if (text.Contains("rowingcanoe")) { num = NjordConfig.MaxSpeed_RowingCanoe.Value; } else if (text.Contains("littleboat")) { num = NjordConfig.MaxSpeed_LittleBoat.Value; } else if (text.Contains("warship")) { num = NjordConfig.MaxSpeed_WarShip.Value; } } if (num <= 0f || float.IsNaN(num) || float.IsInfinity(num)) { return 0f; } return num; } } } // ---- Njord/plugins/Njord.dll :: Njord.Core.NjordSync ---- namespace Njord.Core { public static class NjordSync { private const string RPC_CONFIG = "Njord_Config"; private const string RPC_CONFIG_UPDATE = "Njord_ConfigUpdate"; private static bool _initialized; public static NjordRuntimeConfig RuntimeConfig { get; private set; } public static void Initialize() { if (!_initialized && !(ZNet.instance == null)) { RuntimeConfig = NjordRuntimeConfig.FromConfig(); RegisterRpcs(); if (ZNet.instance.IsServer()) { BroadcastConfig(); } _initialized = true; NjordDebug.Info("[Sync] RuntimeConfig loaded at init: " + $"BaseForward={RuntimeConfig.BaseForwardForce}, " + $"Accel={RuntimeConfig.AccelerationMultiplier}, " + $"Full={RuntimeConfig.FullSailForce}"); } } public static void ReloadRuntimeConfig() { RuntimeConfig = NjordRuntimeConfig.FromConfig(); if (ZNet.instance != null && ZNet.instance.IsServer()) { BroadcastConfig(); } NjordDebug.Info("[Sync] RuntimeConfig reloaded (client or server)."); } private static void RegisterRpcs() { if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.Register("Njord_Config", RPC_ReceiveConfig); ZRoutedRpc.instance.Register("Njord_ConfigUpdate", RPC_ReceiveConfigUpdate); } } private static void BroadcastConfig() { if (ZRoutedRpc.instance != null) { ZPackage zPackage = new ZPackage(); RuntimeConfig.WriteToPackage(zPackage); ZRoutedRpc.instance.InvokeRoutedRPC("Njord_Config", zPackage); NjordDebug.Info("[Sync] Broadcasted runtime config to all clients."); } } private static void RPC_ReceiveConfig(long sender, ZPackage pkg) { NjordRuntimeConfig njordRuntimeConfig = new NjordRuntimeConfig(); njordRuntimeConfig.ReadFromPackage(pkg); RuntimeConfig = njordRuntimeConfig; NjordDebug.Info("[Sync] Received runtime config from server."); } public static void Client_RequestConfigUpdate() { if (ZRoutedRpc.instance != null) { ZPackage zPackage = new ZPackage(); RuntimeConfig.WriteToPackage(zPackage); ZRoutedRpc.instance.InvokeRoutedRPC("Njord_ConfigUpdate", zPackage); NjordDebug.Info("[Sync] Client requested config update."); } } private static void RPC_ReceiveConfigUpdate(long sender, ZPackage pkg) { if (!(ZNet.instance == null) && ZNet.instance.IsServer()) { if (!ZNet.instance.IsAdmin(sender.ToString())) { BroadcastConfig(); return; } NjordRuntimeConfig njordRuntimeConfig = new NjordRuntimeConfig(); njordRuntimeConfig.ReadFromPackage(pkg); RuntimeConfig = njordRuntimeConfig; BroadcastConfig(); NjordDebug.Info("[Sync] Admin updated config; broadcasted to clients."); } } public static void ServerReloadAndBroadcast() { if (!(ZNet.instance == null) && ZNet.instance.IsServer()) { RuntimeConfig = NjordRuntimeConfig.FromConfig(); BroadcastConfig(); NjordDebug.Info("[Sync] Server reloaded config from disk and broadcast."); } } } } // ---- Njord/plugins/Njord.dll :: Njord.Core.Njord_Telemetry ---- namespace Njord.Core { public static class Njord_Telemetry { public static string VesselName = "Unknown"; public static bool GateReady = false; public static bool IsFloating = false; public static float WaterLevel = 0f; public static float BoatY = 0f; public static float Depth = 0f; public static float DisableLevel = 0f; public static float ReadinessTimer = 0f; public static float ReadinessMax = 0f; public static float HPCurrent = 0f; public static float HPMax = 1f; public static float RudderVanilla = 0f; public static float RudderNjord = 0f; public static float SteeringMultiplier = 1f; public static float SteeringGainPerSecond = 0f; private static float _lastRudder = 0f; private static float _lastRudderTime = 0f; public static float SpeedCurrent = 0f; public static float SpeedVanillaBaseline = 0f; public static float SpeedGainPerSecond = 0f; private static float _lastSpeed = 0f; private static float _lastSpeedTime = 0f; public static float NjordSpeedPrev = 0f; public static float NjordSpeedCurr = 0f; public static float MortalSpeedPrev = 0f; public static float MortalSpeedCurr = 0f; public static float WindUIBlend = 0f; public static Color WindUIColor = Color.white; public static bool HasError = false; public static bool DepthError = false; public static bool WaterError = false; public static bool SpeedError = false; public static bool RudderError = false; public static void ClearErrors() { HasError = (DepthError = (WaterError = (SpeedError = (RudderError = false)))); } public static void UpdateBasic(Ship ship, float waterLevel, float depth, bool floating, float readinessTimer, bool gateReady) { if (!ship) { return; } VesselName = ship.name.Replace("(Clone)", "").Trim(); GateReady = gateReady; IsFloating = floating; ClearErrors(); if (float.IsNaN(waterLevel) || float.IsInfinity(waterLevel)) { WaterError = true; HasError = true; waterLevel = ship.transform.position.y; } WaterLevel = waterLevel; BoatY = ship.transform.position.y; if (float.IsNaN(depth) || float.IsInfinity(depth)) { DepthError = true; HasError = true; depth = 0f; } Depth = depth; DisableLevel = ship.m_disableLevel; ReadinessTimer = readinessTimer; ReadinessMax = 0.35f; float time = Time.time; if (_lastSpeedTime > 0f) { float num = time - _lastSpeedTime; if (num > 0f) { SpeedGainPerSecond = (SpeedCurrent - _lastSpeed) / num; } } _lastSpeed = SpeedCurrent; _lastSpeedTime = time; } public static void SetHP(float hp, float maxHp) { HPCurrent = Mathf.Max(0f, hp); HPMax = Mathf.Max(1f, maxHp); } public static void ReportRudder(float vanilla, float njord, float mult) { if (float.IsNaN(njord)) { RudderError = true; HasError = true; njord = 0f; } RudderVanilla = vanilla; RudderNjord = njord; SteeringMultiplier = mult; float time = Time.time; if (_lastRudderTime > 0f) { float num = time - _lastRudderTime; if (num > 0f) { SteeringGainPerSecond = (njord - _lastRudder) / num; } } _lastRudder = njord; _lastRudderTime = time; } public static void SetWindUI(float blend, Color color) { WindUIBlend = Mathf.Clamp01(blend); WindUIColor = color; } public static void SetVanillaSpeedBaseline(float baseline) { SpeedVanillaBaseline = baseline; } public static void UpdateSpeed(float prevNjord, float currNjord, float prevMortal, float currMortal) { if (float.IsNaN(currNjord)) { SpeedError = true; HasError = true; currNjord = 0f; } NjordSpeedPrev = prevNjord; NjordSpeedCurr = currNjord; MortalSpeedPrev = prevMortal; MortalSpeedCurr = currMortal; SpeedCurrent = currNjord; SpeedVanillaBaseline = currMortal; float time = Time.time; if (_lastSpeedTime > 0f) { float num = time - _lastSpeedTime; if (num > 0f) { SpeedGainPerSecond = (currNjord - _lastSpeed) / num; } } _lastSpeed = currNjord; _lastSpeedTime = time; } } } // ---- Njord/plugins/Njord.dll :: Njord.Core.NjordPatchCaller ---- namespace Njord.Core { public static class NjordPatchCaller { private static Harmony harmony; public static void ApplyPatches() { if (harmony == null) { harmony = new Harmony("wubarrk.njord"); } NjordDebug.Log("When Njord stirs, the runes awaken…"); BindRune("Þráð‑Njörðr Rune", typeof(Njord_PlayerSpawned_Init), "Njord tugs the world‑threads taut; the longship joins the weave.", "The Thread‑Rune slips from Njord’s grasp, but the sea waits for no one."); BindRune("Njord’s Fang‑Rune", typeof(NjordSeaDogEngine), "The deep bares its fangs and drives the longship onward.", "The Fang‑Rune cracks, but the sea remembers its path."); BindRune("Hrafn‑Skygg Rune", typeof(NjordTheWindsCall), "The ravens whisper the winds’ hidden counsel.", "The Raven‑Sight Rune dims, yet the sky still watches."); BindRune("Stýri‑Hjarta Rune", typeof(Patch_Ship_ApplyControlls), "The helm‑heart beats in rhythm with mortal will.", "The Helm‑Heart falters, but the rudder keeps its vigil."); BindRune("Band‑Njörðr Rune", typeof(NjordDoodadControlRune), "Njord knots the helmsman’s spirit to the vessel’s wyrd.", "The Binding Rune slips, yet the helm awaits another hand."); BindRune("Leys‑Njörðr Rune", typeof(NjordDoodadReleaseRune), "Njord loosens the knot; the helm drifts free into quiet waters.", "The Loosening Rune fades, but the sea releases its hold."); NjordDebug.Log("The longship answers the call of the deep."); } private static void BindRune(string runeName, Type patchType, string successSaga, string failureSaga) { NjordDebug.Log("Invoking " + runeName + "…"); try { harmony.PatchAll(patchType); NjordDebug.Log(successSaga); } catch (Exception ex) { NjordDebug.Warn(failureSaga ?? ""); NjordDebug.Warn("Rune‑scribe’s note: " + ex.Message); } } } } // ---- Njord/plugins/Njord.dll :: Njord.Core.NjordRuneTextureGenerator ---- namespace Njord.Core { public static class NjordRuneTextureGenerator { private const int Size = 128; public static Texture2D GenerateRune(Color color) { int num = Mathf.Abs(color.GetHashCode()); int runeIndex = num % 16; return GenerateRune(color, runeIndex); } public static Texture2D GenerateSoftTrail(Color color) { Texture2D texture2D = new Texture2D(128, 128, TextureFormat.RGBA32, mipChain: false); texture2D.filterMode = FilterMode.Bilinear; Vector2 b = new Vector2(64f, 64f); float num = 64f; for (int i = 0; i < 128; i++) { for (int j = 0; j < 128; j++) { Vector2 a = new Vector2((float)j + 0.5f, (float)i + 0.5f); float num2 = Vector2.Distance(a, b) / num; float t = Mathf.Clamp01(1f - num2); t = Mathf.SmoothStep(0f, 1f, t); t = Mathf.Pow(t, 1.6f); Color color2 = new Color(color.r, color.g, color.b, color.a * t); texture2D.SetPixel(j, i, color2); } } texture2D.Apply(updateMipmaps: true); return texture2D; } public static Texture2D GenerateStreak(Color color) { Texture2D texture2D = new Texture2D(128, 128, TextureFormat.RGBA32, mipChain: false); texture2D.filterMode = FilterMode.Bilinear; Vector2 vector = new Vector2(64f, 64f); float num = 64f; for (int i = 0; i < 128; i++) { for (int j = 0; j < 128; j++) { float t = Mathf.Abs((float)i + 0.5f - vector.y) / num; float num2 = Mathf.Abs((float)j + 0.5f - vector.x) / (num * 0.6f); float num3 = Mathf.Clamp01(1f - num2); float f = Mathf.SmoothStep(1f, 0f, t); num3 *= Mathf.Pow(f, 0.9f); Color color2 = new Color(color.r, color.g, color.b, color.a * num3); texture2D.SetPixel(j, i, color2); } } texture2D.Apply(updateMipmaps: true); return texture2D; } public static Texture2D GenerateBlend(Color color) { Texture2D texture2D = GenerateSoftTrail(color); Texture2D texture2D2 = GenerateRune(color); Texture2D texture2D3 = new Texture2D(128, 128, TextureFormat.RGBA32, mipChain: false); texture2D3.filterMode = FilterMode.Bilinear; for (int i = 0; i < 128; i++) { for (int j = 0; j < 128; j++) { Color pixel = texture2D.GetPixel(j, i); Color pixel2 = texture2D2.GetPixel(j, i); float num = Mathf.Clamp01(pixel.a + pixel2.a * 0.65f); Color color2 = new Color(color.r, color.g, color.b, color.a * num); texture2D3.SetPixel(j, i, color2); } } texture2D3.Apply(updateMipmaps: true); return texture2D3; } public static Texture2D GenerateRune(Color color, int runeIndex) { Texture2D texture2D = new Texture2D(128, 128, TextureFormat.RGBA32, mipChain: true); texture2D.filterMode = FilterMode.Bilinear; texture2D.wrapMode = TextureWrapMode.Clamp; Color color2 = new Color(0f, 0f, 0f, 0f); Color c = new Color(color.r * 0.6f, color.g * 0.6f, color.b * 0.6f, 0.6f); Color c2 = new Color(color.r, color.g, color.b, 1f); Color[] array = new Color[16384]; for (int i = 0; i < array.Length; i++) { array[i] = color2; } texture2D.SetPixels(array); switch (runeIndex) { case 0: DrawLine(texture2D, 20, 20, 108, 108, c, 6); DrawLine(texture2D, 108, 20, 20, 108, c, 6); DrawLine(texture2D, 20, 20, 108, 108, c2, 3); DrawLine(texture2D, 108, 20, 20, 108, c2, 3); break; case 1: DrawLine(texture2D, 64, 16, 64, 112, c, 6); DrawLine(texture2D, 64, 64, 32, 96, c, 5); DrawLine(texture2D, 64, 64, 96, 96, c, 5); DrawLine(texture2D, 64, 16, 64, 112, c2, 3); DrawLine(texture2D, 64, 64, 32, 96, c2, 2); DrawLine(texture2D, 64, 64, 96, 96, c2, 2); break; case 2: DrawLine(texture2D, 40, 20, 40, 108, c, 6); DrawLine(texture2D, 40, 96, 96, 112, c, 5); DrawLine(texture2D, 40, 72, 96, 88, c, 5); DrawLine(texture2D, 40, 20, 40, 108, c2, 3); DrawLine(texture2D, 40, 96, 96, 112, c2, 2); DrawLine(texture2D, 40, 72, 96, 88, c2, 2); break; case 3: DrawLine(texture2D, 40, 20, 40, 112, c, 6); DrawLine(texture2D, 40, 96, 88, 112, c, 5); DrawLine(texture2D, 88, 112, 88, 80, c, 5); DrawLine(texture2D, 88, 80, 40, 96, c, 5); DrawLine(texture2D, 40, 20, 40, 112, c2, 3); DrawLine(texture2D, 40, 96, 88, 112, c2, 2); DrawLine(texture2D, 88, 112, 88, 80, c2, 2); DrawLine(texture2D, 88, 80, 40, 96, c2, 2); break; case 4: DrawLine(texture2D, 64, 16, 64, 112, c, 8); DrawLine(texture2D, 64, 16, 64, 112, c2, 4); break; case 5: DrawLine(texture2D, 64, 16, 64, 112, c, 6); DrawLine(texture2D, 64, 40, 32, 72, c, 5); DrawLine(texture2D, 64, 40, 96, 72, c, 5); DrawLine(texture2D, 64, 16, 64, 112, c2, 3); DrawLine(texture2D, 64, 40, 32, 72, c2, 2); DrawLine(texture2D, 64, 40, 96, 72, c2, 2); break; case 6: DrawLine(texture2D, 36, 16, 36, 112, c, 6); DrawLine(texture2D, 92, 16, 92, 112, c, 6); DrawLine(texture2D, 36, 36, 92, 92, c, 5); DrawLine(texture2D, 36, 16, 36, 112, c2, 3); DrawLine(texture2D, 92, 16, 92, 112, c2, 3); DrawLine(texture2D, 36, 36, 92, 92, c2, 2); break; default: DrawLine(texture2D, 80, 20, 40, 20, c, 5); DrawLine(texture2D, 80, 20, 40, 64, c, 5); DrawLine(texture2D, 40, 64, 80, 108, c, 5); DrawLine(texture2D, 80, 108, 40, 108, c, 5); DrawLine(texture2D, 80, 20, 40, 20, c2, 2); DrawLine(texture2D, 80, 20, 40, 64, c2, 2); DrawLine(texture2D, 40, 64, 80, 108, c2, 2); DrawLine(texture2D, 80, 108, 40, 108, c2, 2); break; case 8: DrawLine(texture2D, 64, 16, 64, 112, c, 6); DrawLine(texture2D, 96, 96, 32, 32, c, 5); DrawLine(texture2D, 64, 16, 64, 112, c2, 3); DrawLine(texture2D, 96, 96, 32, 32, c2, 2); break; case 9: DrawLine(texture2D, 32, 48, 32, 112, c, 6); DrawLine(texture2D, 96, 48, 96, 112, c, 6); DrawLine(texture2D, 32, 48, 64, 16, c, 5); DrawLine(texture2D, 64, 16, 96, 48, c, 5); DrawLine(texture2D, 32, 48, 32, 112, c2, 3); DrawLine(texture2D, 96, 48, 96, 112, c2, 3); DrawLine(texture2D, 32, 48, 64, 16, c2, 2); DrawLine(texture2D, 64, 16, 96, 48, c2, 2); break; case 10: DrawLine(texture2D, 48, 16, 48, 112, c, 6); DrawLine(texture2D, 48, 80, 96, 48, c, 5); DrawLine(texture2D, 48, 16, 48, 112, c2, 3); DrawLine(texture2D, 48, 80, 96, 48, c2, 2); break; case 11: DrawLine(texture2D, 64, 112, 96, 72, c, 5); DrawLine(texture2D, 96, 72, 64, 32, c, 5); DrawLine(texture2D, 64, 32, 32, 72, c, 5); DrawLine(texture2D, 32, 72, 64, 112, c, 5); DrawLine(texture2D, 32, 72, 16, 16, c, 5); DrawLine(texture2D, 96, 72, 112, 16, c, 5); DrawLine(texture2D, 64, 112, 96, 72, c2, 2); DrawLine(texture2D, 96, 72, 64, 32, c2, 2); DrawLine(texture2D, 64, 32, 32, 72, c2, 2); DrawLine(texture2D, 32, 72, 64, 112, c2, 2); DrawLine(texture2D, 32, 72, 16, 16, c2, 2); DrawLine(texture2D, 96, 72, 112, 16, c2, 2); break; case 12: DrawLine(texture2D, 20, 20, 64, 64, c, 5); DrawLine(texture2D, 20, 108, 64, 64, c, 5); DrawLine(texture2D, 108, 20, 64, 64, c, 5); DrawLine(texture2D, 108, 108, 64, 64, c, 5); DrawLine(texture2D, 20, 20, 64, 64, c2, 2); DrawLine(texture2D, 20, 108, 64, 64, c2, 2); DrawLine(texture2D, 108, 20, 64, 64, c2, 2); DrawLine(texture2D, 108, 108, 64, 64, c2, 2); break; case 13: DrawLine(texture2D, 36, 16, 36, 112, c, 6); DrawLine(texture2D, 36, 96, 80, 80, c, 5); DrawLine(texture2D, 80, 80, 36, 64, c, 5); DrawLine(texture2D, 36, 64, 80, 48, c, 5); DrawLine(texture2D, 80, 48, 36, 32, c, 5); DrawLine(texture2D, 36, 16, 36, 112, c2, 3); DrawLine(texture2D, 36, 96, 80, 80, c2, 2); DrawLine(texture2D, 80, 80, 36, 64, c2, 2); DrawLine(texture2D, 36, 64, 80, 48, c2, 2); DrawLine(texture2D, 80, 48, 36, 32, c2, 2); break; case 14: DrawLine(texture2D, 32, 16, 32, 112, c, 6); DrawLine(texture2D, 96, 16, 96, 112, c, 6); DrawLine(texture2D, 32, 112, 96, 64, c, 5); DrawLine(texture2D, 96, 112, 32, 64, c, 5); DrawLine(texture2D, 32, 16, 32, 112, c2, 3); DrawLine(texture2D, 96, 16, 96, 112, c2, 3); DrawLine(texture2D, 32, 112, 96, 64, c2, 2); DrawLine(texture2D, 96, 112, 32, 64, c2, 2); break; case 15: DrawLine(texture2D, 64, 16, 108, 64, c, 6); DrawLine(texture2D, 108, 64, 64, 112, c, 6); DrawLine(texture2D, 64, 112, 20, 64, c, 6); DrawLine(texture2D, 20, 64, 64, 16, c, 6); DrawLine(texture2D, 64, 16, 108, 64, c2, 3); DrawLine(texture2D, 108, 64, 64, 112, c2, 3); DrawLine(texture2D, 64, 112, 20, 64, c2, 3); DrawLine(texture2D, 20, 64, 64, 16, c2, 3); break; } texture2D.Apply(updateMipmaps: true); return texture2D; } private static void DrawLine(Texture2D tex, int x0, int y0, int x1, int y1, Color c, int thickness) { int num = Mathf.Abs(x1 - x0); int num2 = Mathf.Abs(y1 - y0); int num3 = ((x0 < x1) ? 1 : (-1)); int num4 = ((y0 < y1) ? 1 : (-1)); int num5 = num - num2; while (true) { DrawCircle(tex, x0, y0, thickness, c); if (x0 != x1 || y0 != y1) { int num6 = 2 * num5; if (num6 > -num2) { num5 -= num2; x0 += num3; } if (num6 < num) { num5 += num; y0 += num4; } continue; } break; } } private static void DrawCircle(Texture2D tex, int cx, int cy, int r, Color c) { for (int i = -r; i <= r; i++) { for (int j = -r; j <= r; j++) { if (j * j + i * i <= r * r) { int num = cx + j; int num2 = cy + i; if (num >= 0 && num < 128 && num2 >= 0 && num2 < 128) { tex.SetPixel(num, num2, c); } } } } } } }