using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("TimeControl")] [assembly: AssemblyDescription("Adjusts the Valheim day/night cycle length via a multiplier, with server-authoritative sync and an exact mod-version gate.")] [assembly: AssemblyProduct("TimeControl")] [assembly: AssemblyCompany("drummercraig")] [assembly: ComVisible(false)] [assembly: AssemblyFileVersion("1.0.1")] [assembly: AssemblyVersion("1.0.1.0")] namespace TimeControl; [BepInPlugin("drummercraig.time_control", "TimeControl", "1.0.1")] public class TimeControlPlugin : BaseUnityPlugin { public const string PluginGuid = "drummercraig.time_control"; public const string PluginName = "TimeControl"; public const string PluginVersion = "1.0.1"; private void Awake() { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown TimeControlConfig.Log = ((BaseUnityPlugin)this).Logger; TimeControlConfig.Bind(((BaseUnityPlugin)this).Config); ResilientPatcher.ApplyAll(new Harmony("drummercraig.time_control"), Assembly.GetExecutingAssembly(), ((BaseUnityPlugin)this).Logger); ((BaseUnityPlugin)this).Logger.LogInfo((object)(string.Format("[TimeControl] v{0} loaded. DayLengthMultiplier={1:0.###} ", "1.0.1", TimeControlConfig.DayLengthMultiplier.Value) + "(local config; server value takes over when connected to a server).")); } } public static class TimeControlConfig { public static ManualLogSource Log; public static ConfigEntry DayLengthMultiplier; public const float MinMultiplier = 0.1f; public const float MaxMultiplier = 100f; public static float ServerMultiplier = -1f; public static float EffectiveMultiplier => Mathf.Clamp((ServerMultiplier >= 0f) ? ServerMultiplier : DayLengthMultiplier.Value, 0.1f, 100f); public static void Bind(ConfigFile config) { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Expected O, but got Unknown DayLengthMultiplier = config.Bind("General", "DayLengthMultiplier", 4f, new ConfigDescription("Multiplier applied to the in-game day/night cycle length. 1.0 = vanilla (a full day is ~30 real minutes). 2.0 = days take twice as long; 4.0 = ~2 real hours per full day. Day and night both scale together (their ratio is fixed by Valheim and is preserved). When you join a server running TimeControl, the server's value is used instead of this one for that session, so everyone shares the same clock. Range: " + 0.1f + " to " + 100f + ".", (AcceptableValueBase)(object)new AcceptableValueRange(0.1f, 100f), Array.Empty())); DayLengthMultiplier.SettingChanged += delegate { DayLength.ApplyEffectiveMultiplier(); }; } } public static class DayLength { [HarmonyPatch(typeof(EnvMan), "Awake")] private static class EnvManAwakePatch { private static void Postfix() { ApplyEffectiveMultiplier(); } } private static long _vanillaBaseSec = -1L; public static bool HasBase => _vanillaBaseSec > 0; public static long VanillaBaseSec => _vanillaBaseSec; public static void ApplyEffectiveMultiplier() { try { EnvMan instance = EnvMan.instance; if ((Object)(object)instance == (Object)null) { return; } if (_vanillaBaseSec < 0) { _vanillaBaseSec = instance.m_dayLengthSec; } float effectiveMultiplier = TimeControlConfig.EffectiveMultiplier; long num = (long)((float)_vanillaBaseSec * effectiveMultiplier); if (num < 1) { num = 1L; } if (instance.m_dayLengthSec != num) { instance.m_dayLengthSec = num; ManualLogSource log = TimeControlConfig.Log; if (log != null) { log.LogInfo((object)($"[TimeControl] Day length set to {num}s " + $"(vanilla base {_vanillaBaseSec}s x {effectiveMultiplier:0.###}).")); } } } catch (Exception arg) { ManualLogSource log2 = TimeControlConfig.Log; if (log2 != null) { log2.LogError((object)$"[TimeControl] ApplyEffectiveMultiplier failed: {arg}"); } } } } public static class NetworkSync { [HarmonyPatch(typeof(ZNet), "OnNewConnection", new Type[] { typeof(ZNetPeer) })] private static class OnNewConnectionPatch { private static void Postfix(ZNetPeer peer, ZNet __instance) { try { if (__instance.IsServer()) { peer.m_rpc.Register("TimeControl_Version", (Action)OnServerReceiveClientVersion); peer.m_rpc.Invoke("TimeControl_Version", new object[1] { "1.0.1" }); } else { _serverSentVersion = false; peer.m_rpc.Register("TimeControl_Version", (Action)OnClientReceiveServerVersion); peer.m_rpc.Register("TimeControl_DayLength", (Action)OnClientReceiveDayLength); peer.m_rpc.Invoke("TimeControl_Version", new object[1] { "1.0.1" }); } } catch (Exception arg) { ManualLogSource log = TimeControlConfig.Log; if (log != null) { log.LogError((object)$"[TimeControl] OnNewConnection handling failed: {arg}"); } } } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] private static class RpcPeerInfoPatch { private static bool Prefix(ZRpc rpc, ZNet __instance) { //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Invalid comparison between Unknown and I4 try { if (__instance.IsServer()) { if (!_validatedPeers.Contains(rpc)) { ManualLogSource log = TimeControlConfig.Log; if (log != null) { log.LogWarning((object)"[TimeControl] Disconnecting client without a matching TimeControl version."); } rpc.Invoke("Error", new object[1] { 3 }); return false; } SendDayLengthToPeer(rpc); return true; } if (!_serverSentVersion) { ManualLogSource log2 = TimeControlConfig.Log; if (log2 != null) { log2.LogWarning((object)"[TimeControl] Server is not running TimeControl. Disconnecting (incompatible version)."); } if ((int)ZNet.GetConnectionStatus() != 3) { SetClientConnectionError((ConnectionStatus)3); } if ((Object)(object)Game.instance != (Object)null) { Game.instance.Logout(true, true); } return false; } return true; } catch (Exception arg) { ManualLogSource log3 = TimeControlConfig.Log; if (log3 != null) { log3.LogError((object)$"[TimeControl] RPC_PeerInfo gate failed, allowing vanilla handshake: {arg}"); } return true; } } private static void SendDayLengthToPeer(ZRpc rpc) { try { rpc.Invoke("TimeControl_DayLength", new object[1] { TimeControlConfig.EffectiveMultiplier }); } catch (Exception ex) { ManualLogSource log = TimeControlConfig.Log; if (log != null) { log.LogWarning((object)("[TimeControl] Failed to push day length: " + ex.Message)); } } } } private const string RpcVersion = "TimeControl_Version"; private const string RpcDayLength = "TimeControl_DayLength"; private static readonly HashSet _validatedPeers = new HashSet(); private static bool _serverSentVersion; private static void OnServerReceiveClientVersion(ZRpc rpc, string clientVersion) { try { if (string.Equals(clientVersion, "1.0.1", StringComparison.Ordinal)) { _validatedPeers.Add(rpc); ManualLogSource log = TimeControlConfig.Log; if (log != null) { log.LogInfo((object)("[TimeControl] Client presented matching version " + clientVersion + ".")); } } else { ManualLogSource log2 = TimeControlConfig.Log; if (log2 != null) { log2.LogWarning((object)("[TimeControl] Client version '" + clientVersion + "' != server '1.0.1'. Will be disconnected at peer-info handshake.")); } } } catch (Exception arg) { ManualLogSource log3 = TimeControlConfig.Log; if (log3 != null) { log3.LogError((object)$"[TimeControl] OnServerReceiveClientVersion failed: {arg}"); } } } private static void OnClientReceiveServerVersion(ZRpc rpc, string serverVersion) { try { _serverSentVersion = true; if (!string.Equals(serverVersion, "1.0.1", StringComparison.Ordinal)) { ManualLogSource log = TimeControlConfig.Log; if (log != null) { log.LogWarning((object)("[TimeControl] Server version '" + serverVersion + "' != client '1.0.1'. Disconnecting (incompatible version).")); } } else { ManualLogSource log2 = TimeControlConfig.Log; if (log2 != null) { log2.LogInfo((object)("[TimeControl] Server version " + serverVersion + " matches.")); } } } catch (Exception arg) { ManualLogSource log3 = TimeControlConfig.Log; if (log3 != null) { log3.LogError((object)$"[TimeControl] OnClientReceiveServerVersion failed: {arg}"); } } } private static void OnClientReceiveDayLength(ZRpc rpc, float serverMultiplier) { try { TimeControlConfig.ServerMultiplier = serverMultiplier; ManualLogSource log = TimeControlConfig.Log; if (log != null) { log.LogInfo((object)$"[TimeControl] Adopting server day-length multiplier {serverMultiplier:0.###} for this session."); } DayLength.ApplyEffectiveMultiplier(); } catch (Exception arg) { ManualLogSource log2 = TimeControlConfig.Log; if (log2 != null) { log2.LogError((object)$"[TimeControl] OnClientReceiveDayLength failed: {arg}"); } } } private static void SetClientConnectionError(ConnectionStatus status) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) AccessTools.Field(typeof(ZNet), "m_connectionStatus")?.SetValue(null, status); } } internal static class ResilientPatcher { internal static void ApplyAll(Harmony harmony, Assembly assembly, ManualLogSource log) { if (harmony == null || assembly == null) { return; } int num = 0; int num2 = 0; List list = null; Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(assembly); foreach (Type type in typesFromAssembly) { try { List list2 = harmony.CreateClassProcessor(type).Patch(); if (list2 != null && list2.Count > 0) { num++; if (log != null) { log.LogDebug((object)$"[TimeControl] patched {type.Name} ({list2.Count} method(s))."); } } } catch (Exception ex) { num2++; (list ?? (list = new List())).Add(type.Name); if (log != null) { log.LogWarning((object)("[TimeControl] SKIP patch class " + type.Name + ": " + ex.Message)); } } } if (num2 == 0) { if (log != null) { log.LogInfo((object)$"[TimeControl] patches: {num} applied, 0 skipped."); } } else if (log != null) { log.LogWarning((object)(string.Format("[TimeControl] patches: {0} applied, {1} skipped ({2}). ", num, num2, string.Join(", ", list)) + "A skipped patch usually means a Valheim update changed a hooked method; that feature is disabled but the mod is still running.")); } } }