Decompiled source of LiveComfort v1.0.0

LiveComfort.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
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: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("LiveComfort")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Confort, abri et effet En forme recalcules en temps reel.")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+0ebe48a0d3cb31344ad9cc189d2f30b6aca504c1")]
[assembly: AssemblyProduct("Live Comfort")]
[assembly: AssemblyTitle("LiveComfort")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace LiveComfort
{
	internal static class CoverScheduler
	{
		private static Player _player;

		private static Vector3 _lastCoverPos;

		private static bool _primed;

		private static bool _lastDecisionWasMoving;

		private static float _comfortTimer;

		private static int _coverCount;

		private static int _comfortCount;

		private static long _comfortTicks;

		private static float _statsTimer;

		private static readonly Stopwatch Watch = new Stopwatch();

		private static void EnsurePlayer(Player p)
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			if (!_primed || _player != p)
			{
				_player = p;
				_lastCoverPos = ((Component)p).transform.position;
				_primed = true;
				_lastDecisionWasMoving = true;
				_comfortTimer = 0f;
			}
		}

		internal static float GetCoverInterval(Player player)
		{
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_0078: Unknown result type (might be due to invalid IL or missing references)
			//IL_007d: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)player == (Object)null || ModConfig.Enabled == null || !ModConfig.Enabled.Value)
			{
				_lastDecisionWasMoving = false;
				return 1f;
			}
			EnsurePlayer(player);
			switch (ModConfig.Mode.Value)
			{
			case ReactivityMode.EveryFrame:
				_lastDecisionWasMoving = true;
				return 0f;
			case ReactivityMode.FixedInterval:
				_lastDecisionWasMoving = true;
				return Mathf.Max(0f, ModConfig.FixedCoverInterval.Value);
			default:
			{
				Vector3 val = ((Component)player).transform.position - _lastCoverPos;
				float value = ModConfig.MoveThreshold.Value;
				if (!(_lastDecisionWasMoving = ((Vector3)(ref val)).sqrMagnitude >= value * value))
				{
					return Mathf.Max(0.05f, ModConfig.IdleCoverInterval.Value);
				}
				return Mathf.Max(0f, ModConfig.MovingCoverInterval.Value);
			}
			}
		}

		internal static void OnCoverRecomputed(Player player)
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			EnsurePlayer(player);
			_lastCoverPos = ((Component)player).transform.position;
			_coverCount++;
		}

		internal static bool ShouldRecomputeComfort(float dt)
		{
			if (!ModConfig.LiveComfortLevel.Value)
			{
				return false;
			}
			_comfortTimer += dt;
			float num = (_lastDecisionWasMoving ? Mathf.Max(0f, ModConfig.MovingComfortInterval.Value) : ModConfig.IdleComfortInterval.Value);
			if (!_lastDecisionWasMoving && num <= 0f)
			{
				return false;
			}
			if (_comfortTimer < num)
			{
				return false;
			}
			_comfortTimer = 0f;
			return true;
		}

		internal static void BeginComfort()
		{
			if (ModConfig.DebugStatsInterval.Value > 0f)
			{
				Watch.Restart();
			}
		}

		internal static void EndComfort()
		{
			if (!(ModConfig.DebugStatsInterval.Value <= 0f))
			{
				Watch.Stop();
				_comfortTicks += Watch.ElapsedTicks;
				_comfortCount++;
			}
		}

		internal static void TickStats(float dt)
		{
			float value = ModConfig.DebugStatsInterval.Value;
			if (value <= 0f)
			{
				return;
			}
			_statsTimer += dt;
			if (!(_statsTimer < value))
			{
				double num = ((_comfortCount > 0) ? ((double)_comfortTicks * 1000000.0 / (double)Stopwatch.Frequency / (double)_comfortCount) : 0.0);
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogInfo((object)$"[stats] abri {(float)_coverCount / _statsTimer:0.0}/s, confort {(float)_comfortCount / _statsTimer:0.0}/s, cout confort moyen {num:0.0} us");
				}
				_statsTimer = 0f;
				_coverCount = 0;
				_comfortCount = 0;
				_comfortTicks = 0L;
			}
		}
	}
	public enum ReactivityMode
	{
		Adaptive,
		FixedInterval,
		EveryFrame
	}
	internal static class ModConfig
	{
		internal static ConfigEntry<bool> Enabled;

		internal static ConfigEntry<ReactivityMode> Mode;

		internal static ConfigEntry<float> MovingCoverInterval;

		internal static ConfigEntry<float> IdleCoverInterval;

		internal static ConfigEntry<float> FixedCoverInterval;

		internal static ConfigEntry<float> MoveThreshold;

		internal static ConfigEntry<bool> LiveComfortLevel;

		internal static ConfigEntry<float> MovingComfortInterval;

		internal static ConfigEntry<float> IdleComfortInterval;

		internal static ConfigEntry<bool> InstantRested;

		internal static ConfigEntry<float> CozyDelay;

		internal static ConfigEntry<float> ShelterLossGrace;

		internal static ConfigEntry<bool> GradualFill;

		internal static ConfigEntry<float> FillSpeed;

		internal static ConfigEntry<bool> CapOnComfortDrop;

		internal static ConfigEntry<bool> HideSeconds;

		internal static ConfigEntry<bool> DebugLogging;

		internal static ConfigEntry<float> DebugStatsInterval;

		internal static void Init(ConfigFile cfg)
		{
			//IL_005f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0069: Expected O, but got Unknown
			//IL_0097: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Expected O, but got Unknown
			//IL_00cf: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d9: Expected O, but got Unknown
			//IL_0107: Unknown result type (might be due to invalid IL or missing references)
			//IL_0111: Expected O, but got Unknown
			//IL_015a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0164: Expected O, but got Unknown
			//IL_0192: Unknown result type (might be due to invalid IL or missing references)
			//IL_019c: Expected O, but got Unknown
			//IL_01e5: Unknown result type (might be due to invalid IL or missing references)
			//IL_01ef: Expected O, but got Unknown
			//IL_021d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0227: Expected O, but got Unknown
			//IL_0270: Unknown result type (might be due to invalid IL or missing references)
			//IL_027a: Expected O, but got Unknown
			//IL_02f9: Unknown result type (might be due to invalid IL or missing references)
			//IL_0303: Expected O, but got Unknown
			Enabled = cfg.Bind<bool>("1 - General", "Enabled", true, "Enables every change made by the mod. Unchecking restores the base game behaviour without restarting.");
			Mode = cfg.Bind<ReactivityMode>("2 - Shelter", "Mode", ReactivityMode.Adaptive, "Adaptive: high frequency only while the player moves, base game rate when standing still. FixedInterval: constant interval no matter what. EveryFrame: recomputed every physics frame (~50 Hz), the most reactive and the most expensive.");
			MovingCoverInterval = cfg.Bind<float>("2 - Shelter", "MovingInterval", 0.05f, new ConfigDescription("Minimum seconds between two shelter checks while the player moves. 0 = every physics frame (~50 Hz). This check allocates no memory.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			IdleCoverInterval = cfg.Bind<float>("2 - Shelter", "IdleInterval", 1f, new ConfigDescription("Seconds between two shelter checks while the player stands still. 1.0 = exactly the base game. Raising it to 2 or 3 makes the mod cheaper than the base game when standing still, at the cost of a slower detection if a roof is built above you.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.05f, 10f), Array.Empty<object>()));
			FixedCoverInterval = cfg.Bind<float>("2 - Shelter", "FixedInterval", 0.1f, new ConfigDescription("Used only in FixedInterval mode. 0.1 = 10 Hz.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 5f), Array.Empty<object>()));
			MoveThreshold = cfg.Bind<float>("2 - Shelter", "MoveThreshold", 0.1f, new ConfigDescription("Distance in metres travelled since the last check beyond which the player is considered moving (Adaptive mode).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.01f, 5f), Array.Empty<object>()));
			LiveComfortLevel = cfg.Bind<bool>("3 - Comfort", "LiveComfortLevel", true, "Recomputes the comfort level outside the base game's 2 second cycle.");
			MovingComfortInterval = cfg.Bind<float>("3 - Comfort", "MovingInterval", 0.1f, new ConfigDescription("Minimum seconds between two comfort recomputations while the player moves. 0.1 = 10 Hz. Do not go below 0.05: the game's furniture sort boxes an enum on every comparison, which puts the garbage collector under pressure.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 2f), Array.Empty<object>()));
			IdleComfortInterval = cfg.Bind<float>("3 - Comfort", "IdleInterval", 0.25f, new ConfigDescription("Seconds between two comfort recomputations while the player stands still. Keeps placing a piece of furniture reactive even when not moving. 0 = no recomputation, left entirely to the base game, which already does it every 2 seconds: the cheapest setting.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 30f), Array.Empty<object>()));
			InstantRested = cfg.Bind<bool>("4 - Rested", "InstantRested", true, "Removes the 10 second delay of SE_Cozy before the Rested effect is granted.");
			CozyDelay = cfg.Bind<float>("4 - Rested", "RestedDelay", 0f, new ConfigDescription("Seconds to spend in comfort before getting the Rested effect. The base game uses 10. 0 = immediate (on the next frame).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 60f), Array.Empty<object>()));
			ShelterLossGrace = cfg.Bind<float>("4 - Rested", "ShelterLossGrace", 0.5f, new ConfigDescription("Grace time before the player is considered out of shelter. Prevents the Resting effect from flickering while standing on a doorstep, which would repeat its message and instantiate its effect every frame. Also affects wind, cloth and indoor audio, which read the same InShelter(). 0 = disables this fix.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 5f), Array.Empty<object>()));
			GradualFill = cfg.Bind<bool>("5 - Timer", "GradualFill", true, "Fills the Rested timer gradually instead of making it jump to its target value. Unchecking restores the base game behaviour.");
			FillSpeed = cfg.Bind<float>("5 - Timer", "FillSpeed", 1f, new ConfigDescription("Fill speed multiplier. 1.0 = a full fill, from zero to the target value, takes exactly the base game's waiting time (10 s). 2.0 = twice as fast.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.1f, 20f), Array.Empty<object>()));
			CapOnComfortDrop = cfg.Bind<bool>("5 - Timer", "CapOnComfortDrop", true, "When comfort drops, the timer runs down naturally to the new target, then stays there. Unchecking keeps the best value reached, like the base game.");
			HideSeconds = cfg.Bind<bool>("5 - Timer", "HideSeconds", false, "Shows the Rested timer in minutes only, rounded up to the next minute (13m instead of 12:34). Unchecked, the timer keeps the base game display.");
			DebugLogging = cfg.Bind<bool>("9 - Debug", "VerboseLogging", false, "Logs every comfort level change. Used to measure latency.");
			DebugStatsInterval = cfg.Bind<float>("9 - Debug", "StatsInterval", 0f, new ConfigDescription("Seconds between two performance summaries in the log (recomputations per second, average cost of the comfort computation). 0 = disabled.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 60f), Array.Empty<object>()));
		}
	}
	[BepInPlugin("grogui.LiveComfort", "Live Comfort", "1.0.0")]
	[BepInProcess("valheim.exe")]
	public class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "grogui.LiveComfort";

		public const string PluginName = "Live Comfort";

		public const string PluginVersion = "1.0.0";

		internal static ManualLogSource Log;

		private readonly Harmony _harmony = new Harmony("grogui.LiveComfort");

		private void Awake()
		{
			Log = ((BaseUnityPlugin)this).Logger;
			ModConfig.Init(((BaseUnityPlugin)this).Config);
			_harmony.PatchAll(Assembly.GetExecutingAssembly());
			Log.LogInfo((object)"Live Comfort v1.0.0 charge.");
		}

		private void OnDestroy()
		{
			_harmony.UnpatchSelf();
		}

		internal static bool IsLocalPlayer(Player p)
		{
			if ((Object)(object)p == (Object)null)
			{
				return false;
			}
			Player localPlayer = Player.m_localPlayer;
			if ((Object)(object)localPlayer == (Object)null)
			{
				return false;
			}
			return p == localPlayer;
		}
	}
}
namespace LiveComfort.Patches
{
	[HarmonyPatch(typeof(Player), "InShelter")]
	internal static class Player_InShelter_Patch
	{
		private static Player _player;

		private static float _lastTrueAt = -999f;

		[HarmonyPostfix]
		private static void Postfix(Player __instance, ref bool __result)
		{
			if (!ModConfig.Enabled.Value)
			{
				return;
			}
			float value = ModConfig.ShelterLossGrace.Value;
			if (!(value <= 0f) && Plugin.IsLocalPlayer(__instance))
			{
				if (_player != __instance)
				{
					_player = __instance;
					_lastTrueAt = -999f;
				}
				if (__result)
				{
					_lastTrueAt = Time.time;
				}
				else if (Time.time - _lastTrueAt < value)
				{
					__result = true;
				}
			}
		}
	}
	[HarmonyPatch(typeof(Player), "UpdateCover")]
	internal static class Player_UpdateCover_Patch
	{
		private static readonly MethodInfo IntervalGetter = AccessTools.Method(typeof(CoverScheduler), "GetCoverInterval", (Type[])null, (Type[])null);

		[HarmonyTranspiler]
		private static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
		{
			//IL_0076: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Expected O, but got Unknown
			List<CodeInstruction> list = new List<CodeInstruction>(instructions);
			for (int i = 0; i < list.Count; i++)
			{
				if (!(list[i].opcode != OpCodes.Ldc_R4) && list[i].operand is float num && num == 1f)
				{
					list[i].opcode = OpCodes.Ldarg_0;
					list[i].operand = null;
					list.Insert(i + 1, new CodeInstruction(OpCodes.Call, (object)IntervalGetter));
					ManualLogSource log = Plugin.Log;
					if (log != null)
					{
						log.LogInfo((object)"Player.UpdateCover : seuil d'abri rendu configurable.");
					}
					return list;
				}
			}
			ManualLogSource log2 = Plugin.Log;
			if (log2 != null)
			{
				log2.LogError((object)"Player.UpdateCover : constante 1f introuvable, le seuil reste a 1 s. Le jeu a probablement ete mis a jour ; le reste du mod continue de fonctionner.");
			}
			return list;
		}

		[HarmonyPostfix]
		private static void Postfix(Player __instance, float dt, float ___m_updateCoverTimer, ref int ___m_comfortLevel)
		{
			if (!ModConfig.Enabled.Value || !Plugin.IsLocalPlayer(__instance))
			{
				return;
			}
			if (___m_updateCoverTimer == 0f)
			{
				CoverScheduler.OnCoverRecomputed(__instance);
			}
			CoverScheduler.TickStats(dt);
			if (!CoverScheduler.ShouldRecomputeComfort(dt))
			{
				return;
			}
			CoverScheduler.BeginComfort();
			int num = SE_Rested.CalculateComfortLevel(__instance);
			CoverScheduler.EndComfort();
			if (ModConfig.DebugLogging.Value && num != ___m_comfortLevel)
			{
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogInfo((object)$"confort {___m_comfortLevel} -> {num} | abri={__instance.InShelter()}");
				}
			}
			___m_comfortLevel = num;
		}
	}
	[HarmonyPatch(typeof(SE_Cozy), "UpdateStatusEffect")]
	internal static class SE_Cozy_Patch
	{
		internal static float VanillaDelay = 10f;

		private static bool _captured;

		[HarmonyPrefix]
		[HarmonyPriority(800)]
		private static void Prefix(SE_Cozy __instance)
		{
			if (!_captured)
			{
				if (__instance.m_delay > 0f)
				{
					VanillaDelay = __instance.m_delay;
				}
				_captured = true;
				ManualLogSource log = Plugin.Log;
				if (log != null)
				{
					log.LogInfo((object)("SE_Cozy : delai du jeu de base = " + VanillaDelay + " s (sert aussi de duree de remplissage du minuteur)."));
				}
			}
			float num = ((ModConfig.Enabled.Value && ModConfig.InstantRested.Value) ? Mathf.Max(0f, ModConfig.CozyDelay.Value) : VanillaDelay);
			if (__instance.m_delay != num)
			{
				__instance.m_delay = num;
			}
		}
	}
	internal static class RestedTimer
	{
		internal static bool ShouldManage(StatusEffect se)
		{
			if (!ModConfig.Enabled.Value || !ModConfig.GradualFill.Value)
			{
				return false;
			}
			Character character = se.m_character;
			Player val = (Player)(object)((character is Player) ? character : null);
			if (!Plugin.IsLocalPlayer(val))
			{
				return false;
			}
			return ((Character)val).GetSEMan().HaveStatusEffect(SEMan.s_statusEffectResting);
		}
	}
	[HarmonyPatch(typeof(SE_Rested), "UpdateTTL")]
	internal static class SE_Rested_UpdateTTL_Patch
	{
		[HarmonyPrefix]
		private static bool Prefix(SE_Rested __instance)
		{
			return !RestedTimer.ShouldManage((StatusEffect)(object)__instance);
		}
	}
	[HarmonyPatch(typeof(SE_Rested), "Setup")]
	internal static class SE_Rested_Setup_Patch
	{
		[HarmonyPostfix]
		private static void Postfix(SE_Rested __instance, ref float ___m_time)
		{
			if (RestedTimer.ShouldManage((StatusEffect)(object)__instance))
			{
				((StatusEffect)__instance).m_ttl = 1f;
				___m_time = 0f;
			}
		}
	}
	[HarmonyPatch(typeof(SE_Rested), "UpdateStatusEffect")]
	internal static class SE_Rested_UpdateStatusEffect_Patch
	{
		[HarmonyPostfix]
		private static void Postfix(SE_Rested __instance, float dt, ref float ___m_time)
		{
			if (!RestedTimer.ShouldManage((StatusEffect)(object)__instance))
			{
				return;
			}
			Character character = ((StatusEffect)__instance).m_character;
			Player val = (Player)(object)((character is Player) ? character : null);
			if (!((Object)(object)val == (Object)null))
			{
				int num = Mathf.Max(1, val.GetComfortLevel());
				float num2 = __instance.m_baseTTL + (float)(num - 1) * __instance.m_TTLPerComfortLevel;
				float num3 = ((StatusEffect)__instance).m_ttl - ___m_time;
				if (num3 < num2)
				{
					float num4 = Mathf.Max(0.05f, SE_Cozy_Patch.VanillaDelay / ModConfig.FillSpeed.Value);
					float num5 = num2 / num4;
					num3 = Mathf.Min(num2, num3 + (num5 + 1f) * dt);
				}
				else if (!ModConfig.CapOnComfortDrop.Value)
				{
					return;
				}
				((StatusEffect)__instance).m_ttl = num3;
				___m_time = 0f;
			}
		}
	}
	[HarmonyPatch(typeof(StatusEffect), "GetIconText")]
	internal static class StatusEffect_GetIconText_Patch
	{
		[HarmonyPostfix]
		private static void Postfix(StatusEffect __instance, ref string __result)
		{
			if (__instance is SE_Rested && ModConfig.Enabled.Value && ModConfig.HideSeconds.Value && !string.IsNullOrEmpty(__result) && Plugin.IsLocalPlayer((Player)/*isinst with value type is only supported in some contexts*/))
			{
				__result = Mathf.CeilToInt(__instance.GetRemaningTime() / 60f) + "m";
			}
		}
	}
}