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 BaseFood v0.2.0
BepInEx/plugins/BaseFood/BaseFood.dll
Decompiled 3 hours agousing 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 Microsoft.CodeAnalysis; 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: InternalsVisibleTo("PatchTests")] [assembly: AssemblyCompany("BaseFood")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.0.0")] [assembly: AssemblyInformationalVersion("0.2.0")] [assembly: AssemblyProduct("BaseFood")] [assembly: AssemblyTitle("BaseFood")] [assembly: AssemblyVersion("0.2.0.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 BaseFood { internal sealed class BaseDetector { private readonly bool requireAll; private readonly ProtectionClock protection = new ProtectionClock(); private readonly TrackedStructure?[] cached = new TrackedStructure[3]; private float nextDiscovery; private float nextValidation; private bool searching; private int phase; private int cursor; private int searchRevision; internal BaseDetector(bool requireAll = true) { this.requireAll = requireAll; } internal bool IsProtected(float now) { return protection.IsProtected(now); } internal void Reset() { protection.Reset(); for (int i = 0; i < cached.Length; i++) { cached[i] = null; } nextDiscovery = (nextValidation = 0f); searching = false; phase = (cursor = 0); } internal void Tick(Vector3 position, float now, float radius, float grace, float discoveryInterval, float validationInterval, int budget) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_01bb: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Unknown result type (might be due to invalid IL or missing references) float radiusSquared = radius * radius; if (now >= nextValidation) { Validate(position, radiusSquared, now, grace); nextValidation = now + validationInterval; } if (!searching && now >= nextDiscovery) { nextDiscovery = now + discoveryInterval; if (!Validate(position, radiusSquared, now, grace)) { searching = true; phase = (cursor = 0); searchRevision = StructureRegistry.Revision; } } if (!searching) { return; } if (searchRevision != StructureRegistry.Revision) { phase = (cursor = 0); searchRevision = StructureRegistry.Revision; } while (phase < 3 && budget > 0) { TrackedStructure trackedStructure = cached[phase]; if (Object.op_Implicit((Object)(object)trackedStructure) && trackedStructure.Qualifies(position, radiusSquared)) { phase = (requireAll ? (phase + 1) : 3); cursor = 0; budget--; continue; } cached[phase] = null; List<TrackedStructure> list = StructureRegistry.Lists[phase]; if (cursor >= list.Count) { phase++; cursor = 0; continue; } trackedStructure = list[cursor++]; budget--; if (Object.op_Implicit((Object)(object)trackedStructure) && trackedStructure.Qualifies(position, radiusSquared)) { cached[phase] = trackedStructure; phase = (requireAll ? (phase + 1) : 3); cursor = 0; } } if (phase >= 3) { searching = false; nextDiscovery = now + discoveryInterval; Validate(position, radiusSquared, now, grace); } } private bool Validate(Vector3 position, float radiusSquared, float now, float grace) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) bool flag = true; bool flag2 = false; for (int i = 0; i < cached.Length; i++) { TrackedStructure trackedStructure = cached[i]; if (!Object.op_Implicit((Object)(object)trackedStructure) || !trackedStructure.Qualifies(position, radiusSquared)) { cached[i] = null; flag = false; } else { flag2 = true; } } bool flag3 = (requireAll ? flag : flag2); if (!requireAll && protection.ConditionsMet && !flag3) { nextDiscovery = now; } protection.Observe(flag3, now, grace); return flag3; } } internal sealed class ProtectionClock { private double graceUntil = double.NegativeInfinity; public bool ConditionsMet { get; private set; } public void Observe(bool valid, double now, double graceSeconds) { if (ConditionsMet && !valid) { graceUntil = now + Math.Max(0.0, graceSeconds); } if (valid) { graceUntil = double.NegativeInfinity; } ConditionsMet = valid; } public bool IsProtected(double now) { if (!ConditionsMet) { return now < graceUntil; } return true; } public void Reset() { ConditionsMet = false; graceUntil = double.NegativeInfinity; } } internal static class FoodRules { public static float Multiplier(bool enabled, bool protectedAtBase, bool rested, float reductionPercent) { if (!enabled) { return 1f; } if (protectedAtBase) { return 0f; } if (!rested) { return 1f; } return 1f - Math.Min(100f, Math.Max(0f, reductionPercent)) / 100f; } } internal static class DiscoveryBudget { internal static void Split(int total, bool nearbyEnabled, ref bool extraToNearby, out int baseBudget, out int nearbyBudget) { nearbyBudget = (nearbyEnabled ? (total / 2) : 0); if ((nearbyEnabled && (total & 1) != 0) & extraToNearby) { nearbyBudget++; } extraToNearby = !extraToNearby; baseBudget = total - nearbyBudget; } } [HarmonyPatch(typeof(Player), "UpdateFood", new Type[] { typeof(float), typeof(bool) })] internal static class FoodTimerPatch { internal static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions) { //IL_00d0: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Expected O, but got Unknown //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0146: Expected O, but got Unknown List<CodeInstruction> list = new List<CodeInstruction>(instructions); FieldInfo fieldInfo = AccessTools.Field(typeof(Food), "m_time"); MethodInfo methodInfo = AccessTools.Method(typeof(FoodTimerPatch), "ScaleCountdown", (Type[])null, (Type[])null); int num = -1; int num2 = 0; for (int i = 0; i + 3 < list.Count; i++) { if (CodeInstructionExtensions.LoadsField(list[i], fieldInfo, false) && IsLoadLocal(list[i + 1]) && list[i + 2].opcode == OpCodes.Sub && CodeInstructionExtensions.StoresField(list[i + 3], fieldInfo)) { num = i + 2; num2++; } } if (num2 != 1) { throw new InvalidOperationException("Expected exactly one food countdown subtraction; found " + num2 + ". BaseFood must be updated for this game/mod combination."); } CodeInstruction val = new CodeInstruction(OpCodes.Ldarg_0, (object)null); val.labels.AddRange(list[num].labels); list[num].labels.Clear(); val.blocks.AddRange(list[num].blocks); list[num].blocks.Clear(); list.Insert(num, val); list.Insert(num + 1, new CodeInstruction(OpCodes.Call, (object)methodInfo)); ManualLogSource? log = Plugin.Log; if (log != null) { log.LogInfo((object)"Food countdown patch verified: exactly one subtraction modified; healing clock untouched."); } return list; } private static bool IsLoadLocal(CodeInstruction instruction) { if (!(instruction.opcode == OpCodes.Ldloc_0) && !(instruction.opcode == OpCodes.Ldloc_1) && !(instruction.opcode == OpCodes.Ldloc_2) && !(instruction.opcode == OpCodes.Ldloc_3) && !(instruction.opcode == OpCodes.Ldloc)) { return instruction.opcode == OpCodes.Ldloc_S; } return true; } private static float ScaleCountdown(float vanillaCountdown, Player player) { return vanillaCountdown * (Plugin.Instance?.GetMultiplier(player) ?? 1f); } } [HarmonyPatch(typeof(Player), "TeleportTo")] internal static class TeleportResetPatch { private static void Postfix(Player __instance, bool __result) { if (__result) { Plugin.Instance?.ClearProtection(__instance); } } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class DeathResetPatch { private static void Prefix(Player __instance) { Plugin.Instance?.ClearProtection(__instance); } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class SpawnResetPatch { private static void Postfix(Player __instance) { Plugin.Instance?.ClearProtection(__instance); } } [BepInPlugin("local.basefood", "BaseFood", "0.2.0")] public sealed class Plugin : BaseUnityPlugin { public const string Id = "local.basefood"; private Harmony? harmony; private readonly BaseDetector detector = new BaseDetector(); private readonly BaseDetector nearbyDetector = new BaseDetector(requireAll: false); private readonly HashSet<string> allowedFires = new HashSet<string>(StringComparer.Ordinal); private Player? currentPlayer; private bool suspended; private bool runtimeErrorLogged; private bool lastProtection; private bool extraBudgetToNearby; private static readonly int RestedHash = StringExtensionMethods.GetStableHashCode("Rested"); internal ConfigEntry<bool> Enabled; internal ConfigEntry<float> Radius; internal ConfigEntry<float> GraceSeconds; internal ConfigEntry<float> DiscoverySeconds; internal ConfigEntry<float> ValidationSeconds; internal ConfigEntry<int> CandidatesPerFrame; internal ConfigEntry<float> RestedReduction; internal ConfigEntry<bool> NearbyEnabled; internal ConfigEntry<float> NearbyRadius; internal ConfigEntry<float> NearbyGraceSeconds; private ConfigEntry<string> firePrefabs; private ConfigEntry<bool> logTransitions; internal static Plugin? Instance { get; private set; } internal static ManualLogSource? Log { get; private set; } private void Awake() { //IL_01b3: Unknown result type (might be due to invalid IL or missing references) //IL_01bd: Expected O, but got Unknown //IL_02a1: Unknown result type (might be due to invalid IL or missing references) //IL_02ab: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Enabled", true, "Enable food countdown changes for the local player only."); Radius = ((BaseUnityPlugin)this).Config.Bind<float>("Base", "Radius", 75f, Range("Each of the three structures must be within this distance of the player, in metres (3D).", 1f, 200f)); GraceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Base", "GraceSeconds", 20f, Range("Seconds of protection after the first failed validation. Repeated failures do not extend it.", 0f, 300f)); NearbyEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("NearbyStructure", "Enabled", true, "Pause food timers near any one qualifying burning fire, workbench, or forge. Independent of the full base rule and Rested."); NearbyRadius = ((BaseUnityPlugin)this).Config.Bind<float>("NearbyStructure", "Radius", 10f, Range("Distance in metres (3D) to any one qualifying structure. Two or three nearby structures also qualify.", 1f, 200f)); NearbyGraceSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("NearbyStructure", "GraceSeconds", 5f, Range("Seconds of nearby-structure protection after the first failed validation. Independent of Base GraceSeconds; repeated failures do not extend it.", 0f, 300f)); DiscoverySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Performance", "DiscoverySeconds", 10f, Range("Interval between searches for missing qualifying structures. Never scans all scene objects.", 1f, 60f)); ValidationSeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Performance", "ValidationSeconds", 1f, Range("Interval for validating cached structures for both protection rules.", 0.25f, 10f)); CandidatesPerFrame = ((BaseUnityPlugin)this).Config.Bind<int>("Performance", "CandidatesPerFrame", 64, new ConfigDescription("Maximum discovery candidates examined per frame across both rules combined. Large searches continue on subsequent frames.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 1024), Array.Empty<object>())); RestedReduction = ((BaseUnityPlugin)this).Config.Bind<float>("Rested", "TimerReductionPercent", 25f, Range("Food countdown reduction while Rested, anywhere. 25 means 75% normal ticking speed. Base protection takes priority.", 0f, 100f)); firePrefabs = ((BaseUnityPlugin)this).Config.Bind<string>("Base", "FirePrefabs", "fire_pit,bonfire,hearth,piece_brazierceiling,piece_brazierfloor", "Comma-separated qualifying Fireplace prefab names. Fires must be burning. Ordinary torches are excluded by default. Restart after changing this list."); logTransitions = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "LogProtectionChanges", false, "Log entering/leaving base protection for troubleshooting. No on-screen messages."); string[] array = firePrefabs.Value.Split(new char[1] { ',' }); foreach (string text in array) { if (!string.IsNullOrWhiteSpace(text)) { allowedFires.Add(text.Trim()); } } ((BaseUnityPlugin)this).Config.SettingChanged += OnSettingChanged; try { harmony = new Harmony("local.basefood"); harmony.PatchAll(typeof(Plugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"BaseFood 0.2.0 loaded. Client-side food countdown changes; normal healing is preserved."); } catch (Exception ex) { Harmony? obj = harmony; if (obj != null) { obj.UnpatchSelf(); } ((BaseUnityPlugin)this).Logger.LogError((object)("BaseFood could not apply its patches. Vanilla food behavior is preserved. " + ex)); Instance = null; ((Behaviour)this).enabled = false; } } private static ConfigDescription Range(string text, float min, float max) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Expected O, but got Unknown return new ConfigDescription(text, (AcceptableValueBase)(object)new AcceptableValueRange<float>(min, max), Array.Empty<object>()); } private void OnSettingChanged(object sender, SettingChangedEventArgs args) { ResetDetection(); if ((object)args.ChangedSetting == firePrefabs) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Restart Valheim to apply the changed FirePrefabs list."); } } internal bool AllowsFire(string prefab) { return allowedFires.Contains(prefab); } private void ResetDetection() { detector.Reset(); nearbyDetector.Reset(); extraBudgetToNearby = false; } private bool IsProtected(float now) { if (!detector.IsProtected(now)) { if (NearbyEnabled.Value) { return nearbyDetector.IsProtected(now); } return false; } return true; } private void Update() { //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Unknown result type (might be due to invalid IL or missing references) try { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)(object)currentPlayer) { ResetDetection(); currentPlayer = localPlayer; suspended = false; } if (!Enabled.Value || !Object.op_Implicit((Object)(object)localPlayer) || ((Character)localPlayer).IsDead() || ((Character)localPlayer).IsTeleporting()) { ResetDetection(); suspended = true; return; } if (suspended) { ResetDetection(); suspended = false; } if (!(Time.deltaTime <= 0f)) { DiscoveryBudget.Split(CandidatesPerFrame.Value, NearbyEnabled.Value, ref extraBudgetToNearby, out var baseBudget, out var nearbyBudget); Vector3 position = ((Component)localPlayer).transform.position; detector.Tick(position, Time.time, Radius.Value, GraceSeconds.Value, DiscoverySeconds.Value, ValidationSeconds.Value, baseBudget); if (NearbyEnabled.Value) { nearbyDetector.Tick(position, Time.time, NearbyRadius.Value, NearbyGraceSeconds.Value, DiscoverySeconds.Value, ValidationSeconds.Value, nearbyBudget); } bool flag = IsProtected(Time.time); if (logTransitions.Value && flag != lastProtection) { ((BaseUnityPlugin)this).Logger.LogInfo((object)(flag ? "Food protection active." : "Food protection ended.")); } lastProtection = flag; } } catch (Exception ex) { ResetDetection(); if (!runtimeErrorLogged) { ((BaseUnityPlugin)this).Logger.LogError((object)("Base detection failed; protection cleared. Further identical failures will not spam the log. " + ex)); runtimeErrorLogged = true; } } } internal float GetMultiplier(Player player) { if (!Enabled.Value || (Object)(object)player != (Object)(object)Player.m_localPlayer || ((Character)player).IsDead() || ((Character)player).IsTeleporting()) { return 1f; } bool protectedAtBase = (Object)(object)player == (Object)(object)currentPlayer && !suspended && IsProtected(Time.time); return FoodRules.Multiplier(enabled: true, protectedAtBase, ((Character)player).GetSEMan().HaveStatusEffect(RestedHash), RestedReduction.Value); } internal void ClearProtection(Player player) { if ((Object)(object)player == (Object)(object)Player.m_localPlayer) { ResetDetection(); suspended = true; } } private void OnDestroy() { ((BaseUnityPlugin)this).Config.SettingChanged -= OnSettingChanged; Harmony? obj = harmony; if (obj != null) { obj.UnpatchSelf(); } ResetDetection(); StructureRegistry.Clear(); Instance = null; Log = null; } } internal enum StructureKind { Fire, Workbench, Forge } internal static class StructureRegistry { internal static readonly List<TrackedStructure>[] Lists = new List<TrackedStructure>[3] { new List<TrackedStructure>(), new List<TrackedStructure>(), new List<TrackedStructure>() }; internal static int Revision { get; private set; } internal static void Add(TrackedStructure node) { if (node.Index < 0) { List<TrackedStructure> list = Lists[(int)node.Kind]; node.Index = list.Count; list.Add(node); Revision++; } } internal static void Remove(TrackedStructure node) { if (node.Index >= 0) { List<TrackedStructure> list = Lists[(int)node.Kind]; int index = list.Count - 1; list[node.Index] = list[index]; list[node.Index].Index = node.Index; list.RemoveAt(index); node.Index = -1; Revision++; } } internal static void Clear() { List<TrackedStructure>[] lists = Lists; foreach (List<TrackedStructure> list in lists) { foreach (TrackedStructure item in list) { item.Index = -1; } list.Clear(); } Revision++; } } public sealed class TrackedStructure : MonoBehaviour { internal int Index = -1; internal StructureKind Kind; private Behaviour source; private ZNetView view; private Transform location; private bool initialized; internal void Initialize(StructureKind kind, Behaviour structure, ZNetView netView) { Kind = kind; source = structure; view = netView; location = ((Component)structure).transform; initialized = true; if (((Behaviour)this).isActiveAndEnabled) { StructureRegistry.Add(this); } } internal bool Qualifies(Vector3 playerPosition, float radiusSquared) { //IL_0044: 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_004a: 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_006a: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)this) || !Object.op_Implicit((Object)(object)source) || !source.isActiveAndEnabled || !Object.op_Implicit((Object)(object)view) || !view.IsValid()) { return false; } Vector3 val = location.position - playerPosition; if (((Vector3)(ref val)).sqrMagnitude > radiusSquared) { return false; } if (Kind == StructureKind.Fire) { return ((Fireplace)source).IsBurning(); } return true; } private void OnEnable() { if (initialized) { StructureRegistry.Add(this); } } private void OnDisable() { StructureRegistry.Remove(this); } private void OnDestroy() { StructureRegistry.Remove(this); } } [HarmonyPatch(typeof(Fireplace), "Awake")] internal static class TrackFirePatch { private static void Postfix(Fireplace __instance) { if (!((Object)(object)Plugin.Instance == (Object)null)) { ZNetView component = ((Component)__instance).GetComponent<ZNetView>(); if (Object.op_Implicit((Object)(object)component) && component.IsValid() && Plugin.Instance.AllowsFire(Utils.GetPrefabName(((Component)__instance).gameObject))) { ((Component)__instance).gameObject.AddComponent<TrackedStructure>().Initialize(StructureKind.Fire, (Behaviour)(object)__instance, component); } } } } [HarmonyPatch(typeof(CraftingStation), "Start")] internal static class TrackStationPatch { private static void Postfix(CraftingStation __instance) { if ((Object)(object)Plugin.Instance == (Object)null) { return; } ZNetView component = ((Component)__instance).GetComponent<ZNetView>(); if (!Object.op_Implicit((Object)(object)component) || !component.IsValid()) { return; } string prefabName = Utils.GetPrefabName(((Component)__instance).gameObject); StructureKind kind; if (prefabName == "piece_workbench") { kind = StructureKind.Workbench; } else { if (!(prefabName == "forge")) { return; } kind = StructureKind.Forge; } ((Component)__instance).gameObject.AddComponent<TrackedStructure>().Initialize(kind, (Behaviour)(object)__instance, component); } } }