Decompiled source of DelicateWatchSaver v1.3.2

DelicateWatchSaver.dll

Decompiled 2 weeks ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Bootstrap;
using BepInEx.Configuration;
using On.RoR2;
using RiskOfOptions;
using RiskOfOptions.OptionConfigs;
using RiskOfOptions.Options;
using RoR2;
using UnityEngine;
using UnityEngine.Networking;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("DelicateWatchSaver")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("DelicateWatchSaver")]
[assembly: AssemblyTitle("DelicateWatchSaver")]
[assembly: AssemblyVersion("1.0.0.0")]
namespace WatchSaverMod;

[BepInPlugin("com.CATAHA_CCCP.DelicateWatchSaver", "DelicateWatchSaver", "1.3.2")]
[BepInDependency(/*Could not decode attribute arguments.*/)]
public class WatchSaverPlugin : BaseUnityPlugin
{
	private enum InteractionBlockResult
	{
		Allow,
		FirstClickBlock,
		DebounceBlock
	}

	private struct ShrineUnlockKey : IEquatable<ShrineUnlockKey>
	{
		public int ShrineId;

		public int PlayerBodyId;

		public ShrineUnlockKey(int shrineId, int playerBodyId)
		{
			ShrineId = shrineId;
			PlayerBodyId = playerBodyId;
		}

		public bool Equals(ShrineUnlockKey other)
		{
			return ShrineId == other.ShrineId && PlayerBodyId == other.PlayerBodyId;
		}

		public override bool Equals(object obj)
		{
			return obj is ShrineUnlockKey other && Equals(other);
		}

		public override int GetHashCode()
		{
			return (ShrineId * 397) ^ PlayerBodyId;
		}
	}

	private Dictionary<ShrineUnlockKey, float> unlockedShrines = new Dictionary<ShrineUnlockKey, float>();

	private bool serverHasMod = false;

	private BuffIndex parryingBuffIndex = (BuffIndex)(-1);

	private bool parryingBuffCached = false;

	private ConfigEntry<bool> configGlobalMode;

	private ConfigEntry<float> configConfirmWindow;

	private ConfigEntry<float> configDebounceTime;

	private ConfigEntry<string> configCustomWarningText;

	private ConfigEntry<string> configCustomDebounceWarningText;

	private ConfigEntry<bool> configEnableDebugLogs;

	private ConfigEntry<bool> configTrustSaferSpaces;

	private ConfigEntry<bool> configTrustParry;

	public void Awake()
	{
		//IL_0120: Unknown result type (might be due to invalid IL or missing references)
		//IL_012a: Expected O, but got Unknown
		//IL_0132: Unknown result type (might be due to invalid IL or missing references)
		//IL_013c: Expected O, but got Unknown
		//IL_0144: Unknown result type (might be due to invalid IL or missing references)
		//IL_014e: Expected O, but got Unknown
		configGlobalMode = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Global Mode", false, "If true, operates in Global mode (protects everyone, requires you to be Host/Server, broadcasts warnings to everyone in chat with player names).\nIf false (default), operates in Local mode (only protects you, messages are private, works on any server).\nNOTE: This setting automatically falls back to Local mode if you join someone else's server as a client to ensure you stay protected. Use this mode ONLY if you (the host) have the mod downloaded, but your friends do not. If everyone in the lobby has the mod, it’s best to leave this turned off.");
		configConfirmWindow = ((BaseUnityPlugin)this).Config.Bind<float>("General", "Confirm Window", 3f, "Time in seconds you have to click again to confirm.");
		configDebounceTime = ((BaseUnityPlugin)this).Config.Bind<float>("General", "Debounce Time", 0.3f, "Minimum delay in seconds required before the second click is accepted. Prevents accidental instant double-clicks. Set to 0 to disable.");
		configCustomWarningText = ((BaseUnityPlugin)this).Config.Bind<string>("General", "Custom Warning Text", "", "Leave empty to use default warning. Use {0} for the timer. In Global mode, {1} will be replaced with the player's name.");
		configCustomDebounceWarningText = ((BaseUnityPlugin)this).Config.Bind<string>("General", "Custom Debounce Warning Text", "", "Leave empty to use default warning. Short warning shown when clicking too fast.");
		configEnableDebugLogs = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "Enable Debug Logs", false, "Prints math calculations to console.");
		configTrustSaferSpaces = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Trust Safer Spaces", true, "If true, no warning will be shown if Safer Spaces is ready to block the damage.");
		configTrustParry = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "Trust Parry", true, "If true, no warning will be shown if you are actively parrying (e.g. using Deus Ex Machina equipment from Alloyed Collective DLC).");
		Interactor.AttemptInteraction += new hook_AttemptInteraction(Interactor_AttemptInteraction);
		PurchaseInteraction.OnInteractionBegin += new hook_OnInteractionBegin(PurchaseInteraction_OnInteractionBegin);
		Chat.AddMessage_ChatMessageBase += new hook_AddMessage_ChatMessageBase(Chat_AddMessage_ChatMessageBase);
		Stage.onStageStartGlobal += OnStageStart;
		CharacterBody.onBodyStartGlobal += OnBodyStart;
		configGlobalMode.SettingChanged += OnGlobalModeChanged;
		if (Chainloader.PluginInfos.ContainsKey("com.rune580.riskofoptions"))
		{
			InitRiskOfOptions();
		}
		((BaseUnityPlugin)this).Logger.LogInfo((object)"DelicateWatchSaver loaded successfully!");
	}

	[MethodImpl(MethodImplOptions.NoInlining)]
	private void InitRiskOfOptions()
	{
		//IL_0012: Unknown result type (might be due to invalid IL or missing references)
		//IL_001c: Expected O, but got Unknown
		//IL_0023: Unknown result type (might be due to invalid IL or missing references)
		//IL_0028: Unknown result type (might be due to invalid IL or missing references)
		//IL_0033: Unknown result type (might be due to invalid IL or missing references)
		//IL_003e: Unknown result type (might be due to invalid IL or missing references)
		//IL_004e: Expected O, but got Unknown
		//IL_0049: Unknown result type (might be due to invalid IL or missing references)
		//IL_0053: Expected O, but got Unknown
		//IL_005a: Unknown result type (might be due to invalid IL or missing references)
		//IL_005f: 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_0075: Unknown result type (might be due to invalid IL or missing references)
		//IL_0085: Expected O, but got Unknown
		//IL_0080: Unknown result type (might be due to invalid IL or missing references)
		//IL_008a: Expected O, but got Unknown
		//IL_0091: Unknown result type (might be due to invalid IL or missing references)
		//IL_009b: Expected O, but got Unknown
		//IL_00a2: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ac: Expected O, but got Unknown
		//IL_00b3: Unknown result type (might be due to invalid IL or missing references)
		//IL_00bd: Expected O, but got Unknown
		//IL_00c4: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ce: Expected O, but got Unknown
		//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
		//IL_00df: Expected O, but got Unknown
		ModSettingsManager.SetModDescription("Safety confirmation for health-costing interactions to prevent breaking Delicate Watches.");
		ModSettingsManager.AddOption((BaseOption)new CheckBoxOption(configGlobalMode));
		ModSettingsManager.AddOption((BaseOption)new StepSliderOption(configConfirmWindow, new StepSliderConfig
		{
			min = 1f,
			max = 10f,
			increment = 0.5f
		}));
		ModSettingsManager.AddOption((BaseOption)new StepSliderOption(configDebounceTime, new StepSliderConfig
		{
			min = 0f,
			max = 2f,
			increment = 0.05f
		}));
		ModSettingsManager.AddOption((BaseOption)new StringInputFieldOption(configCustomWarningText));
		ModSettingsManager.AddOption((BaseOption)new StringInputFieldOption(configCustomDebounceWarningText));
		ModSettingsManager.AddOption((BaseOption)new CheckBoxOption(configTrustSaferSpaces));
		ModSettingsManager.AddOption((BaseOption)new CheckBoxOption(configTrustParry));
		ModSettingsManager.AddOption((BaseOption)new CheckBoxOption(configEnableDebugLogs));
	}

	private void OnDestroy()
	{
		//IL_0008: Unknown result type (might be due to invalid IL or missing references)
		//IL_0012: Expected O, but got Unknown
		//IL_001a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0024: Expected O, but got Unknown
		//IL_002c: Unknown result type (might be due to invalid IL or missing references)
		//IL_0036: Expected O, but got Unknown
		Interactor.AttemptInteraction -= new hook_AttemptInteraction(Interactor_AttemptInteraction);
		PurchaseInteraction.OnInteractionBegin -= new hook_OnInteractionBegin(PurchaseInteraction_OnInteractionBegin);
		Chat.AddMessage_ChatMessageBase -= new hook_AddMessage_ChatMessageBase(Chat_AddMessage_ChatMessageBase);
		Stage.onStageStartGlobal -= OnStageStart;
		CharacterBody.onBodyStartGlobal -= OnBodyStart;
		configGlobalMode.SettingChanged -= OnGlobalModeChanged;
	}

	private void OnStageStart(Stage stage)
	{
		unlockedShrines.Clear();
		serverHasMod = false;
	}

	private void OnGlobalModeChanged(object sender, EventArgs e)
	{
		//IL_0046: Unknown result type (might be due to invalid IL or missing references)
		//IL_004b: Unknown result type (might be due to invalid IL or missing references)
		//IL_0066: Expected O, but got Unknown
		if (NetworkServer.active)
		{
			string text = (configGlobalMode.Value ? "[WatchSaverHandshake]" : "[WatchSaverHandshakeDisable]");
			LogDebug($"Global Mode changed to {configGlobalMode.Value}. Broadcasting signal.");
			Chat.SendBroadcastChat((ChatMessageBase)new SimpleChatMessage
			{
				baseToken = "<size=0>" + text + "</size>"
			});
		}
	}

	private void OnBodyStart(CharacterBody body)
	{
		//IL_0048: Unknown result type (might be due to invalid IL or missing references)
		//IL_004d: Unknown result type (might be due to invalid IL or missing references)
		//IL_005d: Expected O, but got Unknown
		if ((Object)(object)body != (Object)null && NetworkServer.active && configGlobalMode.Value && body.isPlayerControlled)
		{
			LogDebug("Player body spawned (" + body.GetUserName() + "). Sending handshake signal.");
			Chat.SendBroadcastChat((ChatMessageBase)new SimpleChatMessage
			{
				baseToken = "<size=0>[WatchSaverHandshake]</size>"
			});
		}
	}

	private void LogDebug(string message)
	{
		if (configEnableDebugLogs.Value)
		{
			((BaseUnityPlugin)this).Logger.LogWarning((object)("[WatchSaver] " + message));
		}
	}

	private void Chat_AddMessage_ChatMessageBase(orig_AddMessage_ChatMessageBase orig, ChatMessageBase message)
	{
		SimpleChatMessage val = (SimpleChatMessage)(object)((message is SimpleChatMessage) ? message : null);
		if (val != null && !string.IsNullOrEmpty(val.baseToken))
		{
			bool flag = val.baseToken.Contains("[WatchSaverHandshake]");
			bool flag2 = val.baseToken.Contains("[WatchSaverHandshakeDisable]");
			if (flag || flag2)
			{
				if (!NetworkServer.active)
				{
					serverHasMod = flag;
					LogDebug($"Handshake signal received. serverHasMod set to: {serverHasMod}");
				}
				return;
			}
		}
		orig.Invoke(message);
	}

	private BuffIndex GetParryingBuffIndex()
	{
		//IL_0028: Unknown result type (might be due to invalid IL or missing references)
		//IL_002d: Unknown result type (might be due to invalid IL or missing references)
		//IL_0015: Unknown result type (might be due to invalid IL or missing references)
		//IL_001a: Unknown result type (might be due to invalid IL or missing references)
		//IL_0030: Unknown result type (might be due to invalid IL or missing references)
		if (!parryingBuffCached)
		{
			parryingBuffIndex = BuffCatalog.FindBuffIndex("bdParrying");
			parryingBuffCached = true;
		}
		return parryingBuffIndex;
	}

	private string GetLocalizedWarning(float timer, string playerName = "", InteractionBlockResult result = InteractionBlockResult.FirstClickBlock)
	{
		try
		{
			if (result == InteractionBlockResult.DebounceBlock)
			{
				if (!string.IsNullOrWhiteSpace(configCustomDebounceWarningText.Value))
				{
					return configCustomDebounceWarningText.Value;
				}
				if (!string.IsNullOrEmpty(playerName))
				{
					return "<color=#ffcc00>Slow down (" + playerName + "):</color> Clicking too fast!";
				}
				return "<color=#ffcc00>Slow down:</color> Clicking too fast!";
			}
			if (!string.IsNullOrWhiteSpace(configCustomWarningText.Value))
			{
				string text = configCustomWarningText.Value.Replace("{0}", timer.ToString());
				if (!string.IsNullOrEmpty(playerName))
				{
					text = text.Replace("{1}", playerName);
				}
				return text;
			}
			if (!string.IsNullOrEmpty(playerName))
			{
				return $"<color=#ff0000>Warning ({playerName}):</color> This action will break your Delicate Watch! Click again within {timer} sec to confirm.";
			}
			return $"<color=#ff0000>Warning:</color> This interaction will break your Delicate Watch! Click again within {timer} sec to confirm.";
		}
		catch (Exception arg)
		{
			((BaseUnityPlugin)this).Logger.LogError((object)$"[WatchSaver] Warning text formatting error: {arg}");
		}
		return (result == InteractionBlockResult.DebounceBlock) ? "<color=#ffcc00>Slow down:</color> Clicking too fast!" : $"<color=#ff0000>Warning:</color> Interaction will break Delicate Watch! Click again within {timer} sec.";
	}

	private void Interactor_AttemptInteraction(orig_AttemptInteraction orig, Interactor self, GameObject interactableObject)
	{
		//IL_0071: Unknown result type (might be due to invalid IL or missing references)
		//IL_0077: Invalid comparison between Unknown and I4
		if ((Object)(object)interactableObject == (Object)null)
		{
			orig.Invoke(self, interactableObject);
			return;
		}
		CharacterBody component = ((Component)self).GetComponent<CharacterBody>();
		if ((Object)(object)component != (Object)null && component.hasEffectiveAuthority)
		{
			if (serverHasMod)
			{
				LogDebug("Bypassing local check because server is running Global Mode.");
				orig.Invoke(self, interactableObject);
				return;
			}
			PurchaseInteraction component2 = interactableObject.GetComponent<PurchaseInteraction>();
			if ((Object)(object)component2 != (Object)null && (int)component2.costType == 2)
			{
				InteractionBlockResult interactionBlockResult = ShouldBlockInteraction(component, component2.cost, ((Object)((Component)component2).gameObject).GetInstanceID());
				if (interactionBlockResult != 0)
				{
					Chat.AddMessage(GetLocalizedWarning(configConfirmWindow.Value, "", interactionBlockResult));
					return;
				}
			}
		}
		orig.Invoke(self, interactableObject);
	}

	private void PurchaseInteraction_OnInteractionBegin(orig_OnInteractionBegin orig, PurchaseInteraction self, Interactor activator)
	{
		//IL_0028: Unknown result type (might be due to invalid IL or missing references)
		//IL_002e: Invalid comparison between Unknown and I4
		//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ae: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ce: Expected O, but got Unknown
		if ((Object)(object)self == (Object)null || (Object)(object)activator == (Object)null)
		{
			orig.Invoke(self, activator);
			return;
		}
		if ((int)self.costType == 2)
		{
			CharacterBody component = ((Component)activator).GetComponent<CharacterBody>();
			if ((Object)(object)component != (Object)null && configGlobalMode.Value && !component.hasEffectiveAuthority)
			{
				InteractionBlockResult interactionBlockResult = ShouldBlockInteraction(component, self.cost, ((Object)((Component)self).gameObject).GetInstanceID());
				if (interactionBlockResult != 0)
				{
					string text = component.GetUserName();
					if (string.IsNullOrEmpty(text))
					{
						text = "Someone";
					}
					Chat.SendBroadcastChat((ChatMessageBase)new SimpleChatMessage
					{
						baseToken = GetLocalizedWarning(configConfirmWindow.Value, text, interactionBlockResult)
					});
					return;
				}
			}
		}
		orig.Invoke(self, activator);
	}

	private InteractionBlockResult ShouldBlockInteraction(CharacterBody body, float cost, int objectId)
	{
		//IL_00a4: Unknown result type (might be due to invalid IL or missing references)
		//IL_00a9: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ab: Unknown result type (might be due to invalid IL or missing references)
		//IL_00ae: Invalid comparison between Unknown and I4
		//IL_00b1: Unknown result type (might be due to invalid IL or missing references)
		if ((Object)(object)body == (Object)null || (Object)(object)body.inventory == (Object)null || (Object)(object)body.healthComponent == (Object)null)
		{
			return InteractionBlockResult.Allow;
		}
		int itemCountEffective = body.inventory.GetItemCountEffective(Items.FragileDamageBonus);
		if (itemCountEffective <= 0)
		{
			return InteractionBlockResult.Allow;
		}
		if (configTrustSaferSpaces.Value && body.HasBuff(Buffs.BearVoidReady))
		{
			LogDebug("Safer Spaces is ready! Bypassing block.");
			return InteractionBlockResult.Allow;
		}
		if (configTrustParry.Value)
		{
			BuffIndex val = GetParryingBuffIndex();
			if ((int)val != -1 && body.HasBuff(val))
			{
				LogDebug("Player is actively parrying (Deus Ex Machina)! Bypassing block.");
				return InteractionBlockResult.Allow;
			}
		}
		HealthComponent healthComponent = body.healthComponent;
		float num = cost / 100f;
		float num2 = Mathf.RoundToInt(healthComponent.fullCombinedHealth * num);
		float num3 = Mathf.Max(0f, num2 - healthComponent.barrier);
		float num4 = healthComponent.combinedHealth - num3;
		float num5 = healthComponent.fullCombinedHealth * 0.25f;
		LogDebug($"Cost: {cost}%. Health After: {num4}. Threshold: {num5}");
		if (num4 <= num5 + 1f)
		{
			int instanceID = ((Object)((Component)body).gameObject).GetInstanceID();
			ShrineUnlockKey key = new ShrineUnlockKey(objectId, instanceID);
			if (unlockedShrines.TryGetValue(key, out var value))
			{
				float num6 = Time.time - value;
				if (num6 < configDebounceTime.Value)
				{
					LogDebug($"Confirmation click ignored: too fast ({num6:0.00}s elapsed, debounce is {configDebounceTime.Value}s).");
					return InteractionBlockResult.DebounceBlock;
				}
				if (num6 <= configConfirmWindow.Value)
				{
					LogDebug($"Confirmation active for Player {instanceID}. Allowing (elapsed: {num6:0.00}s).");
					unlockedShrines.Remove(key);
					return InteractionBlockResult.Allow;
				}
			}
			unlockedShrines[key] = Time.time;
			return InteractionBlockResult.FirstClickBlock;
		}
		return InteractionBlockResult.Allow;
	}
}