// Consolidated decompiled source — Wubarrk-Njord v1.3.2 // 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 BepInEx.Configuration; using JetBrains.Annotations; using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.Serialization; using HarmonyLib; using UnityEngine; using BepInEx; using TMPro; using Njord.Core; using BepInEx.Bootstrap; using BepInEx.Logging; using Njord.HUD; using UnityEngine.Rendering; using ServerSync; 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 :: ServerSync.OwnConfigEntryBase ---- namespace ServerSync { [PublicAPI] public abstract class OwnConfigEntryBase { public object? LocalBaseValue; public bool SynchronizedConfig = true; public abstract ConfigEntryBase BaseConfig { get; } } } // ---- Njord/plugins/Njord.dll :: ServerSync.SyncedConfigEntry`1 ---- namespace ServerSync { [PublicAPI] public class SyncedConfigEntry(ConfigEntry sourceConfig) : OwnConfigEntryBase { public readonly ConfigEntry SourceConfig = sourceConfig; public override ConfigEntryBase BaseConfig => SourceConfig; public T Value { get { return SourceConfig.Value; } set { SourceConfig.Value = value; } } public void AssignLocalValue(T value) { if (LocalBaseValue == null) { Value = value; } else { LocalBaseValue = value; } } } } // ---- Njord/plugins/Njord.dll :: ServerSync.CustomSyncedValueBase ---- namespace ServerSync { public abstract class CustomSyncedValueBase { public object? LocalBaseValue; public readonly string Identifier; public readonly Type Type; private object? boxedValue; protected bool localIsOwner; public readonly int Priority; public object? BoxedValue { get { return boxedValue; } set { boxedValue = value; this.ValueChanged?.Invoke(); } } public event Action? ValueChanged; protected CustomSyncedValueBase(ConfigSync configSync, string identifier, Type type, int priority) { Priority = priority; Identifier = identifier; Type = type; configSync.AddCustomValue(this); localIsOwner = configSync.IsSourceOfTruth; configSync.SourceOfTruthChanged += delegate(bool truth) { localIsOwner = truth; }; } } } // ---- Njord/plugins/Njord.dll :: ServerSync.CustomSyncedValue`1 ---- namespace ServerSync { [PublicAPI] public sealed class CustomSyncedValue : CustomSyncedValueBase { public T Value { get { return (T)base.BoxedValue; } set { base.BoxedValue = value; } } public CustomSyncedValue(ConfigSync configSync, string identifier, T value = default(T), int priority = 0) : base(configSync, identifier, typeof(T), priority) { Value = value; } public void AssignLocalValue(T value) { if (localIsOwner) { Value = value; } else { LocalBaseValue = value; } } } } // ---- Njord/plugins/Njord.dll :: ServerSync.ConfigurationManagerAttributes ---- namespace ServerSync { internal class ConfigurationManagerAttributes { [UsedImplicitly] public bool? ReadOnly = false; } } // ---- Njord/plugins/Njord.dll :: ServerSync.ConfigSync ---- namespace ServerSync { [PublicAPI] public class ConfigSync { [HarmonyPatch(typeof(ZRpc), "HandlePackage")] private static class SnatchCurrentlyHandlingRPC { public static ZRpc? currentRpc; [HarmonyPrefix] private static void Prefix(ZRpc __instance) { currentRpc = __instance; } } [HarmonyPatch(typeof(ZNet), "Awake")] internal static class RegisterRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance) { isServer = __instance.IsServer(); foreach (ConfigSync configSync2 in configSyncs) { ZRoutedRpc.instance.Register(configSync2.Name + " ConfigSync", configSync2.RPC_FromOtherClientConfigSync); if (isServer) { configSync2.InitialSyncDone = true; Debug.Log("Registered '" + configSync2.Name + " ConfigSync' RPC - waiting for incoming connections"); } } if (isServer) { __instance.StartCoroutine(WatchAdminListChanges()); } static void SendAdmin(List peers, bool isAdmin) { ZPackage package = ConfigsToPackage(null, null, new PackageEntry[1] { new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = isAdmin } }); ConfigSync configSync = configSyncs.First(); if (configSync != null) { ZNet.instance.StartCoroutine(configSync.sendZPackage(peers, package)); } } static IEnumerator WatchAdminListChanges() { MethodInfo listContainsId = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId"); SyncedList adminList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); List CurrentList = new List(adminList.GetList()); while (true) { yield return new WaitForSeconds(30f); if (!adminList.GetList().SequenceEqual(CurrentList)) { CurrentList = new List(adminList.GetList()); List list = ZNet.instance.GetPeers().Where(delegate(ZNetPeer p) { string hostName = p.m_rpc.GetSocket().GetHostName(); return ((object)listContainsId != null) ? ((bool)listContainsId.Invoke(ZNet.instance, new object[2] { adminList, hostName })) : adminList.Contains(hostName); }).ToList(); SendAdmin(ZNet.instance.GetPeers().Except(list).ToList(), isAdmin: false); SendAdmin(list, isAdmin: true); } } } } } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static class RegisterClientRPCPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZNetPeer peer) { if (__instance.IsServer()) { return; } foreach (ConfigSync configSync in configSyncs) { peer.m_rpc.Register(configSync.Name + " ConfigSync", configSync.RPC_FromServerConfigSync); } } } private class ParsedConfigs { public readonly Dictionary configValues = new Dictionary(); public readonly Dictionary customValues = new Dictionary(); } [HarmonyPatch(typeof(ZNet), "Shutdown")] private class ResetConfigsOnShutdown { [HarmonyPostfix] private static void Postfix() { ProcessingServerUpdate = true; foreach (ConfigSync configSync in configSyncs) { configSync.resetConfigsFromServer(); configSync.IsSourceOfTruth = true; configSync.InitialSyncDone = false; } ProcessingServerUpdate = false; } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private class SendConfigsAfterLogin { private class BufferingSocket(ISocket original) : ZPlayFabSocket, ISocket { public volatile bool finished; public volatile int versionMatchQueued = -1; public readonly List Package = new List(); public readonly ISocket Original = original; public new bool IsConnected() { return Original.IsConnected(); } public new ZPackage Recv() { return Original.Recv(); } public new int GetSendQueueSize() { return Original.GetSendQueueSize(); } public new int GetCurrentSendRate() { return Original.GetCurrentSendRate(); } public new bool IsHost() { return Original.IsHost(); } public new void Dispose() { Original.Dispose(); } public new bool GotNewData() { return Original.GotNewData(); } public new void Close() { Original.Close(); } public new string GetEndPointString() { return Original.GetEndPointString(); } public new void GetAndResetStats(out int totalSent, out int totalRecv) { Original.GetAndResetStats(out totalSent, out totalRecv); } public new void GetConnectionQuality(out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { Original.GetConnectionQuality(out localQuality, out remoteQuality, out ping, out outByteSec, out inByteSec); } public new ISocket Accept() { return Original.Accept(); } public new int GetHostPort() { return Original.GetHostPort(); } public new bool Flush() { return Original.Flush(); } public new string GetHostName() { return Original.GetHostName(); } public new void VersionMatch() { if (finished) { Original.VersionMatch(); } else { versionMatchQueued = Package.Count; } } public new void Send(ZPackage pkg) { int pos = pkg.GetPos(); pkg.SetPos(0); int num = pkg.ReadInt(); if ((num == "PeerInfo".GetStableHashCode() || num == "RoutedRPC".GetStableHashCode() || num == "ZDOData".GetStableHashCode()) && !finished) { ZPackage zPackage = new ZPackage(pkg.GetArray()); zPackage.SetPos(pos); Package.Add(zPackage); } else { pkg.SetPos(pos); Original.Send(pkg); } } } [HarmonyPriority(800)] [HarmonyPrefix] private static void Prefix(ref Dictionary? __state, ZNet __instance, ZRpc rpc) { if (!__instance.IsServer()) { return; } BufferingSocket bufferingSocket = new BufferingSocket(rpc.GetSocket()); AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket); if (AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }).Invoke(__instance, new object[1] { rpc }) is ZNetPeer obj && ZNet.m_onlineBackend != OnlineBackendType.Steamworks) { FieldInfo fieldInfo = AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket"); if (fieldInfo.GetValue(obj) is ZPlayFabSocket zPlayFabSocket) { typeof(ZPlayFabSocket).GetField("m_remotePlayerId").SetValue(bufferingSocket, zPlayFabSocket.m_remotePlayerId); } fieldInfo.SetValue(obj, bufferingSocket); } if (__state == null) { __state = new Dictionary(); } __state[Assembly.GetExecutingAssembly()] = bufferingSocket; } [HarmonyPostfix] private static void Postfix(Dictionary __state, ZNet __instance, ZRpc rpc) { ZNetPeer peer; if (__instance.IsServer()) { object obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }).Invoke(__instance, new object[1] { rpc }); peer = obj as ZNetPeer; if (peer == null) { SendBufferedData(); } else { __instance.StartCoroutine(sendAsync()); } } IEnumerator sendAsync() { foreach (ConfigSync configSync in configSyncs) { List list = new List(); if (configSync.CurrentVersion != null) { list.Add(new PackageEntry { section = "Internal", key = "serverversion", type = typeof(string), value = configSync.CurrentVersion }); } MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId"); SyncedList syncedList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); list.Add(new PackageEntry { section = "Internal", key = "lockexempt", type = typeof(bool), value = (((object)methodInfo == null) ? ((object)syncedList.Contains(rpc.GetSocket().GetHostName())) : methodInfo.Invoke(ZNet.instance, new object[2] { syncedList, rpc.GetSocket().GetHostName() })) }); ZPackage package = ConfigsToPackage(configSync.allConfigs.Select((OwnConfigEntryBase c) => c.BaseConfig), configSync.allCustomValues, list, partial: false); yield return __instance.StartCoroutine(configSync.sendZPackage(new List { peer }, package)); } SendBufferedData(); } void SendBufferedData() { if (rpc.GetSocket() is BufferingSocket bufferingSocket) { AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket.Original); if (AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }).Invoke(__instance, new object[1] { rpc }) is ZNetPeer obj2) { AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket").SetValue(obj2, bufferingSocket.Original); } } BufferingSocket bufferingSocket2 = __state[Assembly.GetExecutingAssembly()]; bufferingSocket2.finished = true; for (int i = 0; i < bufferingSocket2.Package.Count; i++) { if (i == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } bufferingSocket2.Original.Send(bufferingSocket2.Package[i]); } if (bufferingSocket2.Package.Count == bufferingSocket2.versionMatchQueued) { bufferingSocket2.Original.VersionMatch(); } } } } private class PackageEntry { public string section; public string key; public Type type; public object? value; } [HarmonyPatch(typeof(ConfigEntryBase), "GetSerializedValue")] private static class PreventSavingServerInfo { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, ref string __result) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || isWritableConfig(ownConfigEntryBase)) { return true; } __result = TomlTypeConverter.ConvertToString(ownConfigEntryBase.LocalBaseValue, __instance.SettingType); return false; } } [HarmonyPatch(typeof(ConfigEntryBase), "SetSerializedValue")] private static class PreventConfigRereadChangingValues { [HarmonyPrefix] private static bool Prefix(ConfigEntryBase __instance, string value) { OwnConfigEntryBase ownConfigEntryBase = configData(__instance); if (ownConfigEntryBase == null || ownConfigEntryBase.LocalBaseValue == null) { return true; } try { ownConfigEntryBase.LocalBaseValue = TomlTypeConverter.ConvertToValue(value, __instance.SettingType); } catch (Exception ex) { Debug.LogWarning($"Config value of setting \"{__instance.Definition}\" could not be parsed and will be ignored. Reason: {ex.Message}; Value: {value}"); } return false; } } private class InvalidDeserializationTypeException : Exception { public string expected; public string received; public string field = ""; } public static bool ProcessingServerUpdate; public readonly string Name; public string? DisplayName; public string? CurrentVersion; public string? MinimumRequiredVersion; public bool ModRequired; private bool? forceConfigLocking; private bool isSourceOfTruth = true; private static readonly HashSet configSyncs; private readonly HashSet allConfigs = new HashSet(); private HashSet allCustomValues = new HashSet(); private static bool isServer; private static bool lockExempt; private OwnConfigEntryBase? lockedConfig; private const byte PARTIAL_CONFIGS = 1; private const byte FRAGMENTED_CONFIG = 2; private const byte COMPRESSED_CONFIG = 4; private readonly Dictionary> configValueCache = new Dictionary>(); private readonly List> cacheExpirations = new List>(); private static long packageCounter; public bool IsLocked { get { bool? flag = forceConfigLocking; bool num; if (!flag.HasValue) { if (lockedConfig == null) { goto IL_0051; } num = ((IConvertible)lockedConfig.BaseConfig.BoxedValue).ToInt32(CultureInfo.InvariantCulture) != 0; } else { num = flag == true; } if (num) { return !lockExempt; } goto IL_0051; IL_0051: return false; } set { forceConfigLocking = value; } } public bool IsAdmin { get { if (!lockExempt) { return isSourceOfTruth; } return true; } } public bool IsSourceOfTruth { get { return isSourceOfTruth; } private set { if (value != isSourceOfTruth) { isSourceOfTruth = value; this.SourceOfTruthChanged?.Invoke(value); } } } public bool InitialSyncDone { get; private set; } public event Action? SourceOfTruthChanged; private event Action? lockedConfigChanged; static ConfigSync() { ProcessingServerUpdate = false; configSyncs = new HashSet(); lockExempt = false; packageCounter = 0L; RuntimeHelpers.RunClassConstructor(typeof(VersionCheck).TypeHandle); } public ConfigSync(string name) { Name = name; configSyncs.Add(this); new VersionCheck(this); } public SyncedConfigEntry AddConfigEntry(ConfigEntry configEntry) { OwnConfigEntryBase ownConfigEntryBase = configData(configEntry); SyncedConfigEntry syncedEntry = ownConfigEntryBase as SyncedConfigEntry; if (syncedEntry == null) { syncedEntry = new SyncedConfigEntry(configEntry); AccessTools.DeclaredField(typeof(ConfigDescription), "k__BackingField").SetValue(configEntry.Description, new object[1] { new ConfigurationManagerAttributes() }.Concat(configEntry.Description.Tags ?? Array.Empty()).Concat(new SyncedConfigEntry[1] { syncedEntry }).ToArray()); configEntry.SettingChanged += delegate { if (!ProcessingServerUpdate && syncedEntry.SynchronizedConfig) { Broadcast(ZRoutedRpc.Everybody, configEntry); } }; allConfigs.Add(syncedEntry); } return syncedEntry; } public SyncedConfigEntry AddLockingConfigEntry(ConfigEntry lockingConfig) where T : IConvertible { if (lockedConfig != null) { throw new Exception("Cannot initialize locking ConfigEntry twice"); } lockedConfig = AddConfigEntry(lockingConfig); lockingConfig.SettingChanged += delegate { this.lockedConfigChanged?.Invoke(); }; return (SyncedConfigEntry)lockedConfig; } internal void AddCustomValue(CustomSyncedValueBase customValue) { if (allCustomValues.Select((CustomSyncedValueBase v) => v.Identifier).Concat(new string[1] { "serverversion" }).Contains(customValue.Identifier)) { throw new Exception("Cannot have multiple settings with the same name or with a reserved name (serverversion)"); } allCustomValues.Add(customValue); allCustomValues = new HashSet(allCustomValues.OrderByDescending((CustomSyncedValueBase v) => v.Priority)); customValue.ValueChanged += delegate { if (!ProcessingServerUpdate) { Broadcast(ZRoutedRpc.Everybody, customValue); } }; } private void RPC_FromServerConfigSync(ZRpc rpc, ZPackage package) { lockedConfigChanged += serverLockedSettingChanged; IsSourceOfTruth = false; if (HandleConfigSyncRPC(0L, package, clientUpdate: false)) { InitialSyncDone = true; } } private void RPC_FromOtherClientConfigSync(long sender, ZPackage package) { HandleConfigSyncRPC(sender, package, clientUpdate: true); } private bool HandleConfigSyncRPC(long sender, ZPackage package, bool clientUpdate) { try { if (isServer && IsLocked) { string text = SnatchCurrentlyHandlingRPC.currentRpc?.GetSocket()?.GetHostName(); if (text != null) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId"); SyncedList syncedList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); if (!(((object)methodInfo == null) ? syncedList.Contains(text) : ((bool)methodInfo.Invoke(ZNet.instance, new object[2] { syncedList, text })))) { return false; } } } cacheExpirations.RemoveAll(delegate(KeyValuePair kv) { if (kv.Key < DateTimeOffset.Now.Ticks) { configValueCache.Remove(kv.Value); return true; } return false; }); byte b = package.ReadByte(); if ((b & 2) != 0) { long num = package.ReadLong(); string text2 = sender.ToString() + num; if (!configValueCache.TryGetValue(text2, out var value)) { value = new SortedDictionary(); configValueCache[text2] = value; cacheExpirations.Add(new KeyValuePair(DateTimeOffset.Now.AddSeconds(60.0).Ticks, text2)); } int key = package.ReadInt(); int num2 = package.ReadInt(); value.Add(key, package.ReadByteArray()); if (value.Count < num2) { return false; } configValueCache.Remove(text2); package = new ZPackage(value.Values.SelectMany((byte[] a) => a).ToArray()); b = package.ReadByte(); } ProcessingServerUpdate = true; if ((b & 4) != 0) { MemoryStream stream = new MemoryStream(package.ReadByteArray()); MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(stream, CompressionMode.Decompress)) { deflateStream.CopyTo(memoryStream); } package = new ZPackage(memoryStream.ToArray()); b = package.ReadByte(); } if ((b & 1) == 0) { resetConfigsFromServer(); } ParsedConfigs parsedConfigs = ReadConfigsFromPackage(package); ConfigFile configFile = null; bool saveOnConfigSet = false; foreach (KeyValuePair configValue in parsedConfigs.configValues) { if (!isServer && configValue.Key.LocalBaseValue == null) { configValue.Key.LocalBaseValue = configValue.Key.BaseConfig.BoxedValue; } if (configFile == null) { configFile = configValue.Key.BaseConfig.ConfigFile; saveOnConfigSet = configFile.SaveOnConfigSet; configFile.SaveOnConfigSet = false; } configValue.Key.BaseConfig.BoxedValue = configValue.Value; } if (configFile != null) { configFile.SaveOnConfigSet = saveOnConfigSet; configFile.Save(); } foreach (KeyValuePair customValue in parsedConfigs.customValues) { if (!isServer) { CustomSyncedValueBase key2 = customValue.Key; if (key2.LocalBaseValue == null) { key2.LocalBaseValue = customValue.Key.BoxedValue; } } customValue.Key.BoxedValue = customValue.Value; } Debug.Log(string.Format("Received {0} configs and {1} custom values from {2} for mod {3}", parsedConfigs.configValues.Count, parsedConfigs.customValues.Count, (isServer || clientUpdate) ? $"client {sender}" : "the server", DisplayName ?? Name)); if (!isServer) { serverLockedSettingChanged(); } return true; } finally { ProcessingServerUpdate = false; } } private ParsedConfigs ReadConfigsFromPackage(ZPackage package) { ParsedConfigs parsedConfigs = new ParsedConfigs(); Dictionary dictionary = allConfigs.Where((OwnConfigEntryBase c) => c.SynchronizedConfig).ToDictionary((OwnConfigEntryBase c) => c.BaseConfig.Definition.Section + "_" + c.BaseConfig.Definition.Key, (OwnConfigEntryBase c) => c); Dictionary dictionary2 = allCustomValues.ToDictionary((CustomSyncedValueBase c) => c.Identifier, (CustomSyncedValueBase c) => c); int num = package.ReadInt(); for (int num2 = 0; num2 < num; num2++) { string text = package.ReadString(); string text2 = package.ReadString(); string text3 = package.ReadString(); Type type = Type.GetType(text3); if (text3 == "" || type != null) { object obj; try { obj = ((text3 == "") ? null : ReadValueWithTypeFromZPackage(package, type)); } catch (InvalidDeserializationTypeException ex) { Debug.LogWarning("Got unexpected struct internal type " + ex.received + " for field " + ex.field + " struct " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + ex.expected); continue; } OwnConfigEntryBase value2; if (text == "Internal") { CustomSyncedValueBase value; if (text2 == "serverversion") { if (obj?.ToString() != CurrentVersion) { Debug.LogWarning("Received server version is not equal: server version = " + (obj?.ToString() ?? "null") + "; local version = " + (CurrentVersion ?? "unknown")); } } else if (text2 == "lockexempt") { if (obj is bool flag) { lockExempt = flag; } } else if (dictionary2.TryGetValue(text2, out value)) { if ((text3 == "" && (!value.Type.IsValueType || Nullable.GetUnderlyingType(value.Type) != null)) || GetZPackageTypeString(value.Type) == text3) { parsedConfigs.customValues[value] = obj; continue; } Debug.LogWarning("Got unexpected type " + text3 + " for internal value " + text2 + " for mod " + (DisplayName ?? Name) + ", expecting " + value.Type.AssemblyQualifiedName); } } else if (dictionary.TryGetValue(text + "_" + text2, out value2)) { Type type2 = configType(value2.BaseConfig); if ((text3 == "" && (!type2.IsValueType || Nullable.GetUnderlyingType(type2) != null)) || GetZPackageTypeString(type2) == text3) { parsedConfigs.configValues[value2] = obj; continue; } Debug.LogWarning("Got unexpected type " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + type2.AssemblyQualifiedName); } else { Debug.LogWarning("Received unknown config entry " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ". This may happen if client and server versions of the mod do not match."); } continue; } Debug.LogWarning("Got invalid type " + text3 + ", abort reading of received configs"); return new ParsedConfigs(); } return parsedConfigs; } private static bool isWritableConfig(OwnConfigEntryBase config) { ConfigSync configSync = configSyncs.FirstOrDefault((ConfigSync cs) => cs.allConfigs.Contains(config)); if (configSync == null) { return true; } if (!configSync.IsSourceOfTruth && config.SynchronizedConfig && config.LocalBaseValue != null) { if (!configSync.IsLocked) { if (config == configSync.lockedConfig) { return lockExempt; } return true; } return false; } return true; } private void serverLockedSettingChanged() { foreach (OwnConfigEntryBase allConfig in allConfigs) { configAttribute(allConfig.BaseConfig).ReadOnly = !isWritableConfig(allConfig); } } private void resetConfigsFromServer() { ConfigFile configFile = null; bool saveOnConfigSet = false; foreach (OwnConfigEntryBase item in allConfigs.Where((OwnConfigEntryBase config) => config.LocalBaseValue != null)) { if (configFile == null) { configFile = item.BaseConfig.ConfigFile; saveOnConfigSet = configFile.SaveOnConfigSet; configFile.SaveOnConfigSet = false; } item.BaseConfig.BoxedValue = item.LocalBaseValue; item.LocalBaseValue = null; } if (configFile != null) { configFile.SaveOnConfigSet = saveOnConfigSet; } foreach (CustomSyncedValueBase item2 in allCustomValues.Where((CustomSyncedValueBase config) => config.LocalBaseValue != null)) { item2.BoxedValue = item2.LocalBaseValue; item2.LocalBaseValue = null; } lockedConfigChanged -= serverLockedSettingChanged; serverLockedSettingChanged(); } private IEnumerator distributeConfigToPeers(ZNetPeer peer, ZPackage package) { ZRoutedRpc rpc = ZRoutedRpc.instance; if (rpc == null) { yield break; } byte[] data = package.GetArray(); if (data != null && data.LongLength > 250000) { int fragments = (int)(1 + (data.LongLength - 1) / 250000); long packageIdentifier = ++packageCounter; int fragment = 0; while (fragment < fragments) { foreach (bool item in waitForQueue()) { yield return item; } if (peer.m_socket.IsConnected()) { ZPackage zPackage = new ZPackage(); zPackage.Write((byte)2); zPackage.Write(packageIdentifier); zPackage.Write(fragment); zPackage.Write(fragments); zPackage.Write(data.Skip(250000 * fragment).Take(250000).ToArray()); SendPackage(zPackage); if (fragment != fragments - 1) { yield return true; } int num = fragment + 1; fragment = num; continue; } break; } yield break; } foreach (bool item2 in waitForQueue()) { yield return item2; } SendPackage(package); void SendPackage(ZPackage pkg) { string text = Name + " ConfigSync"; if (isServer) { peer.m_rpc.Invoke(text, pkg); } else { rpc.InvokeRoutedRPC(peer.m_server ? 0 : peer.m_uid, text, pkg); } } IEnumerable waitForQueue() { float timeout = Time.time + 30f; while (peer.m_socket.GetSendQueueSize() > 20000) { if (Time.time > timeout) { Debug.Log($"Disconnecting {peer.m_uid} after 30 seconds config sending timeout"); peer.m_rpc.Invoke("Error", ZNet.ConnectionStatus.ErrorConnectFailed); ZNet.instance.Disconnect(peer); break; } yield return false; } } } private IEnumerator sendZPackage(long target, ZPackage package) { if (!ZNet.instance) { return Enumerable.Empty().GetEnumerator(); } List list = (List)AccessTools.DeclaredField(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance); if (target != ZRoutedRpc.Everybody) { list = list.Where((ZNetPeer p) => p.m_uid == target).ToList(); } return sendZPackage(list, package); } private IEnumerator sendZPackage(List peers, ZPackage package) { if (!ZNet.instance) { yield break; } byte[] array = package.GetArray(); if (array != null && array.LongLength > 10000) { ZPackage zPackage = new ZPackage(); zPackage.Write((byte)4); MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(memoryStream, System.IO.Compression.CompressionLevel.Optimal)) { deflateStream.Write(array, 0, array.Length); } zPackage.Write(memoryStream.ToArray()); package = zPackage; } List> writers = (from p in peers where p.IsReady() select distributeConfigToPeers(p, package)).ToList(); writers.RemoveAll((IEnumerator writer) => !writer.MoveNext()); while (writers.Count > 0) { yield return null; writers.RemoveAll((IEnumerator writer) => !writer.MoveNext()); } } private void Broadcast(long target, params ConfigEntryBase[] configs) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(configs); ZNet.instance?.StartCoroutine(sendZPackage(target, package)); } } private void Broadcast(long target, params CustomSyncedValueBase[] customValues) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(null, customValues); ZNet.instance?.StartCoroutine(sendZPackage(target, package)); } } private static OwnConfigEntryBase? configData(ConfigEntryBase config) { return config.Description.Tags?.OfType().SingleOrDefault(); } public static SyncedConfigEntry? ConfigData(ConfigEntry config) { return config.Description.Tags?.OfType>().SingleOrDefault(); } private static T configAttribute(ConfigEntryBase config) { return config.Description.Tags.OfType().First(); } private static Type configType(ConfigEntryBase config) { return configType(config.SettingType); } private static Type configType(Type type) { if (!type.IsEnum) { return type; } return Enum.GetUnderlyingType(type); } private static ZPackage ConfigsToPackage(IEnumerable? configs = null, IEnumerable? customValues = null, IEnumerable? packageEntries = null, bool partial = true) { List list = configs?.Where((ConfigEntryBase config) => configData(config).SynchronizedConfig).ToList() ?? new List(); List list2 = customValues?.ToList() ?? new List(); ZPackage zPackage = new ZPackage(); zPackage.Write(partial ? ((byte)1) : ((byte)0)); zPackage.Write(list.Count + list2.Count + (packageEntries?.Count() ?? 0)); foreach (PackageEntry item in packageEntries ?? Array.Empty()) { AddEntryToPackage(zPackage, item); } foreach (CustomSyncedValueBase item2 in list2) { AddEntryToPackage(zPackage, new PackageEntry { section = "Internal", key = item2.Identifier, type = item2.Type, value = item2.BoxedValue }); } foreach (ConfigEntryBase item3 in list) { AddEntryToPackage(zPackage, new PackageEntry { section = item3.Definition.Section, key = item3.Definition.Key, type = configType(item3), value = item3.BoxedValue }); } return zPackage; } private static void AddEntryToPackage(ZPackage package, PackageEntry entry) { package.Write(entry.section); package.Write(entry.key); package.Write((entry.value == null) ? "" : GetZPackageTypeString(entry.type)); AddValueToZPackage(package, entry.value); } private static string GetZPackageTypeString(Type type) { return type.AssemblyQualifiedName; } private static void AddValueToZPackage(ZPackage package, object? value) { Type type = value?.GetType(); if (value is Enum) { value = ((IConvertible)value).ToType(Enum.GetUnderlyingType(value.GetType()), CultureInfo.InvariantCulture); } else { if (value is ICollection collection) { package.Write(collection.Count); { foreach (object item in collection) { AddValueToZPackage(package, item); } return; } } if ((object)type != null && type.IsValueType && !type.IsPrimitive) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); package.Write(fields.Length); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { package.Write(GetZPackageTypeString(fieldInfo.FieldType)); AddValueToZPackage(package, fieldInfo.GetValue(value)); } return; } } ZRpc.Serialize(new object[1] { value }, ref package); } private static object ReadValueWithTypeFromZPackage(ZPackage package, Type type) { if ((object)type != null && type.IsValueType && !type.IsPrimitive && !type.IsEnum) { FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); int num = package.ReadInt(); if (num != fields.Length) { throw new InvalidDeserializationTypeException { received = $"(field count: {num})", expected = $"(field count: {fields.Length})" }; } object uninitializedObject = FormatterServices.GetUninitializedObject(type); FieldInfo[] array = fields; foreach (FieldInfo fieldInfo in array) { string text = package.ReadString(); if (text != GetZPackageTypeString(fieldInfo.FieldType)) { throw new InvalidDeserializationTypeException { received = text, expected = GetZPackageTypeString(fieldInfo.FieldType), field = fieldInfo.Name }; } fieldInfo.SetValue(uninitializedObject, ReadValueWithTypeFromZPackage(package, fieldInfo.FieldType)); } return uninitializedObject; } if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<, >)) { int num2 = package.ReadInt(); IDictionary dictionary = (IDictionary)Activator.CreateInstance(type); Type type2 = typeof(KeyValuePair<, >).MakeGenericType(type.GenericTypeArguments); FieldInfo field = type2.GetField("key", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo field2 = type2.GetField("value", BindingFlags.Instance | BindingFlags.NonPublic); for (int j = 0; j < num2; j++) { object obj = ReadValueWithTypeFromZPackage(package, type2); dictionary.Add(field.GetValue(obj), field2.GetValue(obj)); } return dictionary; } if (type != typeof(List) && type.IsGenericType) { Type type3 = typeof(ICollection<>).MakeGenericType(type.GenericTypeArguments[0]); if ((object)type3 != null && type3.IsAssignableFrom(type)) { int num3 = package.ReadInt(); object obj2 = Activator.CreateInstance(type); MethodInfo method = type3.GetMethod("Add"); for (int k = 0; k < num3; k++) { method.Invoke(obj2, new object[1] { ReadValueWithTypeFromZPackage(package, type.GenericTypeArguments[0]) }); } return obj2; } } ParameterInfo parameterInfo = (ParameterInfo)FormatterServices.GetUninitializedObject(typeof(ParameterInfo)); AccessTools.DeclaredField(typeof(ParameterInfo), "ClassImpl").SetValue(parameterInfo, type); List parameters = new List(); ZRpc.Deserialize(new ParameterInfo[2] { null, parameterInfo }, package, ref parameters); return parameters.First(); } } } // ---- Njord/plugins/Njord.dll :: ServerSync.VersionCheck ---- namespace ServerSync { [PublicAPI] [HarmonyPatch] public class VersionCheck { private static readonly HashSet versionChecks; private static readonly Dictionary notProcessedNames; public string Name; private string? displayName; private string? currentVersion; private string? minimumRequiredVersion; public bool ModRequired = true; private string? ReceivedCurrentVersion; private string? ReceivedMinimumRequiredVersion; private readonly List ValidatedClients = new List(); private ConfigSync? ConfigSync; public string DisplayName { get { return displayName ?? Name; } set { displayName = value; } } public string CurrentVersion { get { return currentVersion ?? "0.0.0"; } set { currentVersion = value; } } public string MinimumRequiredVersion { get { string text = minimumRequiredVersion; if (text == null) { if (!ModRequired) { return "0.0.0"; } text = CurrentVersion; } return text; } set { minimumRequiredVersion = value; } } private static void PatchServerSync() { Patches patchInfo = PatchProcessor.GetPatchInfo(AccessTools.DeclaredMethod(typeof(ZNet), "Awake")); if (patchInfo != null && patchInfo.Postfixes.Count((Patch p) => p.PatchMethod.DeclaringType == typeof(ConfigSync.RegisterRPCPatch)) > 0) { return; } Harmony harmony = new Harmony("org.bepinex.helpers.ServerSync"); foreach (Type item in from t in typeof(ConfigSync).GetNestedTypes(BindingFlags.NonPublic).Concat(new Type[1] { typeof(VersionCheck) }) where t.IsClass select t) { harmony.PatchAll(item); } } static VersionCheck() { versionChecks = new HashSet(); notProcessedNames = new Dictionary(); typeof(ThreadingHelper).GetMethod("StartSyncInvoke").Invoke(ThreadingHelper.Instance, new object[1] { new Action(PatchServerSync) }); } public VersionCheck(string name) { Name = name; ModRequired = true; versionChecks.Add(this); } public VersionCheck(ConfigSync configSync) { ConfigSync = configSync; Name = ConfigSync.Name; versionChecks.Add(this); } public void Initialize() { ReceivedCurrentVersion = null; ReceivedMinimumRequiredVersion = null; if (ConfigSync != null) { Name = ConfigSync.Name; DisplayName = ConfigSync.DisplayName; CurrentVersion = ConfigSync.CurrentVersion; MinimumRequiredVersion = ConfigSync.MinimumRequiredVersion; ModRequired = ConfigSync.ModRequired; } } private bool IsVersionOk() { if (ReceivedMinimumRequiredVersion == null || ReceivedCurrentVersion == null) { return !ModRequired; } bool num = new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion); bool flag = new Version(ReceivedCurrentVersion) >= new Version(MinimumRequiredVersion); return num && flag; } private string ErrorClient() { if (ReceivedMinimumRequiredVersion == null) { return DisplayName + " is not installed on the server."; } if (!(new Version(CurrentVersion) >= new Version(ReceivedMinimumRequiredVersion))) { return DisplayName + " needs to be at least version " + ReceivedMinimumRequiredVersion + ". You have version " + CurrentVersion + "."; } return DisplayName + " may not be higher than version " + ReceivedCurrentVersion + ". You have version " + CurrentVersion + "."; } private string ErrorServer(ZRpc rpc) { return "Disconnect: The client (" + rpc.GetSocket().GetHostName() + ") doesn't have the correct " + DisplayName + " version " + MinimumRequiredVersion; } private string Error(ZRpc? rpc = null) { if (rpc != null) { return ErrorServer(rpc); } return ErrorClient(); } private static VersionCheck[] GetFailedClient() { return versionChecks.Where((VersionCheck check) => !check.IsVersionOk()).ToArray(); } private static VersionCheck[] GetFailedServer(ZRpc rpc) { return versionChecks.Where((VersionCheck check) => check.ModRequired && !check.ValidatedClients.Contains(rpc)).ToArray(); } private static void Logout() { Game.instance.Logout(); AccessTools.DeclaredField(typeof(ZNet), "m_connectionStatus").SetValue(null, ZNet.ConnectionStatus.ErrorVersion); } private static void DisconnectClient(ZRpc rpc) { rpc.Invoke("Error", 3); } private static void CheckVersion(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, null); } private static void CheckVersion(ZRpc rpc, ZPackage pkg, Action? original) { string text = pkg.ReadString(); string text2 = pkg.ReadString(); string text3 = pkg.ReadString(); bool flag = false; foreach (VersionCheck versionCheck in versionChecks) { if (!(text != versionCheck.Name)) { Debug.Log("Received " + versionCheck.DisplayName + " version " + text3 + " and minimum version " + text2 + " from the " + (ZNet.instance.IsServer() ? "client" : "server") + "."); versionCheck.ReceivedMinimumRequiredVersion = text2; versionCheck.ReceivedCurrentVersion = text3; if (ZNet.instance.IsServer() && versionCheck.IsVersionOk()) { versionCheck.ValidatedClients.Add(rpc); } flag = true; } } if (flag) { return; } pkg.SetPos(0); if (original != null) { original(rpc, pkg); if (pkg.GetPos() == 0) { notProcessedNames.Add(text, text3); } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] [HarmonyPrefix] private static bool RPC_PeerInfo(ZRpc rpc, ZNet __instance) { VersionCheck[] array = (__instance.IsServer() ? GetFailedServer(rpc) : GetFailedClient()); if (array.Length == 0) { return true; } VersionCheck[] array2 = array; for (int i = 0; i < array2.Length; i++) { Debug.LogWarning(array2[i].Error(rpc)); } if (__instance.IsServer()) { DisconnectClient(rpc); } else { Logout(); } return false; } [HarmonyPatch(typeof(ZNet), "OnNewConnection")] [HarmonyPrefix] private static void RegisterAndCheckVersion(ZNetPeer peer, ZNet __instance) { notProcessedNames.Clear(); IDictionary dictionary = (IDictionary)typeof(ZRpc).GetField("m_functions", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(peer.m_rpc); if (dictionary.Contains("ServerSync VersionCheck".GetStableHashCode())) { object obj = dictionary["ServerSync VersionCheck".GetStableHashCode()]; Action action = (Action)obj.GetType().GetField("m_action", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj); peer.m_rpc.Register("ServerSync VersionCheck", delegate(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, action); }); } else { peer.m_rpc.Register("ServerSync VersionCheck", CheckVersion); } foreach (VersionCheck versionCheck in versionChecks) { versionCheck.Initialize(); if (versionCheck.ModRequired || __instance.IsServer()) { Debug.Log("Sending " + versionCheck.DisplayName + " version " + versionCheck.CurrentVersion + " and minimum version " + versionCheck.MinimumRequiredVersion + " to the " + (__instance.IsServer() ? "client" : "server") + "."); ZPackage zPackage = new ZPackage(); zPackage.Write(versionCheck.Name); zPackage.Write(versionCheck.MinimumRequiredVersion); zPackage.Write(versionCheck.CurrentVersion); peer.m_rpc.Invoke("ServerSync VersionCheck", zPackage); } } } [HarmonyPatch(typeof(ZNet), "Disconnect")] [HarmonyPrefix] private static void RemoveDisconnected(ZNetPeer peer, ZNet __instance) { if (!__instance.IsServer()) { return; } foreach (VersionCheck versionCheck in versionChecks) { versionCheck.ValidatedClients.Remove(peer.m_rpc); } } [HarmonyPatch(typeof(FejdStartup), "ShowConnectError")] [HarmonyPostfix] private static void ShowConnectionError(FejdStartup __instance) { if (!__instance.m_connectionFailedPanel.activeSelf || ZNet.GetConnectionStatus() != ZNet.ConnectionStatus.ErrorVersion) { return; } bool flag = false; VersionCheck[] failedClient = GetFailedClient(); if (failedClient.Length != 0) { string text = string.Join("\n", failedClient.Select((VersionCheck check) => check.Error())); TMP_Text connectionFailedError = __instance.m_connectionFailedError; connectionFailedError.text = connectionFailedError.text + "\n" + text; flag = true; } foreach (KeyValuePair item in notProcessedNames.OrderBy((KeyValuePair kv) => kv.Key)) { if (!__instance.m_connectionFailedError.text.Contains(item.Key)) { TMP_Text connectionFailedError2 = __instance.m_connectionFailedError; connectionFailedError2.text = connectionFailedError2.text + "\nServer expects you to have " + item.Key + " (Version: " + item.Value + ") installed."; flag = true; } } if (flag) { RectTransform component = __instance.m_connectionFailedPanel.transform.Find("Image").GetComponent(); Vector2 sizeDelta = component.sizeDelta; sizeDelta.x = 675f; component.sizeDelta = sizeDelta; __instance.m_connectionFailedError.ForceMeshUpdate(); float num = __instance.m_connectionFailedError.renderedHeight + 105f; RectTransform component2 = component.transform.Find("ButtonOk").GetComponent(); component2.anchoredPosition = new Vector2(component2.anchoredPosition.x, component2.anchoredPosition.y - (num - component.sizeDelta.y) / 2f); sizeDelta = component.sizeDelta; sizeDelta.y = num; component.sizeDelta = sizeDelta; } } } } // ---- 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 instance = NjordRuntimeConfig.Instance; if (instance == null) { return 0f; } return state switch { NjordSailState.Forward => instance.BaseForwardForce, NjordSailState.Half => instance.BaseForwardForce, NjordSailState.Full => instance.BaseForwardForce, _ => 0f, }; } public static float ApplyAcceleration(float baseline, float dt) { NjordRuntimeConfig instance = NjordRuntimeConfig.Instance; if (instance == null) { return 0f; } float num = baseline * instance.AccelerationMultiplier; if (float.IsNaN(num) || float.IsInfinity(num)) { return 0f; } return num; } public static float GetSailMult(NjordSailState state) { NjordRuntimeConfig instance = NjordRuntimeConfig.Instance; if (instance == null) { return 1f; } return state switch { NjordSailState.Forward => instance.SailForwardForce, NjordSailState.Half => instance.HalfSailForce, NjordSailState.Full => instance.FullSailForce, _ => 1f, }; } public static float GetReverseForce(float dt) { NjordRuntimeConfig instance = NjordRuntimeConfig.Instance; if (instance == null) { return 0f; } float num = instance.BaseReverseForce * instance.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); if ((Player.m_localPlayer?.GetPlayerID() ?? 0) == 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.2")] public class Plugin : BaseUnityPlugin { public const string ModVersion = "1.3.2"; 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; } public static bool OdinShipPlusDetected { 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"); OdinShipPlusDetected = Chainloader.PluginInfos.ContainsKey("marlthon.OdinShipPlus"); if (OdinShipDetected || OdinShipPlusDetected) { Log.LogInfo("Njord smiles upon the shipwrights: OdinShip" + (OdinShipPlusDetected ? "Plus" : "") + " detected! Taking the helm."); } else { Log.LogInfo("Njord watches the waves: OdinShip not detected. We sail on."); } NjordConfig.Bind(); 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)."); 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.F7)) { HudEnabledByUser.Value = !HudEnabledByUser.Value; } } private void OnGUI() { NjordHudManager.OnGUI(); } private void RegisterConsoleCommands() { 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; Color vFX_Color = NjordRuntimeConfig.Instance.VFX_Color; float num = 0.6f; Vector3 vector = transform.position - transform.forward * 1f + Vector3.up * 0.5f; for (int i = 0; i < 8; i++) { NjordRunePiggyback.SpawnAtWorldPosition(vector - transform.forward * ((float)i * num), 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.Instance; 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 && Physics.Raycast(vector + Vector3.up * 2f, Vector3.down, out var hitInfo, 10f, num5)) { vector2.y = hitInfo.point.y + 0.02f; } for (float num6 = Vector3.Dot(vector2 - _lastSpawnPos, _stern.forward); num6 >= _spacing; num6 = Vector3.Dot(vector2 - _lastSpawnPos, _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; } } } 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 instance = NjordRuntimeConfig.Instance; if (instance != null && instance.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 { } gameObject.AddComponent().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; gameObject2.AddComponent().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.Instance; _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) { SyncedConfigEntry 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) { SyncedConfigEntry 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) { SyncedConfigEntry 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 instance = NjordRuntimeConfig.Instance; if (instance != null && instance.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 instance2 = NjordRuntimeConfig.Instance; if (instance2 != null && instance2.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 instance3 = NjordRuntimeConfig.Instance; float num = Mathf.Max(0.25f, instance3.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 { if (!NjordRuntimeConfig.Instance.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; gameObject.AddComponent().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; gameObject2.AddComponent().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) { if (!NjordRuntimeConfig.Instance.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) { if (!NjordRuntimeConfig.Instance.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 || !NjordRuntimeConfig.Instance.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 instance = NjordRuntimeConfig.Instance; Texture2D texture2D = "blend".ToLowerInvariant() switch { "soft" => NjordRuneTextureGenerator.GenerateSoftTrail(instance.VFX_Color), "rune" => NjordRuneTextureGenerator.GenerateRune(instance.VFX_Color), "streak" => NjordRuneTextureGenerator.GenerateStreak(instance.VFX_Color), _ => NjordRuneTextureGenerator.GenerateBlend(instance.VFX_Color), }; if (texture2D == null) { NjordDebug.Error("[VFX] Rune texture generation failed."); return; } _runeTex = texture2D; _material = NjordRuneVFX_Helper.GetOrCreateRuneMaterial(texture2D, instance.VFX_Color); if (_material == null) { NjordDebug.Error("[VFX] Failed to create rune material (no suitable shader)."); return; } if (_material.HasProperty("_EmissionColor")) { Color vFX_Color = instance.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 instance = NjordRuntimeConfig.Instance; Spawn(pos, instance.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) { if (!NjordRuntimeConfig.Instance.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.Instance; float num = 15f; foreach (object item in new List(NjordRuneVFX._emitters.Keys)) { 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; NjordRuneVFX.SpawnBurst(GetStern(ship).position + Vector3.up * 1.2f, 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; Vector3 vector = GetStern(ship).position + Vector3.up * 1.5f; for (int i = 0; i < 4; i++) { Vector3 insideUnitSphere = Random.insideUnitSphere; insideUnitSphere.y = Mathf.Abs(insideUnitSphere.y) + 0.3f; NjordRuneVFX.SpawnBurst(vector + insideUnitSphere * 0.2f, cfg.VFX_Color, 1); } } } float num = 0f; try { num = ship.GetSpeed(); } catch { num = 0f; } Transform stern = 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 {stern?.name} rateMul={num3:F2}"); LastTrailLog[ship] = now; } } NjordRuneVFX.StartTrailForShip(ship, stern, 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(); if (Njord_ShipControlTracker.GetControllerForShip(ship) != 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 instance = NjordRuntimeConfig.Instance; float shipWindAngle = Njord_ShipControlTracker.GetShipWindAngle(ship); if (instance.Wind_SystemEnable) { num2 = Mathf.DeltaAngle(0f, ship.GetWindAngle()); num3 = num2; if (instance.Wind_AlwaysFull) { num3 = 0f; } else { bool flag3 = Mathf.Abs(num2) > 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 num7 = num2; if (flag3 && instance.Wind_NoDeadZone) { FieldInfo fieldInfo2 = AccessTools.Field(type, "m_rudder") ?? AccessTools.Field(type, "m_rudderValue"); float num8 = ((fieldInfo2 != null) ? ((float)fieldInfo2.GetValue(__instance)) : 0f); float num9 = 115f; num7 = ((num8 > 0.05f) ? (0f - num9) : ((!(num8 < -0.05f)) ? ((num2 > 0f) ? num9 : (0f - num9)) : num9)); } else if (!flag3 && instance.Wind_BlendToFull) { num7 = 0f; } num3 = ((!(shipGraceTimer < instance.Wind_GracePeriodDelay)) ? num7 : num2); } num5 = Mathf.MoveTowardsAngle(shipWindAngle, num3, instance.Wind_BlendRate * fixedDeltaTime); Njord_ShipControlTracker.SetShipWindAngle(ship, num5); Njord_Telemetry.WindUIBlend = num5; float num10 = Mathf.Abs(num5); num4 = ((num10 > 130f) ? 0f : ((!(num10 > 90f)) ? Mathf.Lerp(1f, 0.75f, num10 / 90f) : Mathf.Lerp(0.75f, 0f, (num10 - 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") { sailMult = ((!NjordRuntimeConfig.Instance.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 instance2 = NjordRuntimeConfig.Instance; if (instance2.Debug_Enable && instance2.Debug_PhysicsWind && (!_lastDebugLog.TryGetValue(key, out var value4) || Time.time - value4 > 1f)) { _lastDebugLog[key] = Time.time; string text2 = (instance2.Wind_SystemEnable ? "ON " : "OFF"); NjordDebug.Info($"[Physics] {shipPrefabName} | mode={text} | baseline={v:F1} | accel={v2:F1} | mult={sailMult:F2} | fwForce={value2:F1}"); if (instance2.Wind_SystemEnable) { NjordDebug.Info($"[Wind] Sys:{text2} | AlwaysFull:{instance2.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 (__instance is Ship ship3) { try { NjordSeaDogEngineVFXTriggers.Process(ship3, njordSailState, text, controllerForShip, Time.time, transform, value, NjordRuntimeConfig.Instance); } catch (Exception arg) { NjordDebug.Error($"[VFX] Exception in NjordSeaDogEngineVFXTriggers.Process: {arg}"); } } if (!flag2 || !(__instance is Ship ship4)) { return; } if (value && flag) { NjordDebug.Debug($"[SeaDog] Controlled ship tick: speedState={text}, vel={ship4.GetSpeed():0.0}"); } _telemetryTimer += fixedDeltaTime; if (!(_telemetryTimer >= 0.2f)) { return; } _telemetryTimer = 0f; Vector3 position = ship4.transform.position; WaterVolume previousAndOut = null; float waterLevel = Floating.GetWaterLevel(position, ref previousAndOut); float num11 = position.y - waterLevel - ship4.m_waterLevelOffset; bool floating = num11 <= ship4.m_disableLevel; WearNTear component = ship4.GetComponent(); if (component != null) { ZNetView zNetView2 = AccessTools.Field(typeof(WearNTear), "m_nview")?.GetValue(component) as ZNetView; if (zNetView2 != null && zNetView2.IsValid()) { float num12 = Mathf.Max(1f, Safe(component.m_health)); float v3 = zNetView2.GetZDO().GetFloat(ZDOVars.s_health, num12); v3 = Mathf.Max(0f, Safe(v3)); Njord_Telemetry.SetHP(v3, num12); } else { Njord_Telemetry.SetHP(0f, 1f); } } else { Njord_Telemetry.SetHP(0f, 1f); } float num13 = Safe(ship4.GetSpeed()); float num14 = Safe(v); Njord_Telemetry.UpdateBasic(ship4, waterLevel, num11, floating, Njord_ShipControlTracker.ReadyTimer, Njord_ShipControlTracker.SteeringGateReady); Njord_Telemetry.UpdateSpeed(_prevNjordSpeed, num13, _prevVanillaSpeed, num14); _prevNjordSpeed = num13; _prevVanillaSpeed = num14; } } } // ---- 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 instance = NjordRuntimeConfig.Instance; if (instance == null || !instance.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; bool flag = Mathf.Abs(num) > 130f; if (instance.Wind_AlwaysFull) { num2 = 0f; } else { float num3 = num; if (flag && instance.Wind_NoDeadZone) { FieldInfo fieldInfo2 = AccessTools.Field(typeof(Ship), "m_rudder") ?? AccessTools.Field(typeof(Ship), "m_rudderValue"); float num4 = ((fieldInfo2 != null) ? ((float)fieldInfo2.GetValue(controlledShip)) : 0f); float num5 = 115f; num3 = ((num4 > 0.05f) ? (0f - num5) : ((!(num4 < -0.05f)) ? ((num > 0f) ? num5 : (0f - num5)) : num5)); } else if (!flag && instance.Wind_BlendToFull) { num3 = 0f; } num2 = ((!(Njord_ShipControlTracker.GetShipGraceTimer(controlledShip) < instance.Wind_GracePeriodDelay)) ? num3 : num); } float num6 = Mathf.Abs(shipWindAngle); Color white = Color.white; if (Mathf.Abs(Mathf.DeltaAngle(shipWindAngle, num2)) > 2f && (instance.Wind_AlwaysFull || instance.Wind_NoDeadZone || instance.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 (num6 <= 45f) { white = Color.green; } else if (num6 <= 90f) { float num7 = (num6 - 45f) / 45f; white = Color.Lerp(new Color(0.3f, 0.6f, 1f, 1f), Color.green, 1f - num7); } else { float num8 = (num6 - 90f) / 40f; white = Color.Lerp(Color.white, new Color(0.3f, 0.6f, 1f, 1f), 1f - num8); } 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 Njord Authority initializing AFTER player spawn…"); NjordAuthorityEngine.Initialize(); NjordDebug.Info("\ud83c\udf0a Njord 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)) { _ = 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 = NjordRuntimeConfig.Instance.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); GUILayout.Label("Status: " + (Njord_Telemetry.IsFloating ? "Floating ✓" : "Not Floating ✗") + " Steering Gate: " + (Njord_Telemetry.GateReady ? "Open ✓" : "Closed ✗"), _smallLabelStyle); GUILayout.Space(4f); GUILayout.Label("── Waterline ─────────────────────────────", _sectionStyle); string text = (Njord_Telemetry.WaterError ? "—" : Njord_Telemetry.WaterLevel.ToString("F2")); string text2 = (Njord_Telemetry.WaterError ? "—" : Njord_Telemetry.BoatY.ToString("F2")); string arg = (Njord_Telemetry.DepthError ? "—" : Njord_Telemetry.Depth.ToString("F3")); GUILayout.Label("Water Level: " + text, _labelStyle); GUILayout.Label("Boat Y: " + text2, _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); GUI.Box(new Rect(position.x, position.y, position.width * num2, position.height), GUIContent.none, _hpFillStyle); GUILayout.Space(22f); GUILayout.Label("── Steering ─────────────────────────────", _sectionStyle); string text3 = (Njord_Telemetry.RudderError ? "—" : Njord_Telemetry.RudderVanilla.ToString("F2")); string text4 = (Njord_Telemetry.RudderError ? "—" : Njord_Telemetry.RudderNjord.ToString("F2")); GUILayout.BeginHorizontal(); GUILayout.Label("Vanilla: " + text3, _labelStyle, GUILayout.Width(150f)); GUILayout.Label("Njord: " + text4, _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 text5 = (Njord_Telemetry.SpeedError ? "—" : Njord_Telemetry.NjordSpeedCurr.ToString("F1")); string text6 = (Njord_Telemetry.SpeedError ? "—" : Njord_Telemetry.MortalSpeedCurr.ToString("F1")); string text7 = (Njord_Telemetry.SpeedError ? "—" : (Njord_Telemetry.NjordSpeedCurr - Njord_Telemetry.MortalSpeedCurr).ToString("0.0")); GUILayout.BeginHorizontal(); GUILayout.Label("Njord: " + text5 + " m/s", _labelStyle, GUILayout.Width(150f)); GUILayout.Label("Mortal: " + text6 + " m/s", _labelStyle); GUILayout.EndHorizontal(); GUILayout.Label("Gift of the Deep: " + text7 + " 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 y = (float)Screen.height - 220f; GUI.Label(new Rect(40f, 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++) { 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 { private static ConfigSync configSync = new ConfigSync("wubarrk.njord") { DisplayName = "Njord", CurrentVersion = "1.3.2", MinimumRequiredVersion = "1.3.2" }; public static SyncedConfigEntry ServerConfigLocked = null; public static SyncedConfigEntry Wind_SystemEnable = null; public static SyncedConfigEntry Wind_AlwaysFull = null; public static SyncedConfigEntry Wind_NoDeadZone = null; public static SyncedConfigEntry Wind_BlendToFull = null; public static SyncedConfigEntry Wind_BlendRate = null; public static SyncedConfigEntry Wind_GracePeriodDelay = null; public static SyncedConfigEntry SteeringMultiplier = null; public static SyncedConfigEntry AccelerationMultiplier = null; public static SyncedConfigEntry BaseForwardForce = null; public static SyncedConfigEntry BaseReverseForce = null; public static SyncedConfigEntry SailForwardForce = null; public static SyncedConfigEntry HalfSailForce = null; public static SyncedConfigEntry FullSailForce = null; public static SyncedConfigEntry ReverseKick = null; public static SyncedConfigEntry MaxSpeed_Raft = null; public static SyncedConfigEntry MaxSpeed_Karve = null; public static SyncedConfigEntry MaxSpeed_Longship = null; public static SyncedConfigEntry MaxSpeed_Drakkar = null; public static SyncedConfigEntry MaxSpeed_MercantShip = null; public static SyncedConfigEntry MaxSpeed_CargoShip = null; public static SyncedConfigEntry MaxSpeed_BigCargoShip = null; public static SyncedConfigEntry MaxSpeed_RowingCanoe = null; public static SyncedConfigEntry MaxSpeed_DoubleRowingCanoe = null; public static SyncedConfigEntry MaxSpeed_LittleBoat = null; public static SyncedConfigEntry MaxSpeed_WarShip = null; public static SyncedConfigEntry MaxSpeed_CargoCaravel = null; public static SyncedConfigEntry MaxSpeed_HugeCargoShip = null; public static SyncedConfigEntry MaxSpeed_CargoAnimalShip = null; public static SyncedConfigEntry MaxSpeed_HerculeShip = null; public static SyncedConfigEntry MaxSpeed_Skuldelev = null; public static SyncedConfigEntry MaxSpeed_TaurusWarShip = null; public static SyncedConfigEntry MaxSpeed_FastShipSkuldelev = null; public static SyncedConfigEntry MaxSpeed_GoblinShip = null; public static SyncedConfigEntry VFX_Enable = null; public static SyncedConfigEntry VFX_Color = null; public static SyncedConfigEntry Debug_Enable = null; public static SyncedConfigEntry Debug_PhysicsWind = null; public static SyncedConfigEntry Debug_AuthControl = null; private static SyncedConfigEntry config(string group, string name, T value, ConfigDescription description, bool synchronizedSetting = true) { ConfigEntry configEntry = Plugin.Instance.Config.Bind(group, name, value, description); SyncedConfigEntry syncedConfigEntry = configSync.AddConfigEntry(configEntry); syncedConfigEntry.SynchronizedConfig = synchronizedSetting; return syncedConfigEntry; } private static SyncedConfigEntry config(string group, string name, T value, string description, bool synchronizedSetting = true) { return config(group, name, value, new ConfigDescription(description, null), synchronizedSetting); } public static void Bind() { ServerConfigLocked = config("1 - General", "Lock Configuration", value: true, "If on, the configuration is locked and can be changed by server admins only."); configSync.AddLockingConfigEntry(ServerConfigLocked.SourceConfig); string text = "\nNote: This value is safely clamped at runtime to protect against NaN, Infinity, and out-of-bounds inputs breaking physics."; Wind_SystemEnable = config("2 - Wind System", "Wind_SystemEnable", value: true, "Master switch for Njord wind physics and UI."); Wind_AlwaysFull = config("2 - Wind System", "Wind_AlwaysFull", value: true, "Always gives perfect tailwind, ignoring all other rules."); Wind_NoDeadZone = config("2 - Wind System", "Wind_NoDeadZone", value: false, "[EXPERIMENTAL] Enables nudging the wind just out of the dead zone (to the 130° edge)."); Wind_BlendToFull = config("2 - Wind System", "Wind_BlendToFull", value: false, "[EXPERIMENTAL] Enables nudging the wind from the crosswind line to the perfect tailwind line."); Wind_BlendRate = config("2 - Wind System", "Wind_BlendRate", 30f, new ConfigDescription("How fast the wind adjusts (degrees per second). [Range: 0f - 180f]." + text, new AcceptableValueRange(0f, 180f))); Wind_GracePeriodDelay = config("2 - Wind System", "Wind_GracePeriodDelay", 2.5f, new ConfigDescription("Delay in seconds before the wind system starts assisting. [Range: 0f - 60f]." + text, new AcceptableValueRange(0f, 60f))); SteeringMultiplier = config("3 - Steering", "SteeringMultiplier", 1.85f, new ConfigDescription("Multiplier applied to rudder steering. [Range: 0.1f - 10f]." + text, new AcceptableValueRange(0.1f, 10f))); AccelerationMultiplier = config("4 - Acceleration", "AccelerationMultiplier", 2.2f, new ConfigDescription("Acceleration multiplier. [Range: 0.1f - 10f]." + text, new AcceptableValueRange(0.1f, 10f))); BaseForwardForce = config("4 - Acceleration", "BaseForwardForce", 0.85f, new ConfigDescription("Base forward force. [Range: 0f - 10f]." + text, new AcceptableValueRange(0f, 10f))); BaseReverseForce = config("4 - Acceleration", "BaseReverseForce", 0.2f, new ConfigDescription("Base reverse force. [Range: 0f - 10f]." + text, new AcceptableValueRange(0f, 10f))); SailForwardForce = config("5 - Sails", "SailForwardForce", 0.3f, new ConfigDescription("Forward force when sails are down. [Range: 0f - 10f]." + text, new AcceptableValueRange(0f, 10f))); HalfSailForce = config("5 - Sails", "HalfSailForce", 1.2f, new ConfigDescription("Forward force at half sail. [Range: 0f - 10f]." + text, new AcceptableValueRange(0f, 10f))); FullSailForce = config("5 - Sails", "FullSailForce", 4.2f, new ConfigDescription("Forward force at full sail. [Range: 0f - 20f]." + text, new AcceptableValueRange(0f, 20f))); ReverseKick = config("5 - Sails", "ReverseKick", 1.8f, new ConfigDescription("Reverse kick multiplier. [Range: 0f - 10f]." + text, new AcceptableValueRange(0f, 10f))); MaxSpeed_Raft = config("6 - Speed Limits", "MaxSpeed_Raft", 7f, new ConfigDescription("Absolute max speed for the Raft (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_Karve = config("6 - Speed Limits", "MaxSpeed_Karve", 16.8f, new ConfigDescription("Absolute max speed for the Karve (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_Longship = config("6 - Speed Limits", "MaxSpeed_Longship", 26f, new ConfigDescription("Absolute max speed for the Longship (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_Drakkar = config("6 - Speed Limits", "MaxSpeed_Drakkar", 30f, new ConfigDescription("Absolute max speed for the Drakkar (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); if (Plugin.OdinShipDetected || Plugin.OdinShipPlusDetected) { MaxSpeed_MercantShip = config("7 - OdinShip Speed Limits", "MaxSpeed_MercantShip", 22f, new ConfigDescription("Max speed for MercantShip (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_CargoShip = config("7 - OdinShip Speed Limits", "MaxSpeed_CargoShip", 20f, new ConfigDescription("Max speed for CargoShip (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_BigCargoShip = config("7 - OdinShip Speed Limits", "MaxSpeed_BigCargoShip", 23.5f, new ConfigDescription("Max speed for BigCargoShip (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_RowingCanoe = config("7 - OdinShip Speed Limits", "MaxSpeed_RowingCanoe", 10f, new ConfigDescription("Max speed for RowingCanoe (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_DoubleRowingCanoe = config("7 - OdinShip Speed Limits", "MaxSpeed_DoubleRowingCanoe", 12f, new ConfigDescription("Max speed for DoubleRowingCanoe (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_LittleBoat = config("7 - OdinShip Speed Limits", "MaxSpeed_LittleBoat", 17f, new ConfigDescription("Max speed for LittleBoat (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_WarShip = config("7 - OdinShip Speed Limits", "MaxSpeed_WarShip", 32f, new ConfigDescription("Max speed for WarShip (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); } if (Plugin.OdinShipPlusDetected) { MaxSpeed_CargoCaravel = config("8 - OdinShipPlus Speed Limits", "MaxSpeed_CargoCaravel", 22f, new ConfigDescription("Max speed for CargoCaravel (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_HugeCargoShip = config("8 - OdinShipPlus Speed Limits", "MaxSpeed_HugeCargoShip", 18f, new ConfigDescription("Max speed for HugeCargoShip (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_CargoAnimalShip = config("8 - OdinShipPlus Speed Limits", "MaxSpeed_CargoAnimalShip", 19f, new ConfigDescription("Max speed for CargoAnimalShip (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_HerculeShip = config("8 - OdinShipPlus Speed Limits", "MaxSpeed_HerculeShip", 24f, new ConfigDescription("Max speed for HerculeShip (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_Skuldelev = config("8 - OdinShipPlus Speed Limits", "MaxSpeed_Skuldelev", 28f, new ConfigDescription("Max speed for Skuldelev (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_TaurusWarShip = config("8 - OdinShipPlus Speed Limits", "MaxSpeed_TaurusWarShip", 25f, new ConfigDescription("Max speed for TaurusWarShip (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_FastShipSkuldelev = config("8 - OdinShipPlus Speed Limits", "MaxSpeed_FastShipSkuldelev", 35f, new ConfigDescription("Max speed for FastShipSkuldelev (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); MaxSpeed_GoblinShip = config("8 - OdinShipPlus Speed Limits", "MaxSpeed_GoblinShip", 18f, new ConfigDescription("Max speed for GoblinShip (m/s). [Range: 1f - 100f]." + text, new AcceptableValueRange(1f, 100f))); } VFX_Enable = config("9 - VFX", "VFX_Enable", value: true, "Enable rune VFX."); VFX_Color = config("9 - VFX", "VFX_Color", new Color(0.3f, 0.6f, 1f, 1f), "Rune VFX color."); Debug_Enable = config("10 - Debug", "Debug_Enable", value: false, "Master switch to enable Njord debug logs.", synchronizedSetting: false); Debug_PhysicsWind = config("10 - Debug", "Debug_PhysicsWind", value: false, "Enable logging for Wind calculations and Physics.", synchronizedSetting: false); Debug_AuthControl = config("10 - Debug", "Debug_AuthControl", value: false, "Enable logging for ship ownership and control.", synchronizedSetting: false); } } } // ---- Njord/plugins/Njord.dll :: Njord.Core.NjordRuntimeConfig ---- namespace Njord.Core { public class NjordRuntimeConfig { public static NjordRuntimeConfig Instance { get; } = new NjordRuntimeConfig(); public float SteeringMultiplier => NjordConfig.SteeringMultiplier.Value; public float AccelerationMultiplier => NjordConfig.AccelerationMultiplier.Value; public float BaseForwardForce => NjordConfig.BaseForwardForce.Value; public float BaseReverseForce => NjordConfig.BaseReverseForce.Value; public float SailForwardForce => NjordConfig.SailForwardForce.Value; public float HalfSailForce => NjordConfig.HalfSailForce.Value; public float FullSailForce => NjordConfig.FullSailForce.Value; public float ReverseKick => NjordConfig.ReverseKick.Value; public bool Wind_SystemEnable => NjordConfig.Wind_SystemEnable.Value; public bool Wind_AlwaysFull => NjordConfig.Wind_AlwaysFull.Value; public bool Wind_NoDeadZone => NjordConfig.Wind_NoDeadZone.Value; public bool Wind_BlendToFull => NjordConfig.Wind_BlendToFull.Value; public float Wind_BlendRate => NjordConfig.Wind_BlendRate.Value; public float Wind_GracePeriodDelay => NjordConfig.Wind_GracePeriodDelay.Value; public bool VFX_Enable => NjordConfig.VFX_Enable.Value; public Color VFX_Color => NjordConfig.VFX_Color.Value; public bool Debug_Enable => NjordConfig.Debug_Enable.Value; public bool Debug_PhysicsWind => NjordConfig.Debug_PhysicsWind.Value; public bool Debug_AuthControl => NjordConfig.Debug_AuthControl.Value; public float MaxSpeed_Raft => NjordConfig.MaxSpeed_Raft.Value; public float MaxSpeed_Karve => NjordConfig.MaxSpeed_Karve.Value; public float MaxSpeed_Longship => NjordConfig.MaxSpeed_Longship.Value; public float MaxSpeed_Drakkar => NjordConfig.MaxSpeed_Drakkar.Value; } } // ---- 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 || Plugin.OdinShipPlusDetected) { 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 (Plugin.OdinShipPlusDetected) { if (text.Contains("cargocaravel")) { num = NjordConfig.MaxSpeed_CargoCaravel.Value; } else if (text.Contains("hugecargoship")) { num = NjordConfig.MaxSpeed_HugeCargoShip.Value; } else if (text.Contains("cargoanimalship")) { num = NjordConfig.MaxSpeed_CargoAnimalShip.Value; } else if (text.Contains("herculeship")) { num = NjordConfig.MaxSpeed_HerculeShip.Value; } else if (text.Contains("fastshipskuldelev")) { num = NjordConfig.MaxSpeed_FastShipSkuldelev.Value; } else if (text.Contains("skuldelev")) { num = NjordConfig.MaxSpeed_Skuldelev.Value; } else if (text.Contains("tauruswarship")) { num = NjordConfig.MaxSpeed_TaurusWarShip.Value; } else if (text.Contains("goblinship")) { num = NjordConfig.MaxSpeed_GoblinShip.Value; } } if (num <= 0f || float.IsNaN(num) || float.IsInfinity(num)) { return 0f; } return num; } } } // ---- 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 runeIndex = Mathf.Abs(color.GetHashCode()) % 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++) { float num2 = Vector2.Distance(new Vector2((float)j + 0.5f, (float)i + 0.5f), 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); } } } } } } }