Decompiled source of PerfectExtractionBonus v1.3.0

PerfectExtractionBonus.dll

Decompiled a month ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using UnityEngine;
using UnityEngine.SceneManagement;

[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: IgnoresAccessChecksTo("")]
[assembly: AssemblyCompany("REPOJP")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("REPOJP")]
[assembly: AssemblyTitle("REPOJP")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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]
	[Microsoft.CodeAnalysis.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]
	[Microsoft.CodeAnalysis.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 REPOJP.PerfectExtractionBonus
{
	[BepInPlugin("REPOJP.PerfectExtractionBonus", "PerfectExtractionBonus", "1.3.0")]
	public sealed class PerfectExtractionBonusPlugin : BaseUnityPlugin
	{
		[HarmonyPatch(typeof(ExtractionPoint), "DestroyTheFirstPhysObjectsInHaulList")]
		private static class ExtractionPoint_DestroyTheFirstPhysObjectsInHaulList_Patch
		{
			private static void Prefix()
			{
				try
				{
					SnapshotDeliveredValuablesFromHaulList(firstOnly: true);
				}
				catch (Exception ex)
				{
					if ((Object)(object)Instance != (Object)null)
					{
						((BaseUnityPlugin)Instance).Logger.LogError((object)"Failure: DestroyTheFirstPhysObjectsInHaulList Prefix");
						((BaseUnityPlugin)Instance).Logger.LogError((object)ex);
					}
				}
			}

			private static void Postfix()
			{
				try
				{
					CommitPendingDeliveredValuables();
				}
				catch (Exception ex)
				{
					if ((Object)(object)Instance != (Object)null)
					{
						((BaseUnityPlugin)Instance).Logger.LogError((object)"Failure: DestroyTheFirstPhysObjectsInHaulList Postfix");
						((BaseUnityPlugin)Instance).Logger.LogError((object)ex);
					}
				}
			}
		}

		[HarmonyPatch(typeof(ExtractionPoint), "DestroyAllPhysObjectsInHaulList")]
		private static class ExtractionPoint_DestroyAllPhysObjectsInHaulList_Patch
		{
			private static void Prefix()
			{
				try
				{
					SnapshotDeliveredValuablesFromHaulList(firstOnly: false);
				}
				catch (Exception ex)
				{
					if ((Object)(object)Instance != (Object)null)
					{
						((BaseUnityPlugin)Instance).Logger.LogError((object)"Failure: DestroyAllPhysObjectsInHaulList Prefix");
						((BaseUnityPlugin)Instance).Logger.LogError((object)ex);
					}
				}
			}

			private static void Postfix()
			{
				try
				{
					CommitPendingDeliveredValuables();
				}
				catch (Exception ex)
				{
					if ((Object)(object)Instance != (Object)null)
					{
						((BaseUnityPlugin)Instance).Logger.LogError((object)"Failure: DestroyAllPhysObjectsInHaulList Postfix");
						((BaseUnityPlugin)Instance).Logger.LogError((object)ex);
					}
				}
			}
		}

		[HarmonyPatch(typeof(RunManager), "ChangeLevel")]
		private static class RunManager_ChangeLevel_Patch
		{
			private static void Prefix(bool _completedLevel, bool _levelFailed, ChangeLevelType _changeLevelType)
			{
				//IL_00e0: Unknown result type (might be due to invalid IL or missing references)
				//IL_00e2: Invalid comparison between Unknown and I4
				try
				{
					if (!ModEnabled.Value || !SemiFunc.IsMasterClientOrSingleplayer() || !IsTrackedGameplayLevel() || LevelResultProcessed)
					{
						return;
					}
					string currentLevelKey = GetCurrentLevelKey();
					if (!string.Equals(CurrentTrackedLevelKey, currentLevelKey, StringComparison.Ordinal))
					{
						CurrentTrackedLevelKey = currentLevelKey;
					}
					if (_levelFailed)
					{
						LevelResultProcessed = true;
						int perfectClearStreak = PerfectClearStreak;
						if (ResetStreakOnLevelFail.Value)
						{
							PerfectClearStreak = 0;
							WriteLog($"ResetStreak Reason=LevelFailed Previous={perfectClearStreak} Current={PerfectClearStreak}");
						}
						WriteLog("LevelResultProcessed Result=Failed LevelKey=" + CurrentTrackedLevelKey + " BonusResult=NotGranted");
					}
					else if ((int)_changeLevelType == 5 || (_completedLevel && !SemiFunc.RunIsLobby() && !SemiFunc.RunIsShop() && !SemiFunc.RunIsArena()))
					{
						LevelResultProcessed = true;
						WriteLog("LevelResultProcessed Result=Completed LevelKey=" + CurrentTrackedLevelKey);
						TryApplyPerfectExtractionBonus();
					}
				}
				catch (Exception ex)
				{
					if ((Object)(object)Instance != (Object)null)
					{
						((BaseUnityPlugin)Instance).Logger.LogError((object)"Failure: RunManager.ChangeLevel Prefix");
						((BaseUnityPlugin)Instance).Logger.LogError((object)ex);
					}
				}
			}
		}

		public const string PluginGuid = "REPOJP.PerfectExtractionBonus";

		public const string PluginName = "PerfectExtractionBonus";

		public const string PluginVersion = "1.3.0";

		internal static PerfectExtractionBonusPlugin Instance;

		private Harmony harmony;

		internal static ConfigEntry<bool> ModEnabled;

		internal static ConfigEntry<int> BaseBonusPercent;

		internal static ConfigEntry<bool> EnableStreakBonus;

		internal static ConfigEntry<int> StreakAddPercent;

		internal static ConfigEntry<int> MaxBonusPercent;

		internal static ConfigEntry<int> NoLossExtraBonusPercent;

		internal static ConfigEntry<bool> ResetStreakOnNonPerfectClear;

		internal static ConfigEntry<bool> ResetStreakOnLevelFail;

		internal static ConfigEntry<bool> ShowBonusUI;

		internal static ConfigEntry<float> InitialSnapshotDelaySeconds;

		internal static ConfigEntry<int> SnapshotStableFrames;

		internal static ConfigEntry<bool> LogEnabled;

		internal static ConfigEntry<string> PerfectBonusPublicChatMessage;

		internal static ConfigEntry<string> NoLossPerfectBonusPublicChatMessage;

		internal static ConfigEntry<bool> AnnounceRemainingCountInPublicChat;

		internal static ConfigEntry<string> RemainingCountPublicChatMessage;

		internal static string CurrentSceneName = string.Empty;

		internal static string CurrentTrackedLevelKey = string.Empty;

		internal static bool WaitingInitialSnapshot;

		internal static bool InitialSnapshotTaken;

		internal static float InitialSnapshotReadyTime;

		internal static string LastObservedFingerprint = string.Empty;

		internal static int StableObservedFrames;

		internal static bool LevelResultProcessed;

		internal static bool BonusGrantedThisLevel;

		internal static bool PublicBonusChatSentThisLevel;

		internal static readonly HashSet<int> InitialValuableIds = new HashSet<int>();

		internal static readonly HashSet<int> DeliveredInitialValuableIds = new HashSet<int>();

		internal static readonly List<int> PendingDeliveredIds = new List<int>();

		internal static readonly Dictionary<int, float> InitialValuableOriginalValues = new Dictionary<int, float>();

		internal static readonly HashSet<int> ValueLostValuableIds = new HashSet<int>();

		internal static bool NoLossExtraBonusEligible;

		internal static float NextValueLossCheckTime;

		internal static int PerfectClearStreak;

		private void Awake()
		{
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			//IL_0070: Unknown result type (might be due to invalid IL or missing references)
			//IL_007a: Expected O, but got Unknown
			try
			{
				Instance = this;
				if ((Object)(object)((Component)this).transform.parent != (Object)null)
				{
					((Component)this).transform.parent = null;
				}
				((Object)((Component)this).gameObject).hideFlags = (HideFlags)61;
				Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject);
				InitializeConfig();
				Scene activeScene = SceneManager.GetActiveScene();
				CurrentSceneName = ((Scene)(ref activeScene)).name;
				ResetLevelState("Initial");
				harmony = new Harmony("REPOJP.PerfectExtractionBonus");
				harmony.PatchAll();
				WriteLog("Loaded");
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"Failure: Awake");
				((BaseUnityPlugin)this).Logger.LogError((object)ex);
			}
		}

		private void OnDestroy()
		{
			try
			{
				if (harmony != null)
				{
					harmony.UnpatchSelf();
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"Failure: OnDestroy");
				((BaseUnityPlugin)this).Logger.LogError((object)ex);
			}
		}

		private void Update()
		{
			try
			{
				if (ModEnabled.Value && SemiFunc.IsMasterClientOrSingleplayer())
				{
					DetectSceneChange();
					DetectLevelKeyChange();
					TryCaptureInitialValuables();
					TryMonitorNoLossExtraBonusEligibility();
				}
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogError((object)"Failure: Update");
				((BaseUnityPlugin)this).Logger.LogError((object)ex);
			}
		}

		private void InitializeConfig()
		{
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: 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_00ca: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d4: Expected O, but got Unknown
			//IL_00fd: Unknown result type (might be due to invalid IL or missing references)
			//IL_0107: Expected O, but got Unknown
			//IL_019a: Unknown result type (might be due to invalid IL or missing references)
			//IL_01a4: Expected O, but got Unknown
			//IL_01d0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01da: Expected O, but got Unknown
			ModEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "ModEnabled", true, "Enable or disable PerfectExtractionBonus / PerfectExtractionBonusの有効無効");
			BaseBonusPercent = ((BaseUnityPlugin)this).Config.Bind<int>("Bonus", "BaseBonusPercent", 10, new ConfigDescription("Base bonus percent for a perfect extraction clear / 完全納品クリア時の基本ボーナス率", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
			EnableStreakBonus = ((BaseUnityPlugin)this).Config.Bind<bool>("Bonus", "EnableStreakBonus", true, "Enable additional bonus percent for consecutive perfect extractions / 連続完全納品時の追加ボーナス率を有効化");
			StreakAddPercent = ((BaseUnityPlugin)this).Config.Bind<int>("Bonus", "StreakAddPercent", 5, new ConfigDescription("Additional bonus percent added for each consecutive perfect clear after the first / 2連続目以降に1回ごと加算するボーナス率", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
			MaxBonusPercent = ((BaseUnityPlugin)this).Config.Bind<int>("Bonus", "MaxBonusPercent", 50, new ConfigDescription("Maximum total bonus percent for the regular perfect extraction bonus / 通常の完全納品ボーナス率の上限", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
			NoLossExtraBonusPercent = ((BaseUnityPlugin)this).Config.Bind<int>("Bonus", "NoLossExtraBonusPercent", 10, new ConfigDescription("Additional bonus percent granted when all initial valuables are extracted without any value loss / 初期貴重品すべてを一度も価値減少させずに納品した場合の追加ボーナス率", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>()));
			ResetStreakOnNonPerfectClear = ((BaseUnityPlugin)this).Config.Bind<bool>("Streak", "ResetStreakOnNonPerfectClear", true, "Reset streak when the level is cleared without a perfect extraction / 通常クリアだが完全納品でない場合に連続数をリセット");
			ResetStreakOnLevelFail = ((BaseUnityPlugin)this).Config.Bind<bool>("Streak", "ResetStreakOnLevelFail", true, "Reset streak when the level fails / レベル失敗時に連続数をリセット");
			ShowBonusUI = ((BaseUnityPlugin)this).Config.Bind<bool>("UI", "ShowBonusUI", true, "Show bonus UI when the reward is granted / ボーナス獲得時にUI表示");
			InitialSnapshotDelaySeconds = ((BaseUnityPlugin)this).Config.Bind<float>("Tracking", "InitialSnapshotDelaySeconds", 0.75f, new ConfigDescription("Delay after level generation starts before stable initial valuable detection begins / 初期貴重品安定検出を始めるまでの待機秒数", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 10f), Array.Empty<object>()));
			SnapshotStableFrames = ((BaseUnityPlugin)this).Config.Bind<int>("Tracking", "SnapshotStableFrames", 20, new ConfigDescription("Number of consecutive frames the initial valuable set must remain unchanged before it is locked / 初期貴重品集合が変化せず確定する必要がある連続フレーム数", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 600), Array.Empty<object>()));
			LogEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "LogEnabled", true, "Enable detailed debug logging / 詳細ログの出力設定");
			PerfectBonusPublicChatMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Chat", "PerfectBonusPublicChatMessage", "Perfect Extraction Bonus 〇〇k$", "Public forced chat message for a regular perfect extraction bonus. Replace the exact text 〇〇k$ with the dynamic bonus amount automatically / 通常の完全納品ボーナス時に公開強制チャットで送る文言。文字列中の 〇〇k$ は動的なボーナス額に自動置換");
			NoLossPerfectBonusPublicChatMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Chat", "NoLossPerfectBonusPublicChatMessage", "Wow!!! Super No-loss Perfect Extraction Bonus 〇〇k$", "Public forced chat message for a no-loss perfect extraction bonus. Replace the exact text 〇〇k$ with the dynamic bonus amount automatically / 無減額完全納品ボーナス時に公開強制チャットで送る文言。文字列中の 〇〇k$ は動的なボーナス額に自動置換");
			AnnounceRemainingCountInPublicChat = ((BaseUnityPlugin)this).Config.Bind<bool>("Chat", "AnnounceRemainingCountInPublicChat", false, "Force all players to publicly chat the remaining undelivered initial valuable count after each successful delivery / 途中納品ごとに残り未納品数を全員強制公開チャット");
			RemainingCountPublicChatMessage = ((BaseUnityPlugin)this).Config.Bind<string>("Chat", "RemainingCountPublicChatMessage", "{remaining} valuables left", "Public chat message after each successful delivery. Supported placeholders: {remaining} {delivered} {initial} / 途中納品ごとの公開チャット文。使用可能プレースホルダー: {remaining} {delivered} {initial}");
		}

		internal static void WriteLog(string message)
		{
			if (!((Object)(object)Instance == (Object)null) && LogEnabled != null && LogEnabled.Value)
			{
				((BaseUnityPlugin)Instance).Logger.LogInfo((object)("[PerfectExtractionBonus] " + message));
			}
		}

		internal static void DetectSceneChange()
		{
			//IL_0001: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Unknown result type (might be due to invalid IL or missing references)
			Scene activeScene = SceneManager.GetActiveScene();
			string name = ((Scene)(ref activeScene)).name;
			if (!string.Equals(name, CurrentSceneName, StringComparison.Ordinal))
			{
				CurrentSceneName = name;
				ResetLevelState("SceneChanged:" + name);
			}
		}

		internal static void DetectLevelKeyChange()
		{
			string currentLevelKey = GetCurrentLevelKey();
			if (!string.IsNullOrEmpty(currentLevelKey) && !string.Equals(currentLevelKey, CurrentTrackedLevelKey, StringComparison.Ordinal))
			{
				ResetLevelState("LevelKeyChanged:" + currentLevelKey);
			}
		}

		internal static void ResetLevelState(string reason)
		{
			CurrentTrackedLevelKey = GetCurrentLevelKey();
			WaitingInitialSnapshot = false;
			InitialSnapshotTaken = false;
			InitialSnapshotReadyTime = -1f;
			LastObservedFingerprint = string.Empty;
			StableObservedFrames = 0;
			LevelResultProcessed = false;
			BonusGrantedThisLevel = false;
			PublicBonusChatSentThisLevel = false;
			InitialValuableIds.Clear();
			DeliveredInitialValuableIds.Clear();
			PendingDeliveredIds.Clear();
			InitialValuableOriginalValues.Clear();
			ValueLostValuableIds.Clear();
			NoLossExtraBonusEligible = true;
			NextValueLossCheckTime = 0f;
			WriteLog("ResetLevelState Reason=" + reason + " LevelKey=" + CurrentTrackedLevelKey);
		}

		internal static string GetCurrentLevelKey()
		{
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_003d: Unknown result type (might be due to invalid IL or missing references)
			if ((Object)(object)RunManager.instance != (Object)null && (Object)(object)RunManager.instance.levelCurrent != (Object)null)
			{
				return ((Object)RunManager.instance.levelCurrent).name;
			}
			Scene activeScene = SceneManager.GetActiveScene();
			return ((Scene)(ref activeScene)).name;
		}

		internal static bool IsTrackedGameplayLevel()
		{
			if ((Object)(object)RunManager.instance == (Object)null)
			{
				return false;
			}
			if (SemiFunc.MenuLevel())
			{
				return false;
			}
			if (SemiFunc.RunIsLobby())
			{
				return false;
			}
			if (SemiFunc.RunIsShop())
			{
				return false;
			}
			if (SemiFunc.RunIsArena())
			{
				return false;
			}
			if (SemiFunc.RunIsTutorial())
			{
				return false;
			}
			if (SemiFunc.RunIsRecording())
			{
				return false;
			}
			if (SemiFunc.IsSplashScreen())
			{
				return false;
			}
			return true;
		}

		internal static void TryCaptureInitialValuables()
		{
			if (InitialSnapshotTaken || !IsTrackedGameplayLevel() || (Object)(object)LevelGenerator.Instance == (Object)null || !LevelGenerator.Instance.Generated || (Object)(object)ValuableDirector.instance == (Object)null || !ValuableDirector.instance.setupComplete)
			{
				return;
			}
			if (!WaitingInitialSnapshot)
			{
				WaitingInitialSnapshot = true;
				InitialSnapshotReadyTime = Time.time + Mathf.Max(0f, InitialSnapshotDelaySeconds.Value);
				WriteLog($"InitialSnapshotWaiting LevelKey={GetCurrentLevelKey()} ReadyAt={InitialSnapshotReadyTime:0.###}");
			}
			else
			{
				if (Time.time < InitialSnapshotReadyTime)
				{
					return;
				}
				HashSet<int> hashSet = BuildObservedInitialValuableIdSet();
				if (hashSet.Count <= 0)
				{
					return;
				}
				string text = BuildFingerprint(hashSet);
				if (!string.Equals(text, LastObservedFingerprint, StringComparison.Ordinal))
				{
					LastObservedFingerprint = text;
					StableObservedFrames = 1;
					WriteLog($"InitialSnapshotObservedChanged LevelKey={GetCurrentLevelKey()} Count={hashSet.Count} StableFrames={StableObservedFrames}/{SnapshotStableFrames.Value}");
					return;
				}
				StableObservedFrames++;
				WriteLog($"InitialSnapshotObservedStable LevelKey={GetCurrentLevelKey()} Count={hashSet.Count} StableFrames={StableObservedFrames}/{SnapshotStableFrames.Value}");
				if (StableObservedFrames >= SnapshotStableFrames.Value)
				{
					LockInitialValuables(hashSet);
				}
			}
		}

		internal static HashSet<int> BuildObservedInitialValuableIdSet()
		{
			HashSet<int> hashSet = new HashSet<int>();
			if ((Object)(object)ValuableDirector.instance == (Object)null || ValuableDirector.instance.valuableList == null)
			{
				return hashSet;
			}
			foreach (ValuableObject valuable in ValuableDirector.instance.valuableList)
			{
				if (!((Object)(object)valuable == (Object)null) && !((Object)(object)((Component)valuable).gameObject == (Object)null))
				{
					int valuableId = GetValuableId(((Component)valuable).gameObject);
					if (valuableId != 0)
					{
						hashSet.Add(valuableId);
					}
				}
			}
			return hashSet;
		}

		internal static string BuildFingerprint(HashSet<int> valuableIds)
		{
			List<int> list = new List<int>(valuableIds);
			list.Sort();
			StringBuilder stringBuilder = new StringBuilder(list.Count * 8);
			foreach (int item in list)
			{
				stringBuilder.Append(item);
				stringBuilder.Append('|');
			}
			return stringBuilder.ToString();
		}

		internal static void LockInitialValuables(HashSet<int> observedInitialIds)
		{
			InitialValuableIds.Clear();
			DeliveredInitialValuableIds.Clear();
			PendingDeliveredIds.Clear();
			InitialValuableOriginalValues.Clear();
			ValueLostValuableIds.Clear();
			NoLossExtraBonusEligible = true;
			NextValueLossCheckTime = Time.time + 0.25f;
			if ((Object)(object)ValuableDirector.instance != (Object)null && ValuableDirector.instance.valuableList != null)
			{
				foreach (ValuableObject valuable in ValuableDirector.instance.valuableList)
				{
					if (!((Object)(object)valuable == (Object)null) && !((Object)(object)((Component)valuable).gameObject == (Object)null))
					{
						int valuableId = GetValuableId(((Component)valuable).gameObject);
						if (valuableId != 0 && observedInitialIds.Contains(valuableId))
						{
							float valuableOriginalValue = GetValuableOriginalValue(valuable);
							InitialValuableIds.Add(valuableId);
							InitialValuableOriginalValues[valuableId] = valuableOriginalValue;
						}
					}
				}
			}
			CurrentTrackedLevelKey = GetCurrentLevelKey();
			WaitingInitialSnapshot = false;
			InitialSnapshotTaken = true;
			StableObservedFrames = 0;
			WriteLog($"InitialSnapshotCaptured LevelKey={CurrentTrackedLevelKey} InitialValuables={InitialValuableIds.Count} NoLossEligible={NoLossExtraBonusEligible}");
		}

		internal static int GetValuableId(GameObject gameObject)
		{
			if ((Object)(object)gameObject == (Object)null)
			{
				return 0;
			}
			PhotonView component = gameObject.GetComponent<PhotonView>();
			if ((Object)(object)component != (Object)null && component.ViewID != 0)
			{
				return component.ViewID;
			}
			return ((Object)gameObject).GetInstanceID();
		}

		internal static float GetValuableOriginalValue(ValuableObject valuableObject)
		{
			if ((Object)(object)valuableObject == (Object)null)
			{
				return 0f;
			}
			if (valuableObject.dollarValueOriginal > 0f)
			{
				return valuableObject.dollarValueOriginal;
			}
			return Mathf.Max(0f, valuableObject.dollarValueCurrent);
		}

		internal static float GetValuableCurrentValue(ValuableObject valuableObject)
		{
			if ((Object)(object)valuableObject == (Object)null)
			{
				return 0f;
			}
			return Mathf.Max(0f, valuableObject.dollarValueCurrent);
		}

		internal static void TryMonitorNoLossExtraBonusEligibility()
		{
			if (!InitialSnapshotTaken || !NoLossExtraBonusEligible || NoLossExtraBonusPercent.Value <= 0 || Time.time < NextValueLossCheckTime)
			{
				return;
			}
			NextValueLossCheckTime = Time.time + 0.25f;
			if ((Object)(object)ValuableDirector.instance == (Object)null || ValuableDirector.instance.valuableList == null)
			{
				return;
			}
			foreach (ValuableObject valuable in ValuableDirector.instance.valuableList)
			{
				if (!((Object)(object)valuable == (Object)null))
				{
					EvaluateValueLossState(valuable, "PeriodicCheck");
					if (!NoLossExtraBonusEligible)
					{
						break;
					}
				}
			}
		}

		internal static void EvaluateValueLossState(ValuableObject valuableObject, string source)
		{
			if ((Object)(object)valuableObject == (Object)null || !InitialSnapshotTaken || !NoLossExtraBonusEligible || NoLossExtraBonusPercent.Value <= 0)
			{
				return;
			}
			int valuableId = GetValuableId(((Component)valuableObject).gameObject);
			if (valuableId != 0 && InitialValuableIds.Contains(valuableId) && InitialValuableOriginalValues.TryGetValue(valuableId, out var value))
			{
				float valuableCurrentValue = GetValuableCurrentValue(valuableObject);
				if (!(valuableCurrentValue + 0.01f >= value))
				{
					NoLossExtraBonusEligible = false;
					ValueLostValuableIds.Add(valuableId);
					WriteLog($"NoLossExtraBonusDisabled Source={source} ValuableId={valuableId} InitialValue={value:0.###} CurrentValue={valuableCurrentValue:0.###}");
				}
			}
		}

		internal static void SnapshotDeliveredValuablesFromHaulList(bool firstOnly)
		{
			PendingDeliveredIds.Clear();
			if (!ModEnabled.Value || !SemiFunc.IsMasterClientOrSingleplayer() || !InitialSnapshotTaken || (Object)(object)RoundDirector.instance == (Object)null || RoundDirector.instance.dollarHaulList == null)
			{
				return;
			}
			if (firstOnly)
			{
				if (RoundDirector.instance.dollarHaulList.Count != 0)
				{
					GameObject gameObject = RoundDirector.instance.dollarHaulList[0];
					TryAddPendingDeliveredId(gameObject);
				}
				return;
			}
			foreach (GameObject dollarHaul in RoundDirector.instance.dollarHaulList)
			{
				TryAddPendingDeliveredId(dollarHaul);
			}
		}

		internal static void TryAddPendingDeliveredId(GameObject gameObject)
		{
			if ((Object)(object)gameObject == (Object)null)
			{
				return;
			}
			ValuableObject component = gameObject.GetComponent<ValuableObject>();
			if (!((Object)(object)component == (Object)null))
			{
				EvaluateValueLossState(component, "DeliverySnapshot");
				int valuableId = GetValuableId(gameObject);
				if (valuableId != 0 && InitialValuableIds.Contains(valuableId) && !PendingDeliveredIds.Contains(valuableId))
				{
					PendingDeliveredIds.Add(valuableId);
				}
			}
		}

		internal static void CommitPendingDeliveredValuables()
		{
			if (!ModEnabled.Value)
			{
				PendingDeliveredIds.Clear();
			}
			else if (!SemiFunc.IsMasterClientOrSingleplayer())
			{
				PendingDeliveredIds.Clear();
			}
			else
			{
				if (PendingDeliveredIds.Count == 0)
				{
					return;
				}
				int num = 0;
				foreach (int pendingDeliveredId in PendingDeliveredIds)
				{
					if (pendingDeliveredId != 0 && DeliveredInitialValuableIds.Add(pendingDeliveredId))
					{
						num++;
					}
				}
				PendingDeliveredIds.Clear();
				if (num > 0)
				{
					int missingInitialValuableCount = GetMissingInitialValuableCount();
					WriteLog($"CommitDelivered Added={num} Delivered={DeliveredInitialValuableIds.Count} Initial={InitialValuableIds.Count} Remaining={missingInitialValuableCount}");
					if (AnnounceRemainingCountInPublicChat.Value)
					{
						string message = FormatRemainingCountChatMessage(missingInitialValuableCount);
						ForcePublicChatFromAllPlayers(message, "RemainingCount");
					}
					TryBroadcastPublicBonusChatIfPerfect("FinalDelivery");
				}
			}
		}

		internal static bool IsPerfectExtraction()
		{
			if (!InitialSnapshotTaken)
			{
				return false;
			}
			if (InitialValuableIds.Count <= 0)
			{
				return false;
			}
			foreach (int initialValuableId in InitialValuableIds)
			{
				if (!DeliveredInitialValuableIds.Contains(initialValuableId))
				{
					return false;
				}
			}
			return true;
		}

		internal static bool IsNoLossPerfectExtraction()
		{
			if (!IsPerfectExtraction())
			{
				return false;
			}
			if (NoLossExtraBonusPercent.Value <= 0)
			{
				return false;
			}
			if (!NoLossExtraBonusEligible)
			{
				return false;
			}
			return true;
		}

		internal static int GetMissingInitialValuableCount()
		{
			if (!InitialSnapshotTaken)
			{
				return 0;
			}
			int num = InitialValuableIds.Count - DeliveredInitialValuableIds.Count;
			if (num < 0)
			{
				num = 0;
			}
			return num;
		}

		internal static void GetCurrentBonusCalculation(out int baseBonusPercent, out int streakStepCount, out int streakAddPercentPerStep, out int streakBonusPercent, out int uncappedBonusPercent, out int finalBonusPercent, out int maxBonusPercent)
		{
			baseBonusPercent = Mathf.Max(0, BaseBonusPercent.Value);
			streakStepCount = 0;
			streakAddPercentPerStep = 0;
			streakBonusPercent = 0;
			maxBonusPercent = Mathf.Max(0, MaxBonusPercent.Value);
			if (EnableStreakBonus.Value && PerfectClearStreak > 0)
			{
				streakStepCount = PerfectClearStreak;
				streakAddPercentPerStep = Mathf.Max(0, StreakAddPercent.Value);
				streakBonusPercent = streakAddPercentPerStep * streakStepCount;
			}
			uncappedBonusPercent = baseBonusPercent + streakBonusPercent;
			finalBonusPercent = Mathf.Min(uncappedBonusPercent, maxBonusPercent);
		}

		internal static void GetCurrentGrantedBonusAmounts(out int currencyBefore, out int rewardStreakNumber, out int baseBonusPercent, out int streakStepCount, out int streakAddPercentPerStep, out int streakBonusPercent, out int uncappedBonusPercent, out int regularBonusPercent, out int maxBonusPercent, out int regularBonusAmount, out int noLossExtraBonusPercentApplied, out int noLossExtraBonusAmount, out int totalBonusPercentForDisplay, out int totalBonusAmount, out int currencyAfter, out bool isNoLossPerfectExtraction)
		{
			currencyBefore = SemiFunc.StatGetRunCurrency();
			rewardStreakNumber = PerfectClearStreak + 1;
			GetCurrentBonusCalculation(out baseBonusPercent, out streakStepCount, out streakAddPercentPerStep, out streakBonusPercent, out uncappedBonusPercent, out regularBonusPercent, out maxBonusPercent);
			isNoLossPerfectExtraction = IsNoLossPerfectExtraction();
			noLossExtraBonusPercentApplied = (isNoLossPerfectExtraction ? Mathf.Max(0, NoLossExtraBonusPercent.Value) : 0);
			regularBonusAmount = Mathf.FloorToInt((float)currencyBefore * ((float)regularBonusPercent / 100f));
			noLossExtraBonusAmount = Mathf.FloorToInt((float)currencyBefore * ((float)noLossExtraBonusPercentApplied / 100f));
			totalBonusPercentForDisplay = regularBonusPercent + noLossExtraBonusPercentApplied;
			totalBonusAmount = regularBonusAmount + noLossExtraBonusAmount;
			currencyAfter = currencyBefore + totalBonusAmount;
		}

		internal static string FormatBonusAmountForPublicChat(int bonusAmount)
		{
			return ((float)Mathf.Max(0, bonusAmount) / 1000f).ToString("0.##", CultureInfo.InvariantCulture) + "k$";
		}

		internal static string BuildPublicBonusChatMessage(string template, int bonusAmount)
		{
			if (string.IsNullOrEmpty(template))
			{
				return string.Empty;
			}
			return template.Replace("〇〇k$", FormatBonusAmountForPublicChat(bonusAmount));
		}

		internal static string FormatRemainingCountChatMessage(int remainingCount)
		{
			string value = RemainingCountPublicChatMessage.Value;
			if (string.IsNullOrEmpty(value))
			{
				return string.Empty;
			}
			string text = value;
			text = text.Replace("{remaining}", remainingCount.ToString());
			text = text.Replace("{delivered}", DeliveredInitialValuableIds.Count.ToString());
			return text.Replace("{initial}", InitialValuableIds.Count.ToString());
		}

		internal static void ForcePublicChatFromAllPlayers(string message, string source)
		{
			if (string.IsNullOrWhiteSpace(message))
			{
				WriteLog("ForcePublicChatSkipped Source=" + source + " Reason=EmptyMessage");
				return;
			}
			int num = 0;
			if ((Object)(object)GameDirector.instance != (Object)null && GameDirector.instance.PlayerList != null)
			{
				foreach (PlayerAvatar player in GameDirector.instance.PlayerList)
				{
					if ((Object)(object)player == (Object)null)
					{
						continue;
					}
					try
					{
						MethodInfo methodInfo = AccessTools.Method(((object)player).GetType(), "ChatMessageSend", new Type[1] { typeof(string) }, (Type[])null);
						if (methodInfo != null)
						{
							methodInfo.Invoke(player, new object[1] { message });
							num++;
						}
					}
					catch (Exception ex)
					{
						if ((Object)(object)Instance != (Object)null)
						{
							((BaseUnityPlugin)Instance).Logger.LogError((object)"Failure: ForcePublicChatFromAllPlayers");
							((BaseUnityPlugin)Instance).Logger.LogError((object)ex);
						}
					}
				}
			}
			if (num <= 0 && (Object)(object)TruckScreenText.instance != (Object)null)
			{
				try
				{
					TruckScreenText.instance.MessageSendCustom(string.Empty, message, 0);
					num = 1;
				}
				catch (Exception ex2)
				{
					if ((Object)(object)Instance != (Object)null)
					{
						((BaseUnityPlugin)Instance).Logger.LogError((object)"Failure: ForcePublicChatFromAllPlayers Fallback");
						((BaseUnityPlugin)Instance).Logger.LogError((object)ex2);
					}
				}
			}
			WriteLog($"ForcePublicChatSent Source={source} SentCount={num} Message={message}");
		}

		internal static void TryBroadcastPublicBonusChatIfPerfect(string source)
		{
			if (ModEnabled.Value && SemiFunc.IsMasterClientOrSingleplayer() && IsTrackedGameplayLevel() && InitialSnapshotTaken && !PublicBonusChatSentThisLevel && IsPerfectExtraction())
			{
				GetCurrentGrantedBonusAmounts(out var _, out var rewardStreakNumber, out var _, out var _, out var _, out var _, out var _, out var _, out var _, out var regularBonusAmount, out var _, out var _, out var _, out var totalBonusAmount, out var _, out var isNoLossPerfectExtraction);
				string template = (isNoLossPerfectExtraction ? NoLossPerfectBonusPublicChatMessage.Value : PerfectBonusPublicChatMessage.Value);
				int num = (isNoLossPerfectExtraction ? totalBonusAmount : regularBonusAmount);
				string text = BuildPublicBonusChatMessage(template, num);
				if (string.IsNullOrEmpty(text))
				{
					WriteLog("PublicBonusChatSkipped Source=" + source + " Reason=EmptyMessage");
					return;
				}
				ForcePublicChatFromAllPlayers(text, source);
				PublicBonusChatSentThisLevel = true;
				WriteLog($"PublicBonusChatSent Source={source} NoLoss={isNoLossPerfectExtraction} RewardStreak={rewardStreakNumber} ChatBonusAmount={num} Message={text}");
			}
		}

		internal static void ShowPerfectBonusUI(int totalBonusPercent, int totalBonusAmount, int streakValue, int noLossExtraBonusPercentApplied)
		{
			//IL_007b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0080: Unknown result type (might be due to invalid IL or missing references)
			if (!ShowBonusUI.Value)
			{
				return;
			}
			try
			{
				string text = ((noLossExtraBonusPercentApplied <= 0) ? $"PERFECT EXTRACTION BONUS\n+{totalBonusPercent}%  +{SemiFunc.DollarGetString(totalBonusAmount)}\nSTREAK x{streakValue}" : $"PERFECT EXTRACTION BONUS\n+{totalBonusPercent}%  +{SemiFunc.DollarGetString(totalBonusAmount)}\nSTREAK x{streakValue}\nNO LOSS BONUS +{noLossExtraBonusPercentApplied}%");
				SemiFunc.UIFocusText(text, Color.yellow, Color.green, 4f);
			}
			catch (Exception ex)
			{
				if ((Object)(object)Instance != (Object)null)
				{
					((BaseUnityPlugin)Instance).Logger.LogError((object)"Failure: ShowPerfectBonusUI");
					((BaseUnityPlugin)Instance).Logger.LogError((object)ex);
				}
			}
		}

		internal static void TryApplyPerfectExtractionBonus()
		{
			if (!ModEnabled.Value || !SemiFunc.IsMasterClientOrSingleplayer() || !IsTrackedGameplayLevel() || BonusGrantedThisLevel)
			{
				return;
			}
			bool flag = IsPerfectExtraction();
			bool flag2 = IsNoLossPerfectExtraction();
			int missingInitialValuableCount = GetMissingInitialValuableCount();
			WriteLog($"BonusEvaluation LevelKey={CurrentTrackedLevelKey} SnapshotTaken={InitialSnapshotTaken} Delivered={DeliveredInitialValuableIds.Count} Initial={InitialValuableIds.Count} Missing={missingInitialValuableCount} Perfect={flag} NoLossEligible={NoLossExtraBonusEligible} NoLossExtraBonusPercent={Mathf.Max(0, NoLossExtraBonusPercent.Value)} ValueLostCount={ValueLostValuableIds.Count}");
			if (!flag)
			{
				int perfectClearStreak = PerfectClearStreak;
				if (ResetStreakOnNonPerfectClear.Value)
				{
					PerfectClearStreak = 0;
					WriteLog($"ResetStreak Reason=NonPerfectClear Previous={perfectClearStreak} Current={PerfectClearStreak}");
				}
				WriteLog($"BonusResult Granted=False Reason=NotPerfectExtraction Delivered={DeliveredInitialValuableIds.Count} Initial={InitialValuableIds.Count} Missing={missingInitialValuableCount} CurrentStreak={PerfectClearStreak}");
				return;
			}
			TryBroadcastPublicBonusChatIfPerfect("LevelCompletionFallback");
			GetCurrentGrantedBonusAmounts(out var currencyBefore, out var rewardStreakNumber, out var baseBonusPercent, out var streakStepCount, out var streakAddPercentPerStep, out var streakBonusPercent, out var uncappedBonusPercent, out var regularBonusPercent, out var maxBonusPercent, out var regularBonusAmount, out var noLossExtraBonusPercentApplied, out var noLossExtraBonusAmount, out var totalBonusPercentForDisplay, out var totalBonusAmount, out var currencyAfter, out var isNoLossPerfectExtraction);
			WriteLog($"RegularBonusRateCalculation RewardStreak={rewardStreakNumber} Base={baseBonusPercent}% + Streak({streakStepCount} x {streakAddPercentPerStep}%)={streakBonusPercent}% => Uncapped={uncappedBonusPercent}% => Final={regularBonusPercent}% Max={maxBonusPercent}%");
			WriteLog($"RegularBonusAmountCalculation floor({currencyBefore} x ({regularBonusPercent} / 100)) = {regularBonusAmount}");
			if (isNoLossPerfectExtraction)
			{
				WriteLog($"NoLossExtraBonusRateCalculation RewardStreak={rewardStreakNumber} Extra={noLossExtraBonusPercentApplied}%");
				WriteLog($"NoLossExtraBonusAmountCalculation floor({currencyBefore} x ({noLossExtraBonusPercentApplied} / 100)) = {noLossExtraBonusAmount}");
			}
			else
			{
				WriteLog($"NoLossExtraBonusResult Granted=False Eligible={NoLossExtraBonusEligible} ConfigPercent={Mathf.Max(0, NoLossExtraBonusPercent.Value)} ValueLostCount={ValueLostValuableIds.Count}");
			}
			WriteLog($"TotalBonusAmountCalculation {regularBonusAmount} + {noLossExtraBonusAmount} = {totalBonusAmount}");
			SemiFunc.StatSetRunCurrency(currencyAfter);
			PerfectClearStreak++;
			BonusGrantedThisLevel = true;
			WriteLog($"BonusResult Granted=True RewardStreak={PerfectClearStreak} RegularBonusPercent={regularBonusPercent}% RegularBonusAmount={regularBonusAmount} NoLossExtraBonusPercent={noLossExtraBonusPercentApplied}% NoLossExtraBonusAmount={noLossExtraBonusAmount} TotalBonusPercent={totalBonusPercentForDisplay}% TotalBonusAmount={totalBonusAmount} CurrencyBefore={currencyBefore} CurrencyAfter={currencyAfter}");
			ShowPerfectBonusUI(totalBonusPercentForDisplay, totalBonusAmount, PerfectClearStreak, noLossExtraBonusPercentApplied);
		}
	}
}