using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using UnityEngine; using ValuableRegen.Patches; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 ValuableRegen { internal struct DamageRecord { public float LastDamage; public float Original; } internal static class DamagedRegistry { private static readonly Dictionary Records = new Dictionary(); private static readonly List Scratch = new List(); internal static int Count => Records.Count; internal static void MarkDamaged(ValuableObject valuable) { if (!((Object)(object)valuable == (Object)null)) { if (Records.TryGetValue(valuable, out var value)) { value.LastDamage = Time.time; Records[valuable] = value; } else { Records[valuable] = new DamageRecord { LastDamage = Time.time, Original = valuable.dollarValueOriginal }; } } } internal static bool TryGet(ValuableObject valuable, out DamageRecord record) { return Records.TryGetValue(valuable, out record); } internal static void Forget(ValuableObject valuable) { Records.Remove(valuable); } internal static void Clear() { Records.Clear(); } internal static List Snapshot() { Scratch.Clear(); foreach (KeyValuePair record in Records) { Scratch.Add(record.Key); } return Scratch; } } [BepInPlugin("benjamin.valuableregen", "ValuableRegen", "1.2.0")] [BepInProcess("REPO.exe")] public class Plugin : BaseUnityPlugin { public const string Guid = "benjamin.valuableregen"; public const string Name = "ValuableRegen"; public const string Version = "1.2.0"; internal static ManualLogSource Log; private Harmony _harmony; internal static ConfigEntry Enabled; internal static ConfigEntry RegenPercentPerSecond; internal static ConfigEntry DamageCooldown; internal static ConfigEntry MaxRestoreFraction; internal static ConfigEntry RegenInterval; internal static ConfigEntry PauseWhileHeld; internal static ConfigEntry PreventDestruction; internal static ConfigEntry RescueValuePercent; internal static ConfigEntry UseHealEffect; internal static ConfigEntry Verbose; private void Awake() { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Expected O, but got Unknown //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Expected O, but got Unknown //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_011a: Expected O, but got Unknown //IL_018d: Unknown result type (might be due to invalid IL or missing references) //IL_0197: Expected O, but got Unknown //IL_01e2: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Enabled = ((BaseUnityPlugin)this).Config.Bind("General", "Enabled", true, "Master switch. Turn off to restore vanilla behaviour without uninstalling."); RegenPercentPerSecond = ((BaseUnityPlugin)this).Config.Bind("Regen", "RegenPercentPerSecond", 2f, new ConfigDescription("Percent of the valuable's ORIGINAL value restored per second while regenerating. At 2.0 an item smashed down to half value takes about 25 seconds to fully recover.", (AcceptableValueBase)(object)new AcceptableValueRange(0.05f, 50f), Array.Empty())); DamageCooldown = ((BaseUnityPlugin)this).Config.Bind("Regen", "DamageCooldown", 4f, new ConfigDescription("Seconds of no further damage before regeneration starts. Stops an item healing mid-tumble down a staircase.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 60f), Array.Empty())); MaxRestoreFraction = ((BaseUnityPlugin)this).Config.Bind("Regen", "MaxRestoreFraction", 1f, new ConfigDescription("Ceiling on regeneration, as a fraction of original value. 1.0 = full recovery. 0.8 means a damaged item can never climb back above 80% of what it was worth, so carelessness still costs you something.", (AcceptableValueBase)(object)new AcceptableValueRange(0.1f, 1f), Array.Empty())); RegenInterval = ((BaseUnityPlugin)this).Config.Bind("Regen", "RegenInterval", 0.5f, new ConfigDescription("Seconds between regeneration ticks. Lower is smoother, higher is cheaper. The per-second rate is unaffected.", (AcceptableValueBase)(object)new AcceptableValueRange(0.1f, 5f), Array.Empty())); PauseWhileHeld = ((BaseUnityPlugin)this).Config.Bind("Regen", "PauseWhileHeld", false, "Pause regeneration while a player is holding the item, so you can't stand there cradling a vase waiting for it to heal."); PreventDestruction = ((BaseUnityPlugin)this).Config.Bind("Destruction", "PreventDestruction", true, "Stop valuables being destroyed outright, so a hit heavy enough to wipe an item leaves it battered instead of gone and regeneration can bring it back. Only affects valuables - breakable props, hinges and doors are untouched."); RescueValuePercent = ((BaseUnityPlugin)this).Config.Bind("Destruction", "RescueValuePercent", 1f, new ConfigDescription("Percent of original value a rescued item is parked at. Regeneration works fine from zero, but a valuable sitting at exactly nothing risks tripping other zero-value checks in the game.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 50f), Array.Empty())); UseHealEffect = ((BaseUnityPlugin)this).Config.Bind("Advanced", "UseHealEffect", false, "Route regeneration through the game's Heal() call so the heal visual effect plays. Off by default: the amount Heal() actually restores could not be verified, so the default path writes dollar values directly instead. Only turn this on if you want the effect and have checked the numbers with Verbose."); Verbose = ((BaseUnityPlugin)this).Config.Bind("Debug", "Verbose", false, "Log every regeneration tick with before/after values. Turn this on for the first run to calibrate RegenPercentPerSecond."); _harmony = new Harmony("benjamin.valuableregen"); _harmony.PatchAll(typeof(BreakTracker)); _harmony.PatchAll(typeof(DestructionGuard)); _harmony.PatchAll(typeof(LevelResetPatch)); ((Component)this).gameObject.AddComponent(); Log.LogInfo((object)"ValuableRegen v1.2.0 loaded. Broken things mend."); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } internal static void Trace(string msg) { if (Verbose != null && Verbose.Value) { Log.LogInfo((object)msg); } } } internal class RegenRunner : MonoBehaviour { private float _timer; private void Update() { if (Plugin.Enabled.Value && SemiFunc.IsMasterClientOrSingleplayer() && DamagedRegistry.Count != 0) { _timer -= Time.deltaTime; if (!(_timer > 0f)) { Tick(_timer = Plugin.RegenInterval.Value); } } } private void Tick(float delta) { List list = DamagedRegistry.Snapshot(); for (int i = 0; i < list.Count; i++) { ValuableObject val = list[i]; if ((Object)(object)val == (Object)null) { DamagedRegistry.Forget(val); } else { if (!DamagedRegistry.TryGet(val, out var record)) { continue; } float original = record.Original; if (original <= 0f) { DamagedRegistry.Forget(val); continue; } float dollarValueCurrent = val.dollarValueCurrent; float num = original * Plugin.MaxRestoreFraction.Value; if (dollarValueCurrent >= num - 0.01f) { DamagedRegistry.Forget(val); } else if (!(Time.time - record.LastDamage < Plugin.DamageCooldown.Value) && (!Plugin.PauseWhileHeld.Value || !((Object)(object)val.physGrabObject != (Object)null) || !val.physGrabObject.grabbed)) { float num2 = original * (Plugin.RegenPercentPerSecond.Value / 100f) * delta; float target = Mathf.Min(dollarValueCurrent + num2, num); Apply(val, dollarValueCurrent, target, original); } } } } private void Apply(ValuableObject valuable, float current, float target, float original) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) if (Plugin.UseHealEffect.Value && (Object)(object)valuable.impactDetector != (Object)null) { float num = (target - current) / original * 100f; valuable.impactDetector.Heal(num, ((Component)valuable).transform.position); } else { ValueWriter.SetValue(valuable, target, original); } Plugin.Trace($"[ValuableRegen] {((Object)valuable).name}: {current:F0} -> {valuable.dollarValueCurrent:F0} " + $"(target {target:F0}, original {original:F0})"); } } internal static class ValueWriter { internal static void SetValue(ValuableObject valuable, float target, float original) { if (!((Object)(object)valuable == (Object)null)) { valuable.dollarValueCurrent = target; if (SemiFunc.IsMultiplayer() && (Object)(object)valuable.photonView != (Object)null) { valuable.photonView.RPC("DollarValueSetRPC", (RpcTarget)1, new object[1] { target }); } valuable.dollarValueOriginal = original; } } } public static class MyPluginInfo { public const string PLUGIN_GUID = "ValuableRegen"; public const string PLUGIN_NAME = "ValuableRegen"; public const string PLUGIN_VERSION = "1.2.0"; } } namespace ValuableRegen.Patches { [HarmonyPatch(typeof(PhysGrabObjectImpactDetector))] internal static class BreakTracker { private static void Register(PhysGrabObjectImpactDetector detector) { if (Plugin.Enabled.Value && !((Object)(object)detector == (Object)null)) { ValuableObject component = ((Component)detector).GetComponent(); if (!((Object)(object)component == (Object)null)) { DamagedRegistry.MarkDamaged(component); } } } [HarmonyPostfix] [HarmonyPatch("Break")] private static void AfterBreak(PhysGrabObjectImpactDetector __instance, float valueLost, Vector3 _contactPoint, int breakLevel, bool _forceBreak) { Register(__instance); } [HarmonyPostfix] [HarmonyPatch("BreakRPC")] private static void AfterBreakRPC(PhysGrabObjectImpactDetector __instance) { Register(__instance); } } [HarmonyPatch(typeof(PhysGrabObjectImpactDetector))] internal static class DestructionGuard { private static bool Allow(PhysGrabObjectImpactDetector detector) { if (!Plugin.Enabled.Value || !Plugin.PreventDestruction.Value) { return true; } if ((Object)(object)detector == (Object)null) { return true; } ValuableObject valuableObject = detector.valuableObject; if ((Object)(object)valuableObject == (Object)null) { return true; } DamagedRegistry.MarkDamaged(valuableObject); if (!DamagedRegistry.TryGet(valuableObject, out var record)) { return true; } float num = record.Original * (Plugin.RescueValuePercent.Value / 100f); if (valuableObject.dollarValueCurrent < num) { ValueWriter.SetValue(valuableObject, num, record.Original); } Plugin.Trace("[ValuableRegen] Saved " + ((Object)valuableObject).name + " from destruction at " + $"{valuableObject.dollarValueCurrent:F0} (original {record.Original:F0})."); return false; } [HarmonyPrefix] [HarmonyPatch("DestroyObject")] private static bool BeforeDestroyObject(PhysGrabObjectImpactDetector __instance, bool effects) { return Allow(__instance); } [HarmonyPrefix] [HarmonyPatch("DestroyObjectRPC")] private static bool BeforeDestroyObjectRPC(PhysGrabObjectImpactDetector __instance) { return Allow(__instance); } } [HarmonyPatch(typeof(RunManager))] internal static class LevelResetPatch { [HarmonyPostfix] [HarmonyPatch("ChangeLevel")] private static void AfterChangeLevel() { DamagedRegistry.Clear(); } } }