Decompiled source of TimeControl v1.2.1

plugins/TimeControl.dll

Decompiled 2 months ago
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.2.1")]
[assembly: AssemblyVersion("1.2.1.0")]
namespace TimeControl;

[BepInPlugin("drummercraig.time_control", "TimeControl", "1.2.1")]
public class TimeControlPlugin : BaseUnityPlugin
{
	public const string PluginGuid = "drummercraig.time_control";

	public const string PluginName = "TimeControl";

	public const string PluginVersion = "1.2.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);
		((Component)this).gameObject.AddComponent<ClockHud>();
		((BaseUnityPlugin)this).Logger.LogInfo((object)(string.Format("[TimeControl] v{0} loaded. DayLengthMultiplier={1:0.###} ", "1.2.1", TimeControlConfig.DayLengthMultiplier.Value) + "(local config; server value takes over when connected to a server)."));
	}
}
public enum ClockTextColor
{
	White,
	Yellow,
	Orange,
	Red,
	Green,
	Cyan,
	Blue,
	Pink,
	Black,
	Gray
}
public static class TimeControlConfig
{
	public static ManualLogSource Log;

	public static ConfigEntry<float> DayLengthMultiplier;

	public const float MinMultiplier = 0.1f;

	public const float MaxMultiplier = 100f;

	public static ConfigEntry<bool> ShowClock;

	public static ConfigEntry<ClockTextColor> ClockColor;

	public static ConfigEntry<string> ClockColorHex;

	public static ConfigEntry<int> ClockFontSize;

	public static ConfigEntry<FontStyle> ClockFontStyle;

	public static ConfigEntry<string> ClockFont;

	public const string DefaultFontName = "Default";

	public static float ServerMultiplier = -1f;

	private static readonly string[] FontChoices = new string[7] { "Default", "Arial", "Verdana", "Tahoma", "Georgia", "Times New Roman", "Courier New" };

	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
		//IL_0121: Unknown result type (might be due to invalid IL or missing references)
		//IL_012b: Expected O, but got Unknown
		//IL_016f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0179: Expected O, but got Unknown
		DayLengthMultiplier = config.Bind<float>("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<float>(0.1f, 100f), Array.Empty<object>()));
		DayLengthMultiplier.SettingChanged += delegate
		{
			DayLength.ApplyEffectiveMultiplier();
		};
		ShowClock = config.Bind<bool>("General", "ShowClock", true, "Show an on-screen clock in the top-center of the screen that displays the current time of day as a word (Midnight, Night, Dawn, Morning, Midday, Afternoon, Dusk). Display-only and per-client; does not affect the day/night cycle.");
		ClockColor = config.Bind<ClockTextColor>("Clock Appearance", "TextColor", ClockTextColor.White, "Color of the on-screen clock text. Ignored when TextColorHex below is set to a valid hex color.");
		ClockColorHex = config.Bind<string>("Clock Appearance", "TextColorHex", "", "Optional custom clock text color as a hex string (e.g. #FF8800). When set to a valid value this overrides TextColor above. Leave blank to use the TextColor palette.");
		ClockFontSize = config.Bind<int>("Clock Appearance", "FontSize", 16, new ConfigDescription("Font size of the on-screen clock text.", (AcceptableValueBase)(object)new AcceptableValueList<int>(new int[7] { 12, 14, 16, 18, 20, 24, 28 }), Array.Empty<object>()));
		ClockFontStyle = config.Bind<FontStyle>("Clock Appearance", "FontStyle", (FontStyle)1, "Style of the on-screen clock text: Normal, Bold, Italic, or BoldAndItalic.");
		ClockFont = config.Bind<string>("Clock Appearance", "Font", "Default", new ConfigDescription("Font family for the on-screen clock. \"Default\" uses the built-in game UI font. The other entries are common fonts that exist on most systems; if a chosen font is not installed on your machine, the clock falls back to the default font.", (AcceptableValueBase)(object)new AcceptableValueList<string>(FontChoices), Array.Empty<object>()));
	}
}
internal class ClockHud : MonoBehaviour
{
	private static readonly float[] Thresholds = new float[9] { 0.03f, 0.2f, 0.3f, 0.45f, 0.55f, 0.7f, 0.8f, 0.97f, 1.01f };

	private static readonly string[] Words = new string[9] { "Midnight", "Night", "Dawn", "Morning", "Midday", "Afternoon", "Dusk", "Night", "Midnight" };

	private const float LabelWidth = 300f;

	private const float LabelTop = 12f;

	private const float LabelHeight = 30f;

	private GUIStyle _style;

	private int _lastBucket = -1;

	private string _cachedText = "";

	private bool _errorLogged;

	private Color _resolvedColor = Color.white;

	private Font _customFont;

	private ClockTextColor _lastColor = (ClockTextColor)(-1);

	private string _lastHex;

	private int _lastSize = -1;

	private FontStyle _lastStyle = (FontStyle)(-1);

	private string _lastFont;

	private void OnGUI()
	{
		//IL_0113: Unknown result type (might be due to invalid IL or missing references)
		//IL_0092: Unknown result type (might be due to invalid IL or missing references)
		//IL_0097: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ac: Unknown result type (might be due to invalid IL or missing references)
		//IL_00de: Unknown result type (might be due to invalid IL or missing references)
		//IL_00f5: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ff: Unknown result type (might be due to invalid IL or missing references)
		try
		{
			if (!TimeControlConfig.ShowClock.Value)
			{
				return;
			}
			EnvMan instance = EnvMan.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			if (_style == null)
			{
				BuildStyle();
			}
			if (AppearanceChanged())
			{
				ApplyAppearance();
			}
			int num = BucketFor(instance.GetDayFraction());
			if (num != _lastBucket)
			{
				_lastBucket = num;
				_cachedText = Words[num];
			}
			Rect val = default(Rect);
			((Rect)(ref val))..ctor(((float)Screen.width - 300f) * 0.5f, 12f, 300f, 30f);
			Color color = GUI.color;
			try
			{
				GUI.color = new Color(0f, 0f, 0f, 0.6f);
				GUI.Label(new Rect(((Rect)(ref val)).x + 1f, ((Rect)(ref val)).y + 1f, ((Rect)(ref val)).width, ((Rect)(ref val)).height), _cachedText, _style);
				GUI.color = _resolvedColor;
				GUI.Label(val, _cachedText, _style);
			}
			finally
			{
				GUI.color = color;
			}
		}
		catch (Exception arg)
		{
			if (!_errorLogged)
			{
				_errorLogged = true;
				ManualLogSource log = TimeControlConfig.Log;
				if (log != null)
				{
					log.LogError((object)$"[TimeControl] ClockHud.OnGUI failed (further errors suppressed): {arg}");
				}
			}
		}
	}

	private static int BucketFor(float fraction)
	{
		float num = Mathf.Clamp(fraction, 0f, 0.99999f);
		for (int i = 0; i < Thresholds.Length; i++)
		{
			if (num < Thresholds[i])
			{
				return i;
			}
		}
		return Words.Length - 1;
	}

	private void BuildStyle()
	{
		//IL_000b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0010: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Expected O, but got Unknown
		//IL_0027: Unknown result type (might be due to invalid IL or missing references)
		_style = new GUIStyle(GUI.skin.label)
		{
			alignment = (TextAnchor)4
		};
		_style.normal.textColor = Color.white;
	}

	private bool AppearanceChanged()
	{
		//IL_0026: Unknown result type (might be due to invalid IL or missing references)
		//IL_002b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0088: Unknown result type (might be due to invalid IL or missing references)
		//IL_0089: Unknown result type (might be due to invalid IL or missing references)
		//IL_0058: Unknown result type (might be due to invalid IL or missing references)
		//IL_005a: Unknown result type (might be due to invalid IL or missing references)
		ClockTextColor value = TimeControlConfig.ClockColor.Value;
		string value2 = TimeControlConfig.ClockColorHex.Value;
		int value3 = TimeControlConfig.ClockFontSize.Value;
		FontStyle value4 = TimeControlConfig.ClockFontStyle.Value;
		string value5 = TimeControlConfig.ClockFont.Value;
		if (value == _lastColor && value2 == _lastHex && value3 == _lastSize && value4 == _lastStyle && value5 == _lastFont)
		{
			return false;
		}
		_lastColor = value;
		_lastHex = value2;
		_lastSize = value3;
		_lastStyle = value4;
		_lastFont = value5;
		return true;
	}

	private void ApplyAppearance()
	{
		//IL_0036: Unknown result type (might be due to invalid IL or missing references)
		//IL_003b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0062: Unknown result type (might be due to invalid IL or missing references)
		//IL_0023: Unknown result type (might be due to invalid IL or missing references)
		//IL_0024: Unknown result type (might be due to invalid IL or missing references)
		string value = TimeControlConfig.ClockColorHex.Value;
		Color resolvedColor = default(Color);
		if (!string.IsNullOrEmpty(value) && ColorUtility.TryParseHtmlString(value.Trim(), ref resolvedColor))
		{
			_resolvedColor = resolvedColor;
		}
		else
		{
			_resolvedColor = MapColor(TimeControlConfig.ClockColor.Value);
		}
		int value2 = TimeControlConfig.ClockFontSize.Value;
		_style.fontSize = value2;
		_style.fontStyle = TimeControlConfig.ClockFontStyle.Value;
		if ((Object)(object)_customFont != (Object)null)
		{
			Object.Destroy((Object)(object)_customFont);
			_customFont = null;
		}
		string value3 = TimeControlConfig.ClockFont.Value;
		if (string.IsNullOrEmpty(value3) || value3 == "Default")
		{
			_style.font = null;
			return;
		}
		Font val = Font.CreateDynamicFontFromOSFont(value3, value2);
		if ((Object)(object)val == (Object)null)
		{
			ManualLogSource log = TimeControlConfig.Log;
			if (log != null)
			{
				log.LogWarning((object)("[TimeControl] Clock font '" + value3 + "' unavailable on this machine; using default."));
			}
			_style.font = null;
		}
		else
		{
			_customFont = val;
			_style.font = val;
		}
	}

	private static Color MapColor(ClockTextColor c)
	{
		//IL_002e: Unknown result type (might be due to invalid IL or missing references)
		//IL_0043: Unknown result type (might be due to invalid IL or missing references)
		//IL_0049: Unknown result type (might be due to invalid IL or missing references)
		//IL_004f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0055: Unknown result type (might be due to invalid IL or missing references)
		//IL_006a: Unknown result type (might be due to invalid IL or missing references)
		//IL_007f: Unknown result type (might be due to invalid IL or missing references)
		//IL_0085: Unknown result type (might be due to invalid IL or missing references)
		//IL_008b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0091: Unknown result type (might be due to invalid IL or missing references)
		return (Color)(c switch
		{
			ClockTextColor.Yellow => Color.yellow, 
			ClockTextColor.Orange => new Color(1f, 0.55f, 0f), 
			ClockTextColor.Red => Color.red, 
			ClockTextColor.Green => Color.green, 
			ClockTextColor.Cyan => Color.cyan, 
			ClockTextColor.Blue => new Color(0.3f, 0.5f, 1f), 
			ClockTextColor.Pink => new Color(1f, 0.45f, 0.75f), 
			ClockTextColor.Black => Color.black, 
			ClockTextColor.Gray => Color.gray, 
			_ => Color.white, 
		});
	}
}
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<string>("TimeControl_Version", (Action<ZRpc, string>)OnServerReceiveClientVersion);
					peer.m_rpc.Invoke("TimeControl_Version", new object[1] { "1.2.1" });
				}
				else
				{
					_serverSentVersion = false;
					peer.m_rpc.Register<string>("TimeControl_Version", (Action<ZRpc, string>)OnClientReceiveServerVersion);
					peer.m_rpc.Register<float>("TimeControl_DayLength", (Action<ZRpc, float>)OnClientReceiveDayLength);
					peer.m_rpc.Invoke("TimeControl_Version", new object[1] { "1.2.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<ZRpc> _validatedPeers = new HashSet<ZRpc>();

	private static bool _serverSentVersion;

	private static void OnServerReceiveClientVersion(ZRpc rpc, string clientVersion)
	{
		try
		{
			if (string.Equals(clientVersion, "1.2.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.2.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.2.1", StringComparison.Ordinal))
			{
				ManualLogSource log = TimeControlConfig.Log;
				if (log != null)
				{
					log.LogWarning((object)("[TimeControl] Server version '" + serverVersion + "' != client '1.2.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<string> list = null;
		Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(assembly);
		foreach (Type type in typesFromAssembly)
		{
			try
			{
				List<MethodInfo> 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<string>())).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."));
		}
	}
}