Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of DvergrAstrolabe v0.4.2
plugins/DvergrAstrolabe.dll
Decompiled 2 days ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.IO.Compression; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using JetBrains.Annotations; using Microsoft.CodeAnalysis; using ServerSync; using TMPro; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("DvergrAstrolabe")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.4.1.0")] [assembly: AssemblyInformationalVersion("0.4.1+838f259e25400133a05e15faaf400ea349a3120d")] [assembly: AssemblyProduct("DvergrAstrolabe")] [assembly: AssemblyTitle("DvergrAstrolabe")] [assembly: AssemblyVersion("0.4.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ServerSync { [PublicAPI] public abstract class OwnConfigEntryBase { public object? LocalBaseValue; public bool SynchronizedConfig = true; public abstract ConfigEntryBase BaseConfig { get; } } [PublicAPI] public class SyncedConfigEntry<T>(ConfigEntry<T> sourceConfig) : OwnConfigEntryBase() { public readonly ConfigEntry<T> SourceConfig = sourceConfig; public override ConfigEntryBase BaseConfig => (ConfigEntryBase)(object)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; } } } 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; }; } } [PublicAPI] public sealed class CustomSyncedValue<T> : 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; } } } internal class ConfigurationManagerAttributes { [UsedImplicitly] public bool? ReadOnly = false; } [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<ZPackage>(configSync2.Name + " ConfigSync", (Action<long, ZPackage>)configSync2.RPC_FromOtherClientConfigSync); if (isServer) { configSync2.InitialSyncDone = true; Debug.Log((object)("Registered '" + configSync2.Name + " ConfigSync' RPC - waiting for incoming connections")); } } if (isServer) { ((MonoBehaviour)__instance).StartCoroutine(WatchAdminListChanges()); } static void SendAdmin(List<ZNetPeer> 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) { ((MonoBehaviour)ZNet.instance).StartCoroutine(configSync.sendZPackage(peers, package)); } } static IEnumerator WatchAdminListChanges() { MethodInfo listContainsId = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList adminList = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); List<string> CurrentList = new List<string>(adminList.GetList()); while (true) { yield return (object)new WaitForSeconds(30f); if (!adminList.GetList().SequenceEqual(CurrentList)) { CurrentList = new List<string>(adminList.GetList()); List<ZNetPeer> 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<ZPackage>(configSync.Name + " ConfigSync", (Action<ZRpc, ZPackage>)configSync.RPC_FromServerConfigSync); } } } private class ParsedConfigs { public readonly Dictionary<OwnConfigEntryBase, object?> configValues = new Dictionary<OwnConfigEntryBase, object>(); public readonly Dictionary<CustomSyncedValueBase, object?> customValues = new Dictionary<CustomSyncedValueBase, object>(); } [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 : ZPlayFabSocket, ISocket { public volatile bool finished; public volatile int versionMatchQueued = -1; public readonly List<ZPackage> Package = new List<ZPackage>(); public readonly ISocket Original; public BufferingSocket(ISocket original) { Original = original; ((ZPlayFabSocket)this)..ctor(); } public bool IsConnected() { return Original.IsConnected(); } public ZPackage Recv() { return Original.Recv(); } public int GetSendQueueSize() { return Original.GetSendQueueSize(); } public int GetCurrentSendRate() { return Original.GetCurrentSendRate(); } public bool IsHost() { return Original.IsHost(); } public void Dispose() { Original.Dispose(); } public bool GotNewData() { return Original.GotNewData(); } public void Close() { Original.Close(); } public string GetEndPointString() { return Original.GetEndPointString(); } public void GetAndResetStats(out int totalSent, out int totalRecv) { Original.GetAndResetStats(ref totalSent, ref totalRecv); } public void GetConnectionQuality(out float localQuality, out float remoteQuality, out int ping, out float outByteSec, out float inByteSec) { Original.GetConnectionQuality(ref localQuality, ref remoteQuality, ref ping, ref outByteSec, ref inByteSec); } public ISocket Accept() { return Original.Accept(); } public int GetHostPort() { return Original.GetHostPort(); } public bool Flush() { return Original.Flush(); } public string GetHostName() { return Original.GetHostName(); } public void VersionMatch() { if (finished) { Original.VersionMatch(); } else { versionMatchQueued = Package.Count; } } public void Send(ZPackage pkg) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown int pos = pkg.GetPos(); pkg.SetPos(0); int num = pkg.ReadInt(); if ((num == StringExtensionMethods.GetStableHashCode("PeerInfo") || num == StringExtensionMethods.GetStableHashCode("RoutedRPC") || num == StringExtensionMethods.GetStableHashCode("ZDOData")) && !finished) { ZPackage val = new ZPackage(pkg.GetArray()); val.SetPos(pos); Package.Add(val); } else { pkg.SetPos(pos); Original.Send(pkg); } } } [HarmonyPriority(800)] [HarmonyPrefix] private static void Prefix(ref Dictionary<Assembly, BufferingSocket>? __state, ZNet __instance, ZRpc rpc) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) if (!__instance.IsServer()) { return; } BufferingSocket bufferingSocket = new BufferingSocket(rpc.GetSocket()); AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket); object? obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (val != null && (int)ZNet.m_onlineBackend != 0) { FieldInfo fieldInfo = AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket"); object? value = fieldInfo.GetValue(val); ZPlayFabSocket val2 = (ZPlayFabSocket)((value is ZPlayFabSocket) ? value : null); if (val2 != null) { typeof(ZPlayFabSocket).GetField("m_remotePlayerId").SetValue(bufferingSocket, val2.m_remotePlayerId); } fieldInfo.SetValue(val, bufferingSocket); } if (__state == null) { __state = new Dictionary<Assembly, BufferingSocket>(); } __state[Assembly.GetExecutingAssembly()] = bufferingSocket; } [HarmonyPostfix] private static void Postfix(Dictionary<Assembly, BufferingSocket> __state, ZNet __instance, ZRpc rpc) { ZNetPeer peer; if (__instance.IsServer()) { object obj = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); peer = (ZNetPeer)((obj is ZNetPeer) ? obj : null); if (peer == null) { SendBufferedData(); } else { ((MonoBehaviour)__instance).StartCoroutine(sendAsync()); } } void SendBufferedData() { if (rpc.GetSocket() is BufferingSocket bufferingSocket) { AccessTools.DeclaredField(typeof(ZRpc), "m_socket").SetValue(rpc, bufferingSocket.Original); object? obj2 = AccessTools.DeclaredMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(ZRpc) }, (Type[])null).Invoke(__instance, new object[1] { rpc }); ZNetPeer val = (ZNetPeer)((obj2 is ZNetPeer) ? obj2 : null); if (val != null) { AccessTools.DeclaredField(typeof(ZNetPeer), "m_socket").SetValue(val, 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(); } } IEnumerator sendAsync() { foreach (ConfigSync configSync in configSyncs) { List<PackageEntry> list = new List<PackageEntry>(); 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", (Type[])null, (Type[])null); SyncedList val = (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)val.Contains(rpc.GetSocket().GetHostName())) : methodInfo.Invoke(ZNet.instance, new object[2] { val, rpc.GetSocket().GetHostName() })) }); ZPackage package = ConfigsToPackage(configSync.allConfigs.Select((OwnConfigEntryBase c) => c.BaseConfig), configSync.allCustomValues, list, partial: false); yield return ((MonoBehaviour)__instance).StartCoroutine(configSync.sendZPackage(new List<ZNetPeer> { peer }, package)); } SendBufferedData(); } } } 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((object)$"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<ConfigSync> configSyncs; private readonly HashSet<OwnConfigEntryBase> allConfigs = new HashSet<OwnConfigEntryBase>(); private HashSet<CustomSyncedValueBase> allCustomValues = new HashSet<CustomSyncedValueBase>(); 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<string, SortedDictionary<int, byte[]>> configValueCache = new Dictionary<string, SortedDictionary<int, byte[]>>(); private readonly List<KeyValuePair<long, string>> cacheExpirations = new List<KeyValuePair<long, string>>(); 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<bool>? SourceOfTruthChanged; private event Action? lockedConfigChanged; static ConfigSync() { ProcessingServerUpdate = false; configSyncs = new HashSet<ConfigSync>(); lockExempt = false; packageCounter = 0L; RuntimeHelpers.RunClassConstructor(typeof(VersionCheck).TypeHandle); } public ConfigSync(string name) { Name = name; configSyncs.Add(this); new VersionCheck(this); } public SyncedConfigEntry<T> AddConfigEntry<T>(ConfigEntry<T> configEntry) { OwnConfigEntryBase ownConfigEntryBase = configData((ConfigEntryBase)(object)configEntry); SyncedConfigEntry<T> syncedEntry = ownConfigEntryBase as SyncedConfigEntry<T>; if (syncedEntry == null) { syncedEntry = new SyncedConfigEntry<T>(configEntry); AccessTools.DeclaredField(typeof(ConfigDescription), "<Tags>k__BackingField").SetValue(((ConfigEntryBase)configEntry).Description, new object[1] { new ConfigurationManagerAttributes() }.Concat(((ConfigEntryBase)configEntry).Description.Tags ?? Array.Empty<object>()).Concat(new SyncedConfigEntry<T>[1] { syncedEntry }).ToArray()); configEntry.SettingChanged += delegate { if (!ProcessingServerUpdate && syncedEntry.SynchronizedConfig) { Broadcast(0L, (ConfigEntryBase)configEntry); } }; allConfigs.Add(syncedEntry); } return syncedEntry; } public SyncedConfigEntry<T> AddLockingConfigEntry<T>(ConfigEntry<T> lockingConfig) where T : IConvertible { if (lockedConfig != null) { throw new Exception("Cannot initialize locking ConfigEntry twice"); } lockedConfig = AddConfigEntry<T>(lockingConfig); lockingConfig.SettingChanged += delegate { this.lockedConfigChanged?.Invoke(); }; return (SyncedConfigEntry<T>)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<CustomSyncedValueBase>(allCustomValues.OrderByDescending((CustomSyncedValueBase v) => v.Priority)); customValue.ValueChanged += delegate { if (!ProcessingServerUpdate) { Broadcast(0L, 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) { //IL_0209: Unknown result type (might be due to invalid IL or missing references) //IL_0210: Expected O, but got Unknown //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Expected O, but got Unknown //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Expected O, but got Unknown try { if (isServer && IsLocked) { ZRpc? currentRpc = SnatchCurrentlyHandlingRPC.currentRpc; object obj; if (currentRpc == null) { obj = null; } else { ISocket socket = currentRpc.GetSocket(); obj = ((socket != null) ? socket.GetHostName() : null); } string text = (string)obj; if (text != null) { MethodInfo methodInfo = AccessTools.DeclaredMethod(typeof(ZNet), "ListContainsId", (Type[])null, (Type[])null); SyncedList val = (SyncedList)AccessTools.DeclaredField(typeof(ZNet), "m_adminList").GetValue(ZNet.instance); if (!(((object)methodInfo == null) ? val.Contains(text) : ((bool)methodInfo.Invoke(ZNet.instance, new object[2] { val, text })))) { return false; } } } cacheExpirations.RemoveAll(delegate(KeyValuePair<long, string> 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 SortedDictionary<int, byte[]> value)) { value = new SortedDictionary<int, byte[]>(); configValueCache[text2] = value; cacheExpirations.Add(new KeyValuePair<long, string>(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 val2 = null; bool saveOnConfigSet = false; foreach (KeyValuePair<OwnConfigEntryBase, object> configValue in parsedConfigs.configValues) { if (!isServer && configValue.Key.LocalBaseValue == null) { configValue.Key.LocalBaseValue = configValue.Key.BaseConfig.BoxedValue; } if (val2 == null) { val2 = configValue.Key.BaseConfig.ConfigFile; saveOnConfigSet = val2.SaveOnConfigSet; val2.SaveOnConfigSet = false; } configValue.Key.BaseConfig.BoxedValue = configValue.Value; } if (val2 != null) { val2.SaveOnConfigSet = saveOnConfigSet; val2.Save(); } foreach (KeyValuePair<CustomSyncedValueBase, object> 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((object)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<string, OwnConfigEntryBase> dictionary = allConfigs.Where((OwnConfigEntryBase c) => c.SynchronizedConfig).ToDictionary((OwnConfigEntryBase c) => c.BaseConfig.Definition.Section + "_" + c.BaseConfig.Definition.Key, (OwnConfigEntryBase c) => c); Dictionary<string, CustomSyncedValueBase> 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((object)("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((object)("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((object)("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((object)("Got unexpected type " + text3 + " for " + text2 + " in section " + text + " for mod " + (DisplayName ?? Name) + ", expecting " + type2.AssemblyQualifiedName)); } else { Debug.LogWarning((object)("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((object)("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<ConfigurationManagerAttributes>(allConfig.BaseConfig).ReadOnly = !isWritableConfig(allConfig); } } private void resetConfigsFromServer() { ConfigFile val = null; bool saveOnConfigSet = false; foreach (OwnConfigEntryBase item in allConfigs.Where((OwnConfigEntryBase config) => config.LocalBaseValue != null)) { if (val == null) { val = item.BaseConfig.ConfigFile; saveOnConfigSet = val.SaveOnConfigSet; val.SaveOnConfigSet = false; } item.BaseConfig.BoxedValue = item.LocalBaseValue; item.LocalBaseValue = null; } if (val != null) { val.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<bool> 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 val = new ZPackage(); val.Write((byte)2); val.Write(packageIdentifier); val.Write(fragment); val.Write(fragments); val.Write(data.Skip(250000 * fragment).Take(250000).ToArray()); SendPackage(val); 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, new object[1] { pkg }); } else { rpc.InvokeRoutedRPC(peer.m_server ? 0 : peer.m_uid, text, new object[1] { pkg }); } } IEnumerable<bool> waitForQueue() { float timeout = Time.time + 30f; while (peer.m_socket.GetSendQueueSize() > 20000) { if (Time.time > timeout) { Debug.Log((object)$"Disconnecting {peer.m_uid} after 30 seconds config sending timeout"); peer.m_rpc.Invoke("Error", new object[1] { (object)(ConnectionStatus)5 }); ZNet.instance.Disconnect(peer); break; } yield return false; } } } private IEnumerator sendZPackage(long target, ZPackage package) { if (!Object.op_Implicit((Object)(object)ZNet.instance)) { return Enumerable.Empty<object>().GetEnumerator(); } List<ZNetPeer> list = (List<ZNetPeer>)AccessTools.DeclaredField(typeof(ZRoutedRpc), "m_peers").GetValue(ZRoutedRpc.instance); if (target != 0L) { list = list.Where((ZNetPeer p) => p.m_uid == target).ToList(); } return sendZPackage(list, package); } private IEnumerator sendZPackage(List<ZNetPeer> peers, ZPackage package) { if (!Object.op_Implicit((Object)(object)ZNet.instance)) { yield break; } byte[] array = package.GetArray(); if (array != null && array.LongLength > 10000) { ZPackage val = new ZPackage(); val.Write((byte)4); MemoryStream memoryStream = new MemoryStream(); using (DeflateStream deflateStream = new DeflateStream(memoryStream, CompressionLevel.Optimal)) { deflateStream.Write(array, 0, array.Length); } val.Write(memoryStream.ToArray()); package = val; } List<IEnumerator<bool>> writers = (from p in peers where p.IsReady() select distributeConfigToPeers(p, package)).ToList(); writers.RemoveAll((IEnumerator<bool> writer) => !writer.MoveNext()); while (writers.Count > 0) { yield return null; writers.RemoveAll((IEnumerator<bool> writer) => !writer.MoveNext()); } } private void Broadcast(long target, params ConfigEntryBase[] configs) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(configs); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(sendZPackage(target, package)); } } } private void Broadcast(long target, params CustomSyncedValueBase[] customValues) { if (!IsLocked || isServer) { ZPackage package = ConfigsToPackage(null, customValues); ZNet instance = ZNet.instance; if (instance != null) { ((MonoBehaviour)instance).StartCoroutine(sendZPackage(target, package)); } } } private static OwnConfigEntryBase? configData(ConfigEntryBase config) { return config.Description.Tags?.OfType<OwnConfigEntryBase>().SingleOrDefault(); } public static SyncedConfigEntry<T>? ConfigData<T>(ConfigEntry<T> config) { return ((ConfigEntryBase)config).Description.Tags?.OfType<SyncedConfigEntry<T>>().SingleOrDefault(); } private static T configAttribute<T>(ConfigEntryBase config) { return config.Description.Tags.OfType<T>().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<ConfigEntryBase>? configs = null, IEnumerable<CustomSyncedValueBase>? customValues = null, IEnumerable<PackageEntry>? packageEntries = null, bool partial = true) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Expected O, but got Unknown List<ConfigEntryBase> list = configs?.Where((ConfigEntryBase config) => configData(config).SynchronizedConfig).ToList() ?? new List<ConfigEntryBase>(); List<CustomSyncedValueBase> list2 = customValues?.ToList() ?? new List<CustomSyncedValueBase>(); ZPackage val = new ZPackage(); val.Write(partial ? ((byte)1) : ((byte)0)); val.Write(list.Count + list2.Count + (packageEntries?.Count() ?? 0)); foreach (PackageEntry item in packageEntries ?? Array.Empty<PackageEntry>()) { AddEntryToPackage(val, item); } foreach (CustomSyncedValueBase item2 in list2) { AddEntryToPackage(val, new PackageEntry { section = "Internal", key = item2.Identifier, type = item2.Type, value = item2.BoxedValue }); } foreach (ConfigEntryBase item3 in list) { AddEntryToPackage(val, new PackageEntry { section = item3.Definition.Section, key = item3.Definition.Key, type = configType(item3), value = item3.BoxedValue }); } return val; } 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<string>) && 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<object> source = new List<object>(); ZRpc.Deserialize(new ParameterInfo[2] { null, parameterInfo }, package, ref source); return source.First(); } } [PublicAPI] [HarmonyPatch] public class VersionCheck { private static readonly HashSet<VersionCheck> versionChecks; private static readonly Dictionary<string, string> 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<ZRpc> ValidatedClients = new List<ZRpc>(); 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() { //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Expected O, but got Unknown Patches patchInfo = PatchProcessor.GetPatchInfo((MethodBase)AccessTools.DeclaredMethod(typeof(ZNet), "Awake", (Type[])null, (Type[])null)); if (patchInfo != null && patchInfo.Postfixes.Count((Patch p) => p.PatchMethod.DeclaringType == typeof(ConfigSync.RegisterRPCPatch)) > 0) { return; } Harmony val = 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) { val.PatchAll(item); } } static VersionCheck() { versionChecks = new HashSet<VersionCheck>(); notProcessedNames = new Dictionary<string, string>(); 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(true, true); AccessTools.DeclaredField(typeof(ZNet), "m_connectionStatus").SetValue(null, (object)(ConnectionStatus)3); } private static void DisconnectClient(ZRpc rpc) { rpc.Invoke("Error", new object[1] { 3 }); } private static void CheckVersion(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, null); } private static void CheckVersion(ZRpc rpc, ZPackage pkg, Action<ZRpc, ZPackage>? 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((object)("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((object)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) { //IL_0172: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Expected O, but got Unknown notProcessedNames.Clear(); IDictionary dictionary = (IDictionary)typeof(ZRpc).GetField("m_functions", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(peer.m_rpc); if (dictionary.Contains(StringExtensionMethods.GetStableHashCode("ServerSync VersionCheck"))) { object obj = dictionary[StringExtensionMethods.GetStableHashCode("ServerSync VersionCheck")]; Action<ZRpc, ZPackage> action = (Action<ZRpc, ZPackage>)obj.GetType().GetField("m_action", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(obj); peer.m_rpc.Register<ZPackage>("ServerSync VersionCheck", (Action<ZRpc, ZPackage>)delegate(ZRpc rpc, ZPackage pkg) { CheckVersion(rpc, pkg, action); }); } else { peer.m_rpc.Register<ZPackage>("ServerSync VersionCheck", (Action<ZRpc, ZPackage>)CheckVersion); } foreach (VersionCheck versionCheck in versionChecks) { versionCheck.Initialize(); if (versionCheck.ModRequired || __instance.IsServer()) { Debug.Log((object)("Sending " + versionCheck.DisplayName + " version " + versionCheck.CurrentVersion + " and minimum version " + versionCheck.MinimumRequiredVersion + " to the " + (__instance.IsServer() ? "client" : "server") + ".")); ZPackage val = new ZPackage(); val.Write(versionCheck.Name); val.Write(versionCheck.MinimumRequiredVersion); val.Write(versionCheck.CurrentVersion); peer.m_rpc.Invoke("ServerSync VersionCheck", new object[1] { val }); } } } [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) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Invalid comparison between Unknown and I4 //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01be: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: Unknown result type (might be due to invalid IL or missing references) //IL_01de: Unknown result type (might be due to invalid IL or missing references) //IL_01e3: Unknown result type (might be due to invalid IL or missing references) //IL_01ee: Unknown result type (might be due to invalid IL or missing references) if (!__instance.m_connectionFailedPanel.activeSelf || (int)ZNet.GetConnectionStatus() != 3) { 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<string, string> item in notProcessedNames.OrderBy<KeyValuePair<string, string>, string>((KeyValuePair<string, string> 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 = ((Component)__instance.m_connectionFailedPanel.transform.Find("Image")).GetComponent<RectTransform>(); Vector2 sizeDelta = component.sizeDelta; sizeDelta.x = 675f; component.sizeDelta = sizeDelta; __instance.m_connectionFailedError.ForceMeshUpdate(false, false); float num = __instance.m_connectionFailedError.renderedHeight + 105f; RectTransform component2 = ((Component)((Component)component).transform.Find("ButtonOk")).GetComponent<RectTransform>(); component2.anchoredPosition = new Vector2(component2.anchoredPosition.x, component2.anchoredPosition.y - (num - component.sizeDelta.y) / 2f); sizeDelta = component.sizeDelta; sizeDelta.y = num; component.sizeDelta = sizeDelta; } } } } namespace DvergrAstrolabe { [HarmonyPatch(typeof(Terminal), "InitTerminal")] internal static class AstrolabeCommand { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleEvent <>9__1_0; internal void <Postfix>b__1_0(ConsoleEventArgs args) { Terminal context = args.Context; if (!((Object)(object)context == (Object)null)) { string[] array = Cycle.Report().Split(new char[1] { '\n' }); foreach (string text in array) { context.AddString(text); } } } } private static bool _registered; [HarmonyPostfix] private static void Postfix() { //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown if (_registered) { return; } _registered = true; try { object obj = <>c.<>9__1_0; if (obj == null) { ConsoleEvent val = delegate(ConsoleEventArgs args) { Terminal context = args.Context; if (!((Object)(object)context == (Object)null)) { string[] array = Cycle.Report().Split(new char[1] { '\n' }); foreach (string text in array) { context.AddString(text); } } }; <>c.<>9__1_0 = val; obj = (object)val; } new ConsoleCommand("astrolabe", "Dvergr Astrolabe: prints the day/night cycle in force on this machine - the settings, every step of the maths, and the live clock.", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } catch (Exception ex) { Log.Warn("could not register the 'astrolabe' console command (" + ex.GetType().Name + ": " + ex.Message + "); everything else works."); } } } public enum WaveSpeed { Vanilla, FollowClock } internal static class AstrolabeConfig { public sealed class LegacySettings { public float DayMinutes; public float NightMinutes; } public static readonly ConfigSync Sync = new ConfigSync("wubarrk.dvergrastrolabe") { DisplayName = "DvergrAstrolabe", CurrentVersion = "0.4.1", MinimumRequiredVersion = "0.4.1", ModRequired = true }; public static ConfigEntry<bool> Enabled = null; public static ConfigEntry<bool> LockConfiguration = null; public static ConfigEntry<bool> VerboseLogging = null; public static ConfigEntry<float> CycleMinutes = null; public static ConfigEntry<float> DaytimeMinutes = null; public static ConfigEntry<float> NightMinutes = null; public static ConfigEntry<LengthRule> Rule = null; public static ConfigEntry<float> ProductionScale = null; public static ConfigEntry<ProductionBasis> Basis = null; public static ConfigEntry<float> SleepSkipPushInterval = null; public static ConfigEntry<WaveSpeed> Waves = null; public static ConfigEntry<bool> PatchSelfCheck = null; public static ConfigEntry<int> SelfCheckSteps = null; public static bool Dirty { get; private set; } public static bool DirtyFromServer { get; private set; } public static void ClearDirty() { Dirty = false; DirtyFromServer = false; } private static string D(params string[] lines) { return string.Join("\n", lines); } public static void Bind(ConfigFile config) { //IL_0156: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Expected O, but got Unknown //IL_01d1: Unknown result type (might be due to invalid IL or missing references) //IL_01db: Expected O, but got Unknown //IL_024c: Unknown result type (might be due to invalid IL or missing references) //IL_0256: Expected O, but got Unknown //IL_0350: Unknown result type (might be due to invalid IL or missing references) //IL_035a: Expected O, but got Unknown //IL_0419: Unknown result type (might be due to invalid IL or missing references) //IL_0423: Expected O, but got Unknown //IL_052d: Unknown result type (might be due to invalid IL or missing references) //IL_0537: Expected O, but got Unknown Enabled = Synced<bool>(config.Bind<bool>("1 - General", "Enabled", true, D("Master switch for the whole mod. On = the settings below are in force on the server and for every player who connects. Off = everything is vanilla on every machine (a 30-minute day with 21 minutes of daylight and 9 of night, normal production speed), exactly as if the mod were not installed. The server's value wins; players cannot turn it on or off for themselves.", "Examples:", " true = use the cycle configured below (the default).", " false = switch it all off without uninstalling, for instance to compare with vanilla for an evening."))); LockConfiguration = config.Bind<bool>("1 - General", "LockConfiguration", true, D("On = the server's settings are the only ones that count. Every player who connects gets the server's values automatically and cannot change them for themselves. Off = each player's own cfg file applies to them, which breaks the mod: their clock would run at a different speed from the server's and their sun would be in a different place.", "Examples:", " true = leave it on (the default). The only sensible value on a real server.", " false = only for testing on your own machine when you want to edit values in-game with a config manager.")); Sync.AddLockingConfigEntry<bool>(LockConfiguration); Watch<bool>(LockConfiguration); VerboseLogging = Local<bool>(config.Bind<bool>("1 - General", "VerboseLogging", false, D("Extra lines in BepInEx/LogOutput.log. On = every sunrise and sunset is logged with the day number and how long the daylight or night that just ended really took, in real minutes, against what you configured; every sleep skip; and the self-check result. Off = only the settings summary at startup and after a change. Not synced: the server and each player decide for themselves.", "Examples:", " true = you want proof in the log that a 45-minute daytime really lasts 45 minutes.", " false = normal play (the default)."))); CycleMinutes = Synced<float>(config.Bind<float>("2 - Cycle", "CycleMinutes", 30f, new ConfigDescription(D("How many real minutes one full day lasts, from one sunrise to the next (daylight and night together). Vanilla is 30. This is the big dial: set it and the game stretches or shrinks its day to match.", "Examples:", " 30 = a vanilla-length day (with DaytimeMinutes 21 and NightMinutes 9 below).", " 60 = each day takes one real hour.", " 120 = a two-hour day, for builders who hate running out of light.", " 10 = a ten-minute day, for a frantic server.", "DaytimeMinutes + NightMinutes should add up to this; if they do not, LengthRule decides what gives way.", "How the game does it: the day becomes L = round(R x 60 x CycleMinutes) game-seconds long, where R is the clock rate ProductionScale sets. The log prints the working every time the settings are applied."), (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 1440f), Array.Empty<object>()))); DaytimeMinutes = Synced<float>(config.Bind<float>("2 - Cycle", "DaytimeMinutes", 21f, new ConfigDescription(D("How many of those minutes the sun is up: from sunrise to sunset. Vanilla is 21 (of 30). Daytime is when the 'Day N' banner appears, monsters are calmer, and sleeping is not possible until the afternoon (halfway through daytime).", "Examples:", " 21 = vanilla (with NightMinutes 9 and CycleMinutes 30).", " 45 = three quarters of an hour of light in a 60-minute day (with NightMinutes 15).", " 10 = short days with a long 20-minute night, horror style (with CycleMinutes 30 and NightMinutes 20).", " 100 = almost the whole of a two-hour day is light (with CycleMinutes 120 and NightMinutes 20).", "How the game does it: the daylight share f = DaytimeMinutes / CycleMinutes puts sunrise at 0.5 - f/2 and sunset at 0.5 + f/2 of the day (vanilla 0.15 and 0.85). The sun crosses the sky at 180 / DaytimeMinutes degrees per real minute (vanilla 8.57)."), (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 1440f), Array.Empty<object>()))); NightMinutes = Synced<float>(config.Bind<float>("2 - Cycle", "NightMinutes", 9f, new ConfigDescription(D("How many of those minutes the sun is down: from sunset to sunrise, through dusk, midnight (when the day counter ticks over) and dawn. Vanilla is 9 (of 30). Night is when monsters are bolder and you can sleep it away; sleeping still skips to sunrise in 12 real seconds whatever you set here.", "Examples:", " 9 = vanilla (with DaytimeMinutes 21 and CycleMinutes 30).", " 15 = a quarter of an hour of dark in a 60-minute day (with DaytimeMinutes 45).", " 5 = a night that is over almost before you light a torch (with DaytimeMinutes 40 and CycleMinutes 45).", " 20 = a long night, horror style (with DaytimeMinutes 10 and CycleMinutes 30).", "How the game does it: the night sky turns at 180 / NightMinutes degrees per real minute (vanilla 20)."), (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 1440f), Array.Empty<object>()))); Rule = Synced<LengthRule>(config.Bind<LengthRule>("2 - Cycle", "LengthRule", LengthRule.FitToCycle, D("What happens when DaytimeMinutes + NightMinutes is not CycleMinutes. When the three add up, this does nothing. When they do not, one of them has to give:", " FitToCycle = the day really is CycleMinutes long; daytime and night are shrunk or stretched to fit, keeping their proportions. (The default.)", " DaytimePlusNight = the day is as long as daytime + night; CycleMinutes is ignored. (How 0.3 and earlier worked.)", " CycleMinusNight = night is exactly NightMinutes; daytime is whatever is left of CycleMinutes.", " CycleMinusDaytime = daytime is exactly DaytimeMinutes; night is whatever is left of CycleMinutes.", "Example: CycleMinutes 60 with DaytimeMinutes 21 and NightMinutes 9 (you changed the day length and left the other two at vanilla) gives:", " FitToCycle = 42 minutes of daylight and 18 of night: vanilla proportions in a 60-minute day.", " DaytimePlusNight = a 30-minute day of 21 + 9; the 60 is ignored.", " CycleMinusNight = 51 minutes of daylight and 9 of night.", " CycleMinusDaytime = 21 minutes of daylight and 39 of night.", "The log says which value was adjusted."))); ProductionScale = Synced<float>(config.Bind<float>("3 - Production", "ProductionScale", 1f, new ConfigDescription(D("How fast the world works, as a multiple of vanilla. Everything that runs on the game clock follows it: crops and trees growing; smelters, kilns, furnaces, cooking stations and fermenters working; beehives and sap collectors filling; fires burning fuel; dropped items despawning; berries and other pickables respawning; creature spawners; taming, breeding and growing up; creature health regeneration; traps and ballistas rearming; weather and wind changing. It never changes how fast the sun moves: that comes from the three lengths above.", "Examples:", " 1 = vanilla speed (the default). With ProductionBasis RealTime, crops grow exactly as fast per real minute as in vanilla even on a two-hour day.", " 2 = everything twice as fast: a smelter run that took 30 minutes takes 15.", " 0.5 = everything at half speed, for a slow survival-flavoured server.", " 3 = a busy world where a copper run smelts while you cook dinner.", "Raids, random events, the Rested buff, food and comfort run on real time and are NOT affected.", "How the game does it: with ProductionBasis RealTime this is the clock rate R itself (game-seconds per real second), and a vanilla day of work (1800 game-seconds) takes 30 / ProductionScale real minutes whatever the day length."), (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.01f, 100f), Array.Empty<object>()))); Basis = Synced<ProductionBasis>(config.Bind<ProductionBasis>("3 - Production", "ProductionBasis", ProductionBasis.RealTime, D("What ProductionScale = 1 means.", " RealTime = vanilla speed per REAL MINUTE, whatever the day length. A carrot that takes 30 real minutes in vanilla takes 30 real minutes here too, which on a 60-minute day is half a day. The game day is stretched behind the scenes (L = R x 60 x CycleMinutes game-seconds) and the clock runs at R = ProductionScale. The default, and what most servers want: longer days do not slow the world down.", " GameDays = vanilla speed per GAME DAY, the way 0.3 and earlier worked. A carrot that takes one day in vanilla takes one day here too, which on a 60-minute day is 60 real minutes, twice as slow per real minute. The day stays 1800 x ProductionScale game-seconds and the clock runs at R = ProductionScale x 30 / CycleMinutes.", "Example, CycleMinutes 60 and ProductionScale 1:", " RealTime = a 3600-game-second day, the clock at normal speed, crops at vanilla real-time speed; one game day holds two vanilla days of growth.", " GameDays = an 1800-game-second day, the clock at half speed, crops take the same number of days as vanilla (so twice as many real minutes).", "Changing the day length (RealTime does this whenever CycleMinutes or ProductionScale change) makes the day counter and the time of day of an existing world jump once; GameDays with ProductionScale 1 never changes it. See the README."))); SleepSkipPushInterval = Local<float>(config.Bind<float>("4 - Clock", "SleepSkipPushInterval", 0.5f, new ConfigDescription(D("Only matters while everyone is asleep. The server runs the skipped night forward in 12 real seconds and tells the players' games where the clock is every so often; this is how often, in seconds. Smaller = the fast-forwarded night looks smoother on players' screens, at the cost of one tiny network packet per player per interval. Vanilla is 2 (about 6 visible steps); 0.5 gives about 24. Has no effect at any other time, because every machine already runs the clock at the same speed. Server side only.", "Examples:", " 0.5 = smooth (the default).", " 2 = exactly vanilla, no extra packets.", " 0.1 = silky: 10 packets per player per second, for the 12 seconds of the skip."), (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 2f), Array.Empty<object>()))); Waves = Synced<WaveSpeed>(config.Bind<WaveSpeed>("4 - Clock", "WaveSpeed", WaveSpeed.Vanilla, D("How fast the sea moves when the clock runs faster or slower than vanilla (ProductionScale other than 1, or GameDays with a day length other than 30).", " Vanilla = waves, the swell under boats and the fish keep real-time speed, so the sea looks and behaves exactly as in vanilla whatever the clock does. (The default.)", " FollowClock = the sea follows the game clock, as an unmodded game would: at ProductionScale 3 the waves roll three times as fast, and in a storm a hull can take slam damage from a surface that moves that quickly.", "Examples:", " Vanilla with ProductionScale 2 = the world works twice as fast but the sea looks normal.", " FollowClock with ProductionScale 2 = the sea rolls at double speed too.", " Either, with ProductionScale 1 and RealTime = no difference at all.", "Changing the clock speed while Vanilla is selected shifts the wave pattern once, on every machine at the same moment."))); PatchSelfCheck = Local<bool>(config.Bind<bool>("5 - Compatibility", "PatchSelfCheck", true, D("Lets the mod check its own work and tell you if another mod is interfering. On = after the settings are applied, the mod asks the game where sunrise and sunset now are and confirms the answer matches what it set (an error in the log names the other mod's values if not), and every 5 seconds it checks the day length and puts its own value back if something else changed it (a warning in the log says what to turn off). Off = no checks; only if you deliberately let another mod own part of the cycle.", "Examples:", " true = leave it on (the default). Costs nothing you would notice.", " false = you know what you are doing and the warnings are expected."))); SelfCheckSteps = Local<int>(config.Bind<int>("5 - Compatibility", "SelfCheckSteps", 1000, new ConfigDescription(D("How carefully the self-check reads the game's sunrise and sunset: it samples the day this many times, so the positions it reports are accurate to 1/steps of a day. More = more accurate and slightly more work, done only at startup, when the world loads and after a settings change.", "Examples:", " 1000 = accurate to a tenth of a percent of a day (the default; fine for everyone).", " 100 = coarse but instant.", " 10000 = overkill, for debugging a conflict with another mod."), (AcceptableValueBase)(object)new AcceptableValueRange<int>(100, 10000), Array.Empty<object>()))); } private static ConfigEntry<T> Synced<T>(ConfigEntry<T> entry) { Sync.AddConfigEntry<T>(entry); Watch<T>(entry); return entry; } private static ConfigEntry<T> Local<T>(ConfigEntry<T> entry) { Watch<T>(entry); return entry; } private static void Watch<T>(ConfigEntry<T> entry) { entry.SettingChanged += delegate { Dirty = true; if (ConfigSync.ProcessingServerUpdate) { DirtyFromServer = true; } }; } public static LegacySettings? PeekLegacy(string path) { try { if (!File.Exists(path)) { return null; } string text = ""; float? num = null; float? num2 = null; string[] array = File.ReadAllLines(path); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.StartsWith("[") && text2.EndsWith("]")) { text = text2.Substring(1, text2.Length - 2).Trim(); if (text == "2 - Cycle") { return null; } } else { if (text != "2 - Day And Night" || text2.StartsWith("#")) { continue; } int num3 = text2.IndexOf('='); if (num3 >= 0) { string text3 = text2.Substring(0, num3).Trim(); string s = text2.Substring(num3 + 1).Trim(); float result2; if (text3 == "DayMinutes" && float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { num = result; } else if (text3 == "NightMinutes" && float.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out result2)) { num2 = result2; } } } } return (num.HasValue && num2.HasValue) ? new LegacySettings { DayMinutes = num.Value, NightMinutes = num2.Value } : null; } catch { return null; } } public static string ApplyLegacy(ConfigFile config, LegacySettings legacy) { bool saveOnConfigSet = config.SaveOnConfigSet; config.SaveOnConfigSet = false; try { CycleMinutes.Value = legacy.DayMinutes + legacy.NightMinutes; DaytimeMinutes.Value = legacy.DayMinutes; NightMinutes.Value = legacy.NightMinutes; Basis.Value = ProductionBasis.GameDays; } finally { config.SaveOnConfigSet = saveOnConfigSet; } config.Save(); ClearDirty(); return "migrated the 0.3 settings: DayMinutes " + legacy.DayMinutes.ToString(CultureInfo.InvariantCulture) + " + NightMinutes " + legacy.NightMinutes.ToString(CultureInfo.InvariantCulture) + " became CycleMinutes " + CycleMinutes.Value.ToString(CultureInfo.InvariantCulture) + ", DaytimeMinutes " + DaytimeMinutes.Value.ToString(CultureInfo.InvariantCulture) + ", NightMinutes " + NightMinutes.Value.ToString(CultureInfo.InvariantCulture) + " with ProductionBasis = GameDays, which keeps the 1800-second day and the per-day economy of 0.3 (set RealTime for vanilla real-time production). The old [2 - Day And Night], [3 - Client Sync] and [4 - Boats] keys and the old [5 - Compatibility] BandDetection / BandProbeSteps are ignored and can be deleted. Clients now need the mod too."; } } public static class BandProbe { public const float DayBeginsAt = 0.25f; public const float DayEndsAt = 0.75f; public const int MinSteps = 100; public const int MaxSteps = 10000; public const int DefaultSteps = 1000; private const float Tolerance = 1E-05f; private const float Slack = 1E-06f; public static bool Find(Func<float, float> rescale, int steps, out double sunriseFraction, out double sunsetFraction, out string? failure) { sunriseFraction = 0.0; sunsetFraction = 0.0; failure = null; steps = Math.Max(100, Math.Min(10000, steps)); int num = -1; int num2 = -1; float num3 = float.NegativeInfinity; for (int i = 0; i <= steps; i++) { float num4 = (float)i / (float)steps; float num5 = rescale(num4); if (float.IsNaN(num5) || float.IsInfinity(num5)) { failure = $"RescaleDayFraction({num4:0.###}) returned {num5}"; return false; } if (num5 < num3 - 1E-06f) { failure = $"RescaleDayFraction is not monotonic ({num3:0.####} at raw {(double)(i - 1) / (double)steps:0.###}, then {num5:0.####} at raw {num4:0.###})"; return false; } num3 = num5; if (num < 0 && num5 >= 0.24999f) { num = i; } if (num5 <= 0.75001f) { num2 = i; } } if (num < 0) { failure = "RescaleDayFraction never reaches 0.25, so the game would never count day"; return false; } if (num2 < 0) { failure = "RescaleDayFraction starts above 0.75, so the game would never count day"; return false; } sunriseFraction = (double)num / (double)steps; sunsetFraction = (double)num2 / (double)steps; if (sunsetFraction <= sunriseFraction) { failure = $"no daylight: sunset (raw {sunsetFraction:0.###}) is not after sunrise (raw {sunriseFraction:0.###})"; return false; } if (sunsetFraction - sunriseFraction >= 1.0) { failure = "no night: daylight spans the whole day"; return false; } return true; } } [HarmonyPatch(typeof(ZNet), "UpdateNetTime")] internal static class UpdateNetTimePatch { [HarmonyPrefix] private static void Prefix(ZNet __instance, ref float dt) { if (!Cycle.Active || dt <= 0f) { return; } try { dt = (float)((double)dt * Cycle.Current.ClockRate); Cycle.Tick(__instance); } catch (Exception ex) { Cycle.Fail("ZNet.UpdateNetTime", ex); } } } [HarmonyPatch(typeof(ZNet), "GetWrappedDayTimeSeconds")] internal static class WaterClockPatch { [HarmonyPrefix] private static bool Prefix(ZNet __instance, ref float __result) { if (!Cycle.Active || AstrolabeConfig.Waves.Value != WaveSpeed.Vanilla) { return true; } double clockRate = Cycle.Current.ClockRate; if (Math.Abs(clockRate - 1.0) < 1E-09) { return true; } __result = (float)(__instance.GetTimeSeconds() / clockRate % 86400.0); return false; } } internal static class ClockPush { public const float VanillaPushIntervalSeconds = 2f; private static readonly MethodInfo? SendNetTime = AccessTools.Method(typeof(ZNet), "SendNetTime", (Type[])null, (Type[])null); private static float _timer; private static bool _warned; public static void Tick(float realDt) { ZNet instance = ZNet.instance; EnvMan instance2 = EnvMan.instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance2 == (Object)null || !instance.IsServer() || !Cycle.Active || !instance2.IsTimeSkipping()) { _timer = 0f; return; } float value = AstrolabeConfig.SleepSkipPushInterval.Value; if (value >= 2f) { _timer = 0f; return; } _timer += realDt; if (_timer < value) { return; } _timer = 0f; if (SendNetTime == null) { WarnOnce("ZNet.SendNetTime not found in this game build - extra clock pushes during sleep disabled, vanilla's 2 s sync still applies."); return; } try { SendNetTime.Invoke(instance, null); } catch (Exception ex) { WarnOnce("extra clock push failed (" + ex.GetType().Name + ": " + ex.Message + ") - vanilla's 2 s sync still applies."); } } private static void WarnOnce(string message) { if (!_warned) { _warned = true; Log.Warn(message); } } } internal static class Cycle { public const float WatchIntervalSeconds = 5f; private static readonly MethodInfo? RescaleMethod = AccessTools.Method(typeof(EnvMan), "RescaleDayFraction", new Type[1] { typeof(float) }, (Type[])null); private static readonly object[] InvokeArgs = new object[1]; private static readonly SegmentLog Segments = new SegmentLog(); private static string? _failure; private static long? _sceneDayLength; private static EnvMan? _writtenEnv; private static long _foreignLogged = -1L; private static string? _selfCheckLogged; private static float _watchTimer; private static bool _loggedFirstTick; public static CycleSolution Current { get; private set; } = CycleMath.Solve(CycleInputs.Vanilla); public static bool HasFailed => _failure != null; public static bool Active { get { if (_failure == null && AstrolabeConfig.Enabled != null) { return AstrolabeConfig.Enabled.Value; } return false; } } public static string Side { get { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return "no world loaded"; } if (!instance.IsServer()) { return "client"; } if (!instance.IsDedicated()) { return "host"; } return "dedicated server"; } } public static CycleInputs ReadInputs() { return new CycleInputs { CycleMinutes = AstrolabeConfig.CycleMinutes.Value, DaytimeMinutes = AstrolabeConfig.DaytimeMinutes.Value, NightMinutes = AstrolabeConfig.NightMinutes.Value, Rule = AstrolabeConfig.Rule.Value, ProductionScale = AstrolabeConfig.ProductionScale.Value, Basis = AstrolabeConfig.Basis.Value }; } public static void Apply(string reason) { CycleSolution cycleSolution = (Current = CycleMath.Solve(ReadInputs())); Segments.Forget(); EnvMan instance = EnvMan.instance; if (!Active) { string text = (HasFailed ? "a patch failed, see the error above" : "Enabled = false"); Log.Always("disabled (" + reason + ", " + text + "): vanilla on this machine - 1800-second day, 21 + 9 real minutes, 1x clock."); if ((Object)(object)instance != (Object)null) { ApplyToEnvMan(instance); } return; } Log.Always(cycleSolution.Breakdown("cycle (" + reason + ", " + Side + "):")); if (cycleSolution.LengthsAdjusted) { Log.Warn($"the three lengths did not agree, so LengthRule = {cycleSolution.Inputs.Rule} adjusted them: {cycleSolution.LengthNote}. Type three values that add up, or pick the rule that keeps the ones you care about, to silence this."); } ZNet instance2 = ZNet.instance; if ((Object)(object)instance2 != (Object)null && instance2.IsServer() && !AstrolabeConfig.LockConfiguration.Value) { Log.Warn("LockConfiguration = false: clients may run their own cycle settings, and a client whose values differ sees its clock corrected on every push and a different time of day than the server. Keep it on outside testing."); } if (cycleSolution.ClockRate > 10.0) { Log.Always("note: the clock runs at " + F(cycleSolution.ClockRate, "0.##") + "x, so weather and wind change that many times as often per real minute as in vanilla (they live on the game clock), and so do the waves if WaveSpeed = FollowClock."); } if ((Object)(object)instance != (Object)null) { ApplyToEnvMan(instance); SelfCheck(instance); } else { Log.Info("no world loaded yet: EnvMan gets the day length when it wakes up."); } } public static void OnEnvManAwake(EnvMan env) { _loggedFirstTick = false; Segments.Forget(); ApplyToEnvMan(env); if (Active) { SelfCheck(env); } } public static void Restore() { EnvMan instance = EnvMan.instance; if ((Object)(object)instance != (Object)null && _sceneDayLength.HasValue) { instance.m_dayLengthSec = _sceneDayLength.Value; } } public static void Fail(string where, Exception ex) { if (_failure != null) { return; } _failure = where + ": " + ex.GetType().Name + ": " + ex.Message; Log.Error($"{where} threw; Astrolabe goes passive until restart (vanilla day length, knees and clock on this machine): {ex}"); try { Restore(); } catch { } } private static void ApplyToEnvMan(EnvMan env) { bool flag = env != _writtenEnv; if (flag) { _writtenEnv = env; if (!_sceneDayLength.HasValue) { _sceneDayLength = env.m_dayLengthSec; } } long num = (Active ? Current.DayLengthSeconds : (_sceneDayLength ?? 1800)); long dayLengthSec = env.m_dayLengthSec; if (dayLengthSec != num) { env.m_dayLengthSec = num; ZNet instance = ZNet.instance; if (flag || (Object)(object)instance == (Object)null) { Log.Info($"EnvMan.m_dayLengthSec {dayLengthSec} -> {num} game-seconds."); return; } double timeSeconds = instance.GetTimeSeconds(); int num2 = (int)(timeSeconds / (double)dayLengthSec); int num3 = (int)(timeSeconds / (double)num); double value = Frac(timeSeconds, dayLengthSec); double value2 = Frac(timeSeconds, num); Log.Always(string.Format("day length changed on a running world, {0} -> {1} game-seconds: the world clock ({2} game-s) is untouched, so the day counter now reads {3} (was {4}) and the time of day moved from raw {5} to {6} of the day; everything stamped in game-seconds (crops, smelters, fires) carries on unchanged. ProductionBasis = GameDays with ProductionScale = 1 keeps the day at 1800 and never does this.", dayLengthSec, num, F(timeSeconds, "0"), num3, num2, F(value, "0.###"), F(value2, "0.###"))); } } public static void Watch(float realDt) { _watchTimer += realDt; if (_watchTimer < 5f) { return; } _watchTimer = 0f; if (!Active || !AstrolabeConfig.PatchSelfCheck.Value) { return; } EnvMan instance = EnvMan.instance; if ((Object)(object)instance == (Object)null || instance != _writtenEnv) { return; } long dayLengthSeconds = Current.DayLengthSeconds; long dayLengthSec = instance.m_dayLengthSec; if (dayLengthSec != dayLengthSeconds) { instance.m_dayLengthSec = dayLengthSeconds; if (dayLengthSec != _foreignLogged) { _foreignLogged = dayLengthSec; Log.Warn($"another mod set EnvMan.m_dayLengthSec to {dayLengthSec}; Astrolabe put its {dayLengthSeconds} back. Two mods are fighting over the day length: turn the other one's day length off (Gamma of Night Lights: [Day night cycle] Enabled = false) or set Astrolabe's Enabled = false."); } } } private static void SelfCheck(EnvMan env) { if (!AstrolabeConfig.PatchSelfCheck.Value) { return; } int num = Math.Max(100, Math.Min(10000, AstrolabeConfig.SelfCheckSteps.Value)); if (RescaleMethod == null) { SelfCheckError("EnvMan.RescaleDayFraction(float) is not in this game build, so the knees cannot be verified (and the patch on it may not have applied)."); return; } bool flag; double sunriseFraction; double sunsetFraction; string failure; try { flag = BandProbe.Find(delegate(float raw) { InvokeArgs[0] = raw; return (float)RescaleMethod.Invoke(env, InvokeArgs); }, num, out sunriseFraction, out sunsetFraction, out failure); } catch (Exception ex) { Exception ex2 = ((ex is TargetInvocationException { InnerException: not null } ex3) ? ex3.InnerException : ex); SelfCheckError("sampling EnvMan.RescaleDayFraction threw " + ex2.GetType().Name + ": " + ex2.Message); return; } if (!flag) { SelfCheckError("sampling EnvMan.RescaleDayFraction failed: " + failure); return; } double num2 = 1.0 / (double)num + 1E-06; if (Math.Abs(sunriseFraction - Current.SunriseFraction) > num2 || Math.Abs(sunsetFraction - Current.SunsetFraction) > num2) { SelfCheckError("EnvMan.RescaleDayFraction answers with sunrise at raw " + F(sunriseFraction, "0.####") + " and sunset at " + F(sunsetFraction, "0.####") + ", not the " + F(Current.SunriseFraction, "0.####") + " / " + F(Current.SunsetFraction, "0.####") + " Astrolabe set: another mod has patched it as well (Gamma of Night Lights' [Day night cycle] does). Turn that feature off; until then the time of day on this machine is that mod's, not this one's."); } else { _selfCheckLogged = null; Log.Info(string.Format("self-check: EnvMan.RescaleDayFraction answers with sunrise at raw {0} and sunset at {1}, as set ({2} samples); EnvMan.m_dayLengthSec = {3}.", F(sunriseFraction, "0.####"), F(sunsetFraction, "0.####"), num, env.m_dayLengthSec)); } } private static void SelfCheckError(string message) { if (!(message == _selfCheckLogged)) { _selfCheckLogged = message; Log.Error("self-check: " + message); } } public static void Tick(ZNet net) { double timeSeconds = net.GetTimeSeconds(); if (!_loggedFirstTick) { _loggedFirstTick = true; EnvMan instance = EnvMan.instance; double num = Current.RawFraction(timeSeconds); Log.Always(string.Format("clock live on the {0}: day {1}, raw day-fraction {2} ({3}), EnvMan day length {4} game-s, clock at {5}x, {6} player(s).", Side, Current.DayAt(timeSeconds), F(num, "0.000"), Current.IsSunUp(num) ? "daylight" : "night", ((Object)(object)instance != (Object)null) ? instance.m_dayLengthSec.ToString() : "unknown, no EnvMan", F(Current.ClockRate, "0.####"), net.GetNrOfPlayers())); } if (Segments.Observe(Current, timeSeconds, Time.realtimeSinceStartup, out var crossing) && AstrolabeConfig.VerboseLogging.Value) { string text = (crossing.SunUp ? "night" : "daylight"); string text2 = (crossing.Measured ? (text + " ran " + F(crossing.RanSeconds / 60.0, "0.00") + " real min (" + F(crossing.ScheduledSeconds / 60.0, "0.##") + " scheduled); ") : ""); string text3 = (crossing.SunUp ? ("daylight for the next " + F(Current.DaytimeMinutes, "0.##") + " real min") : ("night for the next " + F(Current.NightMinutes, "0.##") + " real min")); Log.Info(string.Format("{0}, day {1} ({2}): {3}{4}.", crossing.SunUp ? "sunrise" : "sunset", crossing.Day, Side, text2, text3)); } } public static string Report() { string text = (Active ? Current.Breakdown("Dvergr Astrolabe 0.4.1 on this " + Side + ":") : ("Dvergr Astrolabe 0.4.1 on this " + Side + ": disabled (" + (HasFailed ? _failure : "Enabled = false") + "), vanilla cycle.")); ZNet instance = ZNet.instance; EnvMan instance2 = EnvMan.instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance2 == (Object)null) { return text + "\n live : no world loaded"; } double timeSeconds = instance.GetTimeSeconds(); double num = Current.RawFraction(timeSeconds); string text2 = (AstrolabeConfig.Sync.IsSourceOfTruth ? "this machine's cfg" : "the server (locked)"); return text + string.Format("\n live : world clock {0} game-s, day {1}, raw day-fraction {2} ({3}), visual {4}, EnvMan day length {5} game-s, {6}; settings from {7}", F(timeSeconds, "0"), Current.DayAt(timeSeconds), F(num, "0.000"), Current.IsSunUp(num) ? "daylight" : "night", F(instance2.GetDayFraction(), "0.000"), instance2.m_dayLengthSec, instance2.IsTimeSkipping() ? "sleeping (night being skipped)" : "clock running", text2); } private static double Frac(double t, long length) { double num = t % (double)length / (double)length; if (!(num < 0.0)) { return num; } return num + 1.0; } private static string F(double value, string format) { return CycleSolution.F(value, format); } } public enum LengthRule { FitToCycle, DaytimePlusNight, CycleMinusNight, CycleMinusDaytime } public enum ProductionBasis { RealTime, GameDays } public struct CycleInputs { public double CycleMinutes; public double DaytimeMinutes; public double NightMinutes; public LengthRule Rule; public double ProductionScale; public ProductionBasis Basis; public static CycleInputs Vanilla => new CycleInputs { CycleMinutes = 30.0, DaytimeMinutes = 21.0, NightMinutes = 9.0, Rule = LengthRule.FitToCycle, ProductionScale = 1.0, Basis = ProductionBasis.RealTime }; } public sealed class CycleSolution { private readonly float _s; private readonly float _e; private readonly float _band; private readonly float _tail; public CycleInputs Inputs { get; } public double CycleMinutes { get; } public double DaytimeMinutes { get; } public double NightMinutes { get; } public bool LengthsAdjusted { get; } public string LengthNote { get; } public double DaylightShare { get; } public double RequestedRate { get; } public long DayLengthSeconds { get; } public double ClockRate { get; } public double SunriseFraction { get; } public double SunsetFraction { get; } public double DaylightGameSeconds => DaylightShare * (double)DayLengthSeconds; public double NightGameSeconds => (double)DayLengthSeconds - DaylightGameSeconds; public double ProductionPerRealMinute => ClockRate; public double ProductionPerGameDay => (double)DayLengthSeconds / 1800.0; public double RealisedProductionScale { get { if (Inputs.Basis != ProductionBasis.RealTime) { return ClockRate * CycleMinutes / 30.0; } return ClockRate; } } public double SunDegreesPerMinuteByDay => 180.0 / DaytimeMinutes; public double SunDegreesPerMinuteByNight => 180.0 / NightMinutes; public double VanillaDayOfWorkRealMinutes => 1800.0 / ClockRate / 60.0; public double VanillaDayOfWorkGameDays => 1800.0 / (double)DayLengthSeconds; public bool IsVanillaBand { get { if (Math.Abs(SunriseFraction - 0.15) < 1E-09) { return Math.Abs(SunsetFraction - 0.85) < 1E-09; } return false; } } public bool IsVanilla { get { if (DayLengthSeconds == 1800 && Math.Abs(ClockRate - 1.0) < 1E-12) { return IsVanillaBand; } return false; } } internal CycleSolution(CycleInputs inputs, double cycle, double daytime, double night, bool adjusted, string note, double share, double requestedRate, long dayLength, double clockRate, double sunrise, double sunset) { Inputs = inputs; CycleMinutes = cycle; DaytimeMinutes = daytime; NightMinutes = night; LengthsAdjusted = adjusted; LengthNote = note; DaylightShare = share; RequestedRate = requestedRate; DayLengthSeconds = dayLength; ClockRate = clockRate; SunriseFraction = sunrise; SunsetFraction = sunset; if (IsVanillaBand) { _s = 0.15f; _e = 0.85f; _band = 0.7f; _tail = 0.15f; } else { _s = (float)sunrise; _e = (float)sunset; _band = (float)(sunset - sunrise); _tail = (float)(1.0 - sunset); } } public float Rescale(float fraction) { if (fraction >= _s && fraction <= _e) { float num = (fraction - _s) / _band; return 0.25f + num * 0.5f; } if (fraction < 0.5f) { return fraction / _s * 0.25f; } float num2 = (fraction - _e) / _tail; return 0.75f + num2 * 0.25f; } public double MorningStartSeconds(long dayLengthSeconds, int day) { return (double)day * (double)dayLengthSeconds + (double)dayLengthSeconds * SunriseFraction; } public double RawFraction(double gameSeconds) { double num = gameSeconds % (double)DayLengthSeconds / (double)DayLengthSeconds; if (!(num < 0.0)) { return num; } return num + 1.0; } public bool IsSunUp(double rawFraction) { if (rawFraction >= SunriseFraction) { return rawFraction < SunsetFraction; } return false; } public int DayAt(double gameSeconds) { return (int)(gameSeconds / (double)DayLengthSeconds); } public string Summary() { return string.Format("cycle {0} real min = daytime {1} + night {2}; day length {3} game-s, clock {4}x, sunrise/sunset at raw {5}/{6}; production {7}x per real minute, {8}x per game day", F(CycleMinutes), F(DaytimeMinutes), F(NightMinutes), DayLengthSeconds, F(ClockRate, "0.####"), F(SunriseFraction, "0.####"), F(SunsetFraction, "0.####"), F(ProductionPerRealMinute, "0.###"), F(ProductionPerGameDay, "0.###")); } public string Breakdown(string heading) { CycleInputs inputs = Inputs; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(heading).Append('\n'); stringBuilder.Append($" settings : CycleMinutes = {F(inputs.CycleMinutes)}, DaytimeMinutes = {F(inputs.DaytimeMinutes)}, NightMinutes = {F(inputs.NightMinutes)}, LengthRule = {inputs.Rule}, ProductionScale = {F(inputs.ProductionScale)}, ProductionBasis = {inputs.Basis}\n"); stringBuilder.Append(" 1 lengths : " + LengthNote + " -> cycle " + F(CycleMinutes) + " real min = daytime " + F(DaytimeMinutes) + " + night " + F(NightMinutes) + "\n"); stringBuilder.Append(" 2 share : daylight share f = daytime / cycle = " + F(DaytimeMinutes) + " / " + F(CycleMinutes) + " = " + F(DaylightShare, "0.####") + " (vanilla 0.7)\n"); if (inputs.Basis == ProductionBasis.RealTime) { stringBuilder.Append(" 3 rate : ProductionBasis RealTime: requested clock rate R0 = ProductionScale = " + F(RequestedRate, "0.####") + " game-seconds per real second (vanilla 1)\n"); } else { stringBuilder.Append(" 3 rate : ProductionBasis GameDays: requested clock rate R0 = ProductionScale x 1800 / (60 x cycle) = " + F(inputs.ProductionScale) + " x 1800 / " + F(60.0 * CycleMinutes) + " = " + F(RequestedRate, "0.####") + " game-seconds per real second (vanilla 1)\n"); } stringBuilder.Append(string.Format(" 4 day : day length L = round(R0 x 60 x cycle) = round({0} x 60 x {1}) = {2} game-seconds (vanilla 1800) -> EnvMan.m_dayLengthSec on every machine\n", F(RequestedRate, "0.####"), F(CycleMinutes), DayLengthSeconds)); stringBuilder.Append(string.Format(" 5 clock : exact clock rate R = L / (60 x cycle) = {0} / {1} = {2} game-seconds per real second -> ZNet.UpdateNetTime adds dt x R on every machine\n", DayLengthSeconds, F(60.0 * CycleMinutes), F(ClockRate, "0.######"))); stringBuilder.Append(" 6 band : sunrise at raw day-fraction 0.5 - f/2 = " + F(SunriseFraction, "0.####") + ", sunset at 0.5 + f/2 = " + F(SunsetFraction, "0.####") + " (vanilla 0.15 / 0.85) -> EnvMan.RescaleDayFraction, GetMorningStartSec and SkipToMorning on every machine\n"); stringBuilder.Append($" check : daylight = f x L = {F(DaylightGameSeconds)} game-s, at R that is {F(DaylightGameSeconds / ClockRate / 60.0)} real min; night = {F(NightGameSeconds)} game-s = {F(NightGameSeconds / ClockRate / 60.0)} real min; full day = {DayLengthSeconds} game-s = {F((double)DayLengthSeconds / ClockRate / 60.0)} real min\n"); stringBuilder.Append(" production : " + F(ProductionPerRealMinute, "0.###") + "x vanilla per real minute (= R; a vanilla day of work, 1800 game-s, takes " + F(VanillaDayOfWorkRealMinutes) + " real min); " + F(ProductionPerGameDay, "0.###") + "x vanilla per game day (= L / 1800; one game day holds " + F(ProductionPerGameDay, "0.###") + " vanilla days of growth); realised ProductionScale " + F(RealisedProductionScale, "0.####") + " (requested " + F(inputs.ProductionScale) + ")\n"); stringBuilder.Append(" sun : " + F(SunDegreesPerMinuteByDay) + " degrees per real minute by day (180 / daytime; vanilla 8.57), " + F(SunDegreesPerMinuteByNight) + " by night (180 / night; vanilla 20); the sun's pace depends on the lengths only, never on ProductionScale"); return stringBuilder.ToString(); } internal static string F(double value, string format = "0.##") { return value.ToString(format, CultureInfo.InvariantCulture); } } public static class CycleMath { public const double VanillaDayLengthSeconds = 1800.0; public const double VanillaCycleMinutes = 30.0; public const double VanillaDaytimeMinutes = 21.0; public const double VanillaNightMinutes = 9.0; public const double VanillaSunriseFraction = 0.15; public const double VanillaSunsetFraction = 0.85; public const double MinSegmentMinutes = 0.25; public const double MaxCycleMinutes = 1440.0; public const double MinProductionScale = 0.01; public const double MaxProductionScale = 100.0; public const long MinDayLengthSeconds = 10L; public static CycleSolution Solve(CycleInputs inputs) { double num = Sane(inputs.CycleMinutes, 30.0, 0.5, 1440.0); double num2 = Sane(inputs.DaytimeMinutes, 21.0, 0.25, 1440.0); double num3 = Sane(inputs.NightMinutes, 9.0, 0.25, 1440.0); double num4 = Sane(inputs.ProductionScale, 1.0, 0.01, 100.0); double num7; double num8; double num6; bool flag; string note; switch (inputs.Rule) { case LengthRule.DaytimePlusNight: num7 = num2; num8 = num3; num6 = num7 + num8; flag = Differ(num6, num); note = "DaytimePlusNight: cycle = daytime + night = " + F(num7) + " + " + F(num8) + " = " + F(num6) + (flag ? ("; CycleMinutes (" + F(num) + ") ignored") : "; CycleMinutes agrees"); break; case LengthRule.CycleMinusNight: num8 = num3; num7 = num - num8; if (num7 < 0.25) { num7 = 0.25; num6 = num7 + num8; flag = true; note = "CycleMinusNight: cycle - night = " + F(num) + " - " + F(num8) + " leaves less than " + F(0.25) + " min of daytime, so daytime = " + F(num7) + " and the cycle stretches to " + F(num6); } else { num6 = num; flag = Differ(num7, num2); note = "CycleMinusNight: daytime = cycle - night = " + F(num6) + " - " + F(num8) + " = " + F(num7) + (flag ? ("; DaytimeMinutes (" + F(num2) + ") ignored") : "; DaytimeMinutes agrees"); } break; case LengthRule.CycleMinusDaytime: num7 = num2; num8 = num - num7; if (num8 < 0.25) { num8 = 0.25; num6 = num7 + num8; flag = true; note = "CycleMinusDaytime: cycle - daytime = " + F(num) + " - " + F(num7) + " leaves less than " + F(0.25) + " min of night, so night = " + F(num8) + " and the cycle stretches to " + F(num6); } else { num6 = num; flag = Differ(num8, num3); note = "CycleMinusDaytime: night = cycle - daytime = " + F(num6) + " - " + F(num7) + " = " + F(num8) + (flag ? ("; NightMinutes (" + F(num3) + ") ignored") : "; NightMinutes agrees"); } break; default: { double num5 = num2 + num3; num6 = num; num7 = num6 * num2 / num5; num8 = num6 - num7; flag = Differ(num5, num6); note = (flag ? ("FitToCycle: daytime + night as typed = " + F(num2) + " + " + F(num3) + " = " + F(num5) + ", not " + F(num6) + ", so both scale by " + F(num6) + " / " + F(num5) + " = " + F(num6 / num5, "0.####") + ": daytime = " + F(num2) + " x " + F(num6 / num5, "0.####") + " = " + F(num7) + ", night = " + F(num3) + " x " + F(num6 / num5, "0.####") + " = " + F(num8)) : ("FitToCycle: daytime + night as typed = " + F(num2) + " + " + F(num3) + " = " + F(num6) + " = CycleMinutes, nothing to adjust")); break; } } double num9 = num7 / num6; double num10 = ((inputs.Basis == ProductionBasis.RealTime) ? num4 : (num4 * 1800.0 / (60.0 * num6))); long num11 = (long)Math.Round(num10 * 60.0 * num6, MidpointRounding.AwayFromZero); if (num11 < 10) { num11 = 10L; } double clockRate = (double)num11 / (60.0 * num6); double sunrise = 0.5 - num9 / 2.0; double sunset = 0.5 + num9 / 2.0; return new CycleSolution(inputs, num6, num7, num8, flag, note, num9, num10, num11, clockRate, sunrise, sunset); } private static bool Differ(double a, double b) { return Math.Abs(a - b) > 1E-06; } private static double Sane(double value, double fallback, double min, double max) { if (double.IsNaN(value) || double.IsInfinity(value)) { return fallback; } return Math.Max(min, Math.Min(max, value)); } private static string F(double value, string format = "0.##") { return CycleSolution.F(value, format); } } [HarmonyPatch(typeof(EnvMan))] internal static class EnvManPatches { private static readonly FieldInfo? SkipTime = AccessTools.Field(typeof(EnvMan), "m_skipTime"); private static readonly FieldInfo? SkipToTime = AccessTools.Field(typeof(EnvMan), "m_skipToTime"); private static readonly FieldInfo? TimeSkipSpeed = AccessTools.Field(typeof(EnvMan), "m_timeSkipSpeed"); [HarmonyPostfix] [HarmonyPatch("Awake")] private static void Awake(EnvMan __instance) { try { Cycle.OnEnvManAwake(__instance); } catch (Exception ex) { Cycle.Fail("EnvMan.Awake", ex); } } [HarmonyPrefix] [HarmonyPatch("RescaleDayFraction")] [HarmonyPriority(0)] private static bool RescaleDayFraction(float fraction, ref float __result) { if (!Cycle.Active) { return true; } __result = Cycle.Current.Rescale(fraction); return false; } [HarmonyPrefix] [HarmonyPatch("GetMorningStartSec")] [HarmonyPriority(0)] private static bool GetMorningStartSec(EnvMan __instance, int day, ref double __result) { if (!Cycle.Active) { return true; } __result = Cycle.Current.MorningStartSeconds(__instance.m_dayLengthSec, day); return false; } [HarmonyPrefix] [HarmonyPatch("SkipToMorning")] [HarmonyPriority(0)] private static bool SkipToMorning(EnvMan __instance) { if (!Cycle.Active) { return true; } try { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || SkipTime == null || SkipToTime == null || TimeSkipSpeed == null) { throw new MissingFieldException("EnvMan", "m_skipTime / m_skipToTime / m_timeSkipSpeed"); } double timeSeconds