Decompiled source of TaxmansCurseHauntedLoot v0.2.9

TaxmansCurseHauntedLoot.dll

Decompiled a day ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;
using UnityEngine.SceneManagement;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("UL")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.2.9.0")]
[assembly: AssemblyInformationalVersion("0.2.9")]
[assembly: AssemblyProduct("TaxmansCurseHauntedLoot")]
[assembly: AssemblyTitle("TaxmansCurseHauntedLoot")]
[assembly: AssemblyVersion("0.2.9.0")]
[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.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace TaxmansCurseHauntedLoot
{
	internal sealed class CurseAnnouncer
	{
		private enum TruckSendResult
		{
			Sent,
			Waiting,
			Failed
		}

		private enum TTSAnnouncementKind
		{
			Reveal,
			Transfer,
			Important
		}

		private sealed class PendingTruckAnnouncement
		{
			internal readonly string Key;

			internal readonly string PlayerName;

			internal readonly string Message;

			internal readonly float QueuedAt;

			internal PendingTruckAnnouncement(string key, string playerName, string message, float queuedAt)
			{
				Key = key;
				PlayerName = playerName;
				Message = message;
				QueuedAt = queuedAt;
			}
		}

		[CompilerGenerated]
		private sealed class <TruckRetryLoop>d__24 : IEnumerator<object>, IEnumerator, IDisposable
		{
			private int <>1__state;

			private object <>2__current;

			public CurseAnnouncer <>4__this;

			object IEnumerator<object>.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			object IEnumerator.Current
			{
				[DebuggerHidden]
				get
				{
					return <>2__current;
				}
			}

			[DebuggerHidden]
			public <TruckRetryLoop>d__24(int <>1__state)
			{
				this.<>1__state = <>1__state;
			}

			[DebuggerHidden]
			void IDisposable.Dispose()
			{
				<>1__state = -2;
			}

			private bool MoveNext()
			{
				//IL_01dc: Unknown result type (might be due to invalid IL or missing references)
				//IL_01e6: Expected O, but got Unknown
				int num = <>1__state;
				CurseAnnouncer curseAnnouncer = <>4__this;
				switch (num)
				{
				default:
					return false;
				case 0:
					<>1__state = -1;
					break;
				case 1:
					<>1__state = -1;
					break;
				}
				while (curseAnnouncer.pendingTruckAnnouncements.Count > 0)
				{
					foreach (PendingTruckAnnouncement item in new List<PendingTruckAnnouncement>(curseAnnouncer.pendingTruckAnnouncements.Values))
					{
						if (!curseAnnouncer.pendingTruckAnnouncements.ContainsKey(item.Key))
						{
							continue;
						}
						string reason;
						TruckSendResult truckSendResult = curseAnnouncer.TrySendTruckNow(item, out reason);
						if (truckSendResult == TruckSendResult.Sent)
						{
							curseAnnouncer.pendingTruckAnnouncements.Remove(item.Key);
							Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Truck announcement sent after retry: key=" + item.Key + ", message=" + item.Message));
						}
						else
						{
							if (!curseAnnouncer.pendingTruckAnnouncements.ContainsKey(item.Key))
							{
								continue;
							}
							if (truckSendResult == TruckSendResult.Waiting)
							{
								Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Truck announcement waiting: TruckScreenText is typing.");
							}
							if (!(Time.realtimeSinceStartup - item.QueuedAt >= 12f))
							{
								continue;
							}
							if (truckSendResult == TruckSendResult.Waiting && ForceTruckAnnouncement && curseAnnouncer.TryForceCompleteTruckTyping(out var _))
							{
								if (curseAnnouncer.TrySendTruckNow(item, out var reason3) == TruckSendResult.Sent)
								{
									curseAnnouncer.pendingTruckAnnouncements.Remove(item.Key);
									Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Truck announcement sent after retry: key=" + item.Key + ", message=" + item.Message));
									continue;
								}
								reason = reason3;
							}
							else if (truckSendResult == TruckSendResult.Waiting && ForceTruckAnnouncement)
							{
								Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Truck force announcement skipped: no safe interrupt method.");
								reason = (string.IsNullOrWhiteSpace(reason) ? "TruckScreenText remained busy and force failed" : reason);
							}
							curseAnnouncer.pendingTruckAnnouncements.Remove(item.Key);
							Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Truck announcement failed after retry: key=" + item.Key + ", reason=" + reason));
						}
					}
					if (curseAnnouncer.pendingTruckAnnouncements.Count > 0)
					{
						<>2__current = (object)new WaitForSeconds(0.5f);
						<>1__state = 1;
						return true;
					}
				}
				curseAnnouncer.truckRetryCoroutine = null;
				return false;
			}

			bool IEnumerator.MoveNext()
			{
				//ILSpy generated this explicit interface implementation from .override directive in MoveNext
				return this.MoveNext();
			}

			[DebuggerHidden]
			void IEnumerator.Reset()
			{
				throw new NotSupportedException();
			}
		}

		private const float TruckRetrySeconds = 12f;

		private const float TruckRetryIntervalSeconds = 0.5f;

		private static readonly bool ForceTruckAnnouncement = false;

		private static readonly bool EnableDeveloperBottomObjective = false;

		private static readonly bool TruckAnnouncementCompact = true;

		private const float TruckAnnouncementCooldownSeconds = 4f;

		private FieldRef<TruckScreenText, bool> isTypingRef;

		private MethodInfo forceCompleteChatMessageMethod;

		private bool loggedObjectiveDiscovery;

		private readonly Dictionary<string, PendingTruckAnnouncement> pendingTruckAnnouncements = new Dictionary<string, PendingTruckAnnouncement>();

		private readonly Dictionary<string, float> truckCooldownUntil = new Dictionary<string, float>();

		private Coroutine truckRetryCoroutine;

		private float lastNonRevealTtsTime = -9999f;

		internal bool TryAnnounceReveal(PlayerAvatar speaker, string playerName, CurseType curse)
		{
			string displayName = GetDisplayName(curse);
			string shortRule = GetShortRule(curse);
			bool flag = false;
			flag |= TryAnnounceViaTts(speaker, BuildCompactChatMessage(playerName, displayName, shortRule), TTSAnnouncementKind.Reveal);
			if (EnableDeveloperBottomObjective)
			{
				flag |= TryAnnounceViaBottomObjective(playerName + " is cursed: " + displayName, revealMessage: true);
			}
			return flag | QueueTruckAnnouncement(BuildTruckKey("reveal", playerName, displayName), playerName, BuildTruckRevealMessage(playerName, displayName, GetTruckShortRule(curse)));
		}

		internal bool TryAnnounceTransfer(PlayerAvatar speaker, string playerName, string message)
		{
			bool flag = false;
			flag |= TryAnnounceViaTts(speaker, message, TTSAnnouncementKind.Transfer);
			if (EnableDeveloperBottomObjective)
			{
				flag |= TryAnnounceViaBottomObjective(message, revealMessage: false);
			}
			flag |= QueueTruckAnnouncement(BuildTruckKey("transfer", playerName, message), playerName, BuildTruckTransferMessage(playerName));
			if (!flag)
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Host-log announcement fallback: " + message));
			}
			return flag;
		}

		private bool TryAnnounceViaTts(PlayerAvatar speaker, string message, TTSAnnouncementKind kind)
		{
			CurseConfig modConfig = Plugin.ModConfig;
			if (modConfig == null || !modConfig.EnableTTS.Value)
			{
				Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] TTS skipped: disabled.");
				return false;
			}
			TTSMode value = modConfig.TTSMode.Value;
			if (!ModeAllows(kind, value, out var reason))
			{
				Plugin.Log.LogInfo((object)string.Format("{0} TTS skipped: mode={1}, reason={2}", "[Taxman's Curse: Haunted Loot]", value, reason));
				return false;
			}
			if (kind != 0)
			{
				float num = Mathf.Max(0f, modConfig.TTSCooldownSeconds.Value) - (Time.realtimeSinceStartup - lastNonRevealTtsTime);
				if (num > 0f)
				{
					Plugin.Log.LogInfo((object)string.Format("{0} TTS skipped: cooldown remaining={1:0.0}", "[Taxman's Curse: Haunted Loot]", num));
					return false;
				}
			}
			bool num2 = TryAnnounceViaTargetedWorldSpaceTts(speaker, message);
			if (num2)
			{
				if (kind != 0)
				{
					lastNonRevealTtsTime = Time.realtimeSinceStartup;
				}
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] TTS sent: " + message));
			}
			return num2;
		}

		private static bool ModeAllows(TTSAnnouncementKind kind, TTSMode mode, out string reason)
		{
			reason = "";
			switch (kind)
			{
			case TTSAnnouncementKind.Reveal:
				return true;
			case TTSAnnouncementKind.Transfer:
				if (mode == TTSMode.RevealAndTransfer || mode == TTSMode.AllImportant)
				{
					return true;
				}
				reason = "transfer announcements disabled";
				return false;
			default:
				if (mode == TTSMode.AllImportant)
				{
					return true;
				}
				reason = "important event announcements disabled";
				return false;
			}
		}

		private bool TryAnnounceViaTargetedWorldSpaceTts(PlayerAvatar speaker, string message)
		{
			try
			{
				List<PlayerAvatar> announcementTargets = GetAnnouncementTargets();
				if (announcementTargets.Count == 0)
				{
					Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] WorldSpaceTTS targeted send skipped: no player targets; falling back to host speaker.");
					return TryAnnounceViaHostSpeakerChat(speaker, message);
				}
				bool result = false;
				foreach (PlayerAvatar item in announcementTargets)
				{
					if (!Object.op_Implicit((Object)(object)item) || !Object.op_Implicit((Object)(object)item.photonView))
					{
						continue;
					}
					bool flag = false;
					PhotonView photonView = item.photonView;
					Player val = null;
					try
					{
						val = photonView.Owner;
					}
					catch
					{
					}
					string text = SafePlayerName(item);
					Plugin.Log.LogInfo((object)string.Format("{0} TTS target: speaker={1}, viewId={2}, owner={3}, isLocalAvatar={4}, PhotonNetwork.IsMasterClient={5}", "[Taxman's Curse: Haunted Loot]", text, photonView.ViewID, FormatPhotonPlayer(val), GameAccess.IsPlayerLocal(item), PhotonNetwork.IsMasterClient));
					if (SemiFunc.IsMultiplayer())
					{
						if (val != null)
						{
							photonView.RPC("ChatMessageSendRPC", val, new object[2]
							{
								message ?? "",
								flag
							});
						}
						else
						{
							photonView.RPC("ChatMessageSendRPC", (RpcTarget)0, new object[2]
							{
								message ?? "",
								flag
							});
						}
						Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] TTS send route: targeted ChatMessageSendRPC to owner=" + FormatPhotonPlayer(val) + ", localCall=False, message=" + message));
					}
					else
					{
						item.ChatMessageSend(message ?? "");
						Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] TTS send route: singleplayer ChatMessageSend local call, message=" + message));
					}
					result = true;
				}
				return result;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] WorldSpaceTTS targeted send failed: " + ex.GetType().Name + ": " + ex.Message + "; falling back to host speaker."));
				return TryAnnounceViaHostSpeakerChat(speaker, message);
			}
		}

		private bool TryAnnounceViaHostSpeakerChat(PlayerAvatar speaker, string message)
		{
			try
			{
				PlayerAvatar val = SelectHostSpeaker(speaker);
				if (!Object.op_Implicit((Object)(object)val) || !Object.op_Implicit((Object)(object)val.photonView))
				{
					Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Chat announcement failed: no safe host speaker.");
					return false;
				}
				PhotonView photonView = val.photonView;
				Plugin.Log.LogInfo((object)string.Format("{0} TTS fallback target: speaker={1}, viewId={2}, owner={3}, isLocalAvatar={4}, PhotonNetwork.IsMasterClient={5}", "[Taxman's Curse: Haunted Loot]", SafePlayerName(val), photonView.ViewID, FormatPhotonPlayer(photonView.Owner), GameAccess.IsPlayerLocal(val), PhotonNetwork.IsMasterClient));
				val.ChatMessageSend(message ?? "");
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Chat announcement sent through PlayerAvatar.ChatMessageSend using host speaker=" + SafePlayerName(val) + ": " + message));
				return true;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Chat announcement failed: " + ex.GetType().Name + ": " + ex.Message));
				return false;
			}
		}

		private static List<PlayerAvatar> GetAnnouncementTargets()
		{
			List<PlayerAvatar> list = new List<PlayerAvatar>();
			try
			{
				if (GameDirector.instance?.PlayerList != null)
				{
					foreach (PlayerAvatar player in GameDirector.instance.PlayerList)
					{
						if (Object.op_Implicit((Object)(object)player) && Object.op_Implicit((Object)(object)player.photonView) && !list.Contains(player))
						{
							list.Add(player);
						}
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] TTS target list from GameDirector failed: " + ex.GetType().Name + ": " + ex.Message));
			}
			if (list.Count == 0 && Object.op_Implicit((Object)(object)PlayerAvatar.instance) && Object.op_Implicit((Object)(object)PlayerAvatar.instance.photonView))
			{
				list.Add(PlayerAvatar.instance);
			}
			return list;
		}

		private static string FormatPhotonPlayer(Player player)
		{
			if (player == null)
			{
				return "<null>";
			}
			return string.Format("{0}#{1}", player.NickName ?? "unknown", player.ActorNumber);
		}

		private bool TryAnnounceViaBottomObjective(string message, bool revealMessage)
		{
			//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)
			LogObjectiveDiscoveryOnce();
			Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Bottom objective announcement requested: " + message));
			try
			{
				if (!Object.op_Implicit((Object)(object)MissionUI.instance))
				{
					Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Bottom objective announcement skipped: MissionUI.instance is null.");
					return false;
				}
				SemiFunc.UIFocusText(message ?? "", Color.yellow, Color.red, revealMessage ? 4f : 3f);
				Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Bottom objective announcement sent.");
				return true;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Bottom objective announcement skipped: " + ex.GetType().Name + ": " + ex.Message));
				return false;
			}
		}

		private bool QueueTruckAnnouncement(string key, string playerName, string message)
		{
			Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Truck announcement requested: " + message));
			float realtimeSinceStartup = Time.realtimeSinceStartup;
			if (truckCooldownUntil.TryGetValue(key, out var value) && value > realtimeSinceStartup)
			{
				Plugin.Log.LogInfo((object)string.Format("{0} Truck announcement skipped cooldown: key={1}, remaining={2:0.0}", "[Taxman's Curse: Haunted Loot]", key, value - realtimeSinceStartup));
				return false;
			}
			if (pendingTruckAnnouncements.ContainsKey(key))
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Truck announcement skipped duplicate: key=" + key));
				return false;
			}
			PendingTruckAnnouncement value2 = new PendingTruckAnnouncement(key, playerName ?? "", message ?? "", realtimeSinceStartup);
			pendingTruckAnnouncements.Add(key, value2);
			truckCooldownUntil[key] = realtimeSinceStartup + Mathf.Max(0f, 4f);
			Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Truck announcement queued: key=" + key + ", message=" + message));
			if (truckRetryCoroutine == null)
			{
				truckRetryCoroutine = Plugin.StartAnnouncementCoroutine(TruckRetryLoop());
				if (truckRetryCoroutine == null)
				{
					pendingTruckAnnouncements.Remove(key);
					Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Truck announcement failed after retry: key=" + key + ", reason=no plugin coroutine host."));
					return false;
				}
			}
			return true;
		}

		internal void ClearTruckQueue(string reason)
		{
			int count = pendingTruckAnnouncements.Count;
			pendingTruckAnnouncements.Clear();
			truckCooldownUntil.Clear();
			Plugin.Log.LogInfo((object)string.Format("{0} Truck announcement queue cleared: {1} ({2} pending)", "[Taxman's Curse: Haunted Loot]", reason, count));
		}

		[IteratorStateMachine(typeof(<TruckRetryLoop>d__24))]
		private IEnumerator TruckRetryLoop()
		{
			//yield-return decompiler failed: Unexpected instruction in Iterator.Dispose()
			return new <TruckRetryLoop>d__24(0)
			{
				<>4__this = this
			};
		}

		private TruckSendResult TrySendTruckNow(PendingTruckAnnouncement pending, out string reason)
		{
			try
			{
				TruckScreenText instance = TruckScreenText.instance;
				Plugin.Log.LogInfo((object)string.Format("{0} TruckScreenText instance found: {1}", "[Taxman's Curse: Haunted Loot]", (Object)(object)instance != (Object)null));
				if (!Object.op_Implicit((Object)(object)instance))
				{
					reason = "TruckScreenText.instance is null";
					return TruckSendResult.Failed;
				}
				bool flag = false;
				try
				{
					if (isTypingRef == null)
					{
						isTypingRef = AccessTools.FieldRefAccess<TruckScreenText, bool>("isTyping");
					}
					flag = isTypingRef.Invoke(instance);
				}
				catch (Exception ex)
				{
					Plugin.Log.LogWarning((object)("[Taxman's Curse: Haunted Loot] Public announcement typing-state probe failed; attempting send anyway. " + ex.GetType().Name + ": " + ex.Message));
				}
				if (flag)
				{
					reason = "TruckScreenText is typing";
					return TruckSendResult.Waiting;
				}
				instance.MessageSendCustom(pending.PlayerName ?? "", pending.Message ?? "", 0);
				reason = "";
				return TruckSendResult.Sent;
			}
			catch (Exception ex2)
			{
				reason = ex2.GetType().Name + ": " + ex2.Message;
				return TruckSendResult.Failed;
			}
		}

		private bool TryForceCompleteTruckTyping(out string reason)
		{
			try
			{
				TruckScreenText instance = TruckScreenText.instance;
				if (!Object.op_Implicit((Object)(object)instance))
				{
					reason = "TruckScreenText.instance is null";
					return false;
				}
				if ((object)forceCompleteChatMessageMethod == null)
				{
					forceCompleteChatMessageMethod = AccessTools.Method(typeof(TruckScreenText), "ForceCompleteChatMessage", (Type[])null, (Type[])null);
				}
				if (forceCompleteChatMessageMethod == null)
				{
					reason = "ForceCompleteChatMessage method not found";
					return false;
				}
				forceCompleteChatMessageMethod.Invoke(instance, Array.Empty<object>());
				reason = "ForceCompleteChatMessage";
				return true;
			}
			catch (Exception ex)
			{
				reason = ex.GetType().Name + ": " + ex.Message;
				return false;
			}
		}

		private static PlayerAvatar SelectHostSpeaker(PlayerAvatar touchSpeaker)
		{
			try
			{
				if (Object.op_Implicit((Object)(object)touchSpeaker) && GameAccess.IsPlayerLocal(touchSpeaker) && Object.op_Implicit((Object)(object)touchSpeaker.photonView))
				{
					return touchSpeaker;
				}
			}
			catch
			{
			}
			try
			{
				if (GameDirector.instance?.PlayerList == null)
				{
					return null;
				}
				foreach (PlayerAvatar player in GameDirector.instance.PlayerList)
				{
					if (Object.op_Implicit((Object)(object)player) && Object.op_Implicit((Object)(object)player.photonView) && GameAccess.IsPlayerLocal(player))
					{
						return player;
					}
				}
			}
			catch
			{
			}
			return null;
		}

		private static string SafePlayerName(PlayerAvatar player)
		{
			if (!Object.op_Implicit((Object)(object)player))
			{
				return "Unknown";
			}
			try
			{
				string text = SemiFunc.PlayerGetName(player);
				return string.IsNullOrWhiteSpace(text) ? ((Object)player).name : text;
			}
			catch
			{
				return ((Object)player).name;
			}
		}

		private void LogObjectiveDiscoveryOnce()
		{
			if (!loggedObjectiveDiscovery)
			{
				loggedObjectiveDiscovery = true;
				Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Bottom objective announcement channel found: SemiFunc.UIFocusText -> MissionUI.MissionText");
				Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Bottom objective visible target: local host only");
			}
		}

		private static string BuildCompactChatMessage(string playerName, string curseName, string shortRule)
		{
			string text = playerName + " is cursed: " + curseName + ".";
			if (!string.IsNullOrWhiteSpace(shortRule))
			{
				text = text + " " + shortRule;
			}
			return text;
		}

		private static string BuildTruckRevealMessage(string playerName, string curseName, string shortRule)
		{
			if (TruckAnnouncementCompact)
			{
				return "TAXMAN'S CURSE:\n" + playerName + " cursed: " + curseName + "\n" + shortRule;
			}
			string text = playerName + " is cursed: " + curseName + ".";
			if (!string.IsNullOrWhiteSpace(shortRule))
			{
				text = text + " " + shortRule;
			}
			return text;
		}

		private static string BuildTruckTransferMessage(string playerName)
		{
			return "TAXMAN'S CURSE:\nCurse moved to " + playerName + "\nLast touch owns it";
		}

		private static string BuildTruckKey(string type, string playerName, string value)
		{
			return type + ":" + playerName + ":" + value;
		}

		private static string GetDisplayName(CurseType curse)
		{
			return curse switch
			{
				CurseType.LootBait => "Loot Bait", 
				CurseType.GlassTouch => "Glass Touch", 
				CurseType.ReverseLuck => "Reverse Luck", 
				CurseType.MimicValuable => "Mimic Valuable", 
				CurseType.GoldenTrap => "Golden Trap", 
				CurseType.LastTouchCurse => "Last-Touch Curse", 
				_ => "Unknown Curse", 
			};
		}

		private static string GetShortRule(CurseType curse)
		{
			return curse switch
			{
				CurseType.LootBait => "Every valuable they touch attracts danger.", 
				CurseType.GlassTouch => "Loot they touch becomes haunted.", 
				CurseType.ReverseLuck => "Their luck can help or betray the team.", 
				CurseType.MimicValuable => "That was not just a valuable.", 
				CurseType.GoldenTrap => "Great value, terrible consequences.", 
				CurseType.LastTouchCurse => "The last one to touch it owns the curse.", 
				_ => "", 
			};
		}

		private static string GetTruckShortRule(CurseType curse)
		{
			return curse switch
			{
				CurseType.LootBait => "Touches attract danger", 
				CurseType.GlassTouch => "Touched loot is haunted", 
				CurseType.ReverseLuck => "Luck may help or betray", 
				CurseType.MimicValuable => "That was not just loot", 
				CurseType.GoldenTrap => "Greed has consequences", 
				CurseType.LastTouchCurse => "Last touch owns it", 
				_ => "Curse is active", 
			};
		}
	}
	internal sealed class CurseEffects
	{
		private static readonly FieldRef<EnemyDirector, float> EnemyActionAmountRef = AccessTools.FieldRefAccess<EnemyDirector, float>("enemyActionAmount");

		public bool ApplyDangerPressure(Vector3 position, int amount, string reason)
		{
			//IL_006b: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (!LazyCurseDirector.IsHostLike())
				{
					Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Enemy pressure skipped: not host/master.");
					return false;
				}
				EnemyDirector instance = EnemyDirector.instance;
				if (!Object.op_Implicit((Object)(object)instance))
				{
					Plugin.Log.LogWarning((object)("[Taxman's Curse: Haunted Loot] Enemy pressure fallback: EnemyDirector.instance not found for " + reason + "."));
					return false;
				}
				float num = Mathf.Clamp(12f + (float)amount * 4f, 12f, 28f);
				instance.SetInvestigate(position, num, false);
				try
				{
					EnemyActionAmountRef.Invoke(instance) += (float)Mathf.Clamp(amount, 1, 3) * 12f;
				}
				catch (Exception ex)
				{
					Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Enemy action pressure field unavailable; investigation-only pressure applied. " + ex.GetType().Name + ": " + ex.Message));
				}
				Plugin.Log.LogInfo((object)string.Format("{0} Enemy/danger effect applied: vanilla EnemyDirector.SetInvestigate radius={1:0.0}, reason={2}.", "[Taxman's Curse: Haunted Loot]", num, reason));
				return true;
			}
			catch (Exception ex2)
			{
				Plugin.Log.LogWarning((object)("[Taxman's Curse: Haunted Loot] Enemy pressure fallback for " + reason + ": " + ex2.GetType().Name + ": " + ex2.Message));
				return false;
			}
		}

		public bool TryApplySmallBonus(ValuableObject valuable, string reason)
		{
			if (!LazyCurseDirector.IsHostLike())
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Reward bonus skipped for " + reason + ": not host/master."));
				return false;
			}
			if (!Object.op_Implicit((Object)(object)valuable))
			{
				Plugin.Log.LogWarning((object)("[Taxman's Curse: Haunted Loot] Reward bonus fallback for " + reason + ": valuable reference missing."));
				return false;
			}
			try
			{
				float num = GameAccess.DollarValueCurrent(valuable);
				if (num <= 0f)
				{
					Plugin.Log.LogInfo((object)string.Format("{0} Reward bonus skipped for {1}: current value is {2}.", "[Taxman's Curse: Haunted Loot]", reason, num));
					return false;
				}
				float num2 = Mathf.Clamp(Mathf.Round(num * 0.05f / 100f) * 100f, 100f, 500f);
				float num3 = Mathf.Min(GameAccess.DollarValueCurrent(valuable) + num2, GameAccess.DollarValueOriginal(valuable) + 500f);
				GameAccess.DollarValueCurrentSet(valuable, num3);
				Plugin.Log.LogInfo((object)string.Format("{0} Reward effect applied: +{1:0} to {2} for {3}; current={4:0}.", "[Taxman's Curse: Haunted Loot]", num2, ((Object)valuable).name, reason, num3));
				return true;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[Taxman's Curse: Haunted Loot] Reward manipulation fallback for " + reason + ": " + ex.GetType().Name + ": " + ex.Message));
				return false;
			}
		}
	}
	public sealed class CurseState
	{
		public bool Active;

		public bool Revealed;

		public CurseType CurrentCurse;

		public int CursedPlayerId;

		public string CursedPlayerName = "";

		public int CursedValuableId;

		public int TriggerCount;

		public float LastTriggerTime = -9999f;

		public readonly HashSet<int> TouchedValuables = new HashSet<int>();

		public readonly HashSet<int> MarkedValuables = new HashSet<int>();

		public int LastTouchPlayerId;

		public string LastTouchPlayerName = "";

		public int TotalTouches;

		public int TotalDrops;

		public int TotalBreaks;

		public int TotalExtractions;

		public int TotalBonuses;

		public int TotalPressureEvents;

		public bool Cured;

		public bool Sacrificed;

		public bool Completed;

		public bool Failed;

		public float RevealTime = -1f;

		public void Reset()
		{
			Active = false;
			Revealed = false;
			CurrentCurse = CurseType.None;
			CursedPlayerId = 0;
			CursedPlayerName = "";
			CursedValuableId = 0;
			TriggerCount = 0;
			LastTriggerTime = -9999f;
			TouchedValuables.Clear();
			MarkedValuables.Clear();
			LastTouchPlayerId = 0;
			LastTouchPlayerName = "";
			TotalTouches = 0;
			TotalDrops = 0;
			TotalBreaks = 0;
			TotalExtractions = 0;
			TotalBonuses = 0;
			TotalPressureEvents = 0;
			Cured = false;
			Sacrificed = false;
			Completed = false;
			Failed = false;
			RevealTime = -1f;
		}
	}
	public enum CurseType
	{
		None,
		LootBait,
		Butterfingers,
		GreedTax,
		GlassTouch,
		ReverseLuck,
		MimicValuable,
		GoldenTrap,
		LastTouchCurse,
		ProtectTheCursed,
		SacrificeCurse
	}
	internal static class GameAccess
	{
		private static readonly FieldRef<ValuableObject, float> DollarValueCurrentRef = AccessTools.FieldRefAccess<ValuableObject, float>("dollarValueCurrent");

		private static readonly FieldRef<ValuableObject, float> DollarValueOriginalRef = AccessTools.FieldRefAccess<ValuableObject, float>("dollarValueOriginal");

		private static readonly FieldRef<PhysGrabObject, PlayerAvatar> LastPlayerGrabbingRef = AccessTools.FieldRefAccess<PhysGrabObject, PlayerAvatar>("lastPlayerGrabbing");

		private static readonly FieldRef<PhysGrabObject, PhotonView> PhysPhotonViewRef = AccessTools.FieldRefAccess<PhysGrabObject, PhotonView>("photonView");

		private static readonly FieldRef<PlayerAvatar, bool> PlayerDisabledRef = AccessTools.FieldRefAccess<PlayerAvatar, bool>("isDisabled");

		private static readonly FieldRef<PlayerAvatar, bool> PlayerLocalRef = AccessTools.FieldRefAccess<PlayerAvatar, bool>("isLocal");

		private static readonly FieldRef<PlayerAvatar, bool> PlayerDeadSetRef = AccessTools.FieldRefAccess<PlayerAvatar, bool>("deadSet");

		internal static float DollarValueCurrent(ValuableObject valuable)
		{
			if (!Object.op_Implicit((Object)(object)valuable))
			{
				return 0f;
			}
			return DollarValueCurrentRef.Invoke(valuable);
		}

		internal static float DollarValueOriginal(ValuableObject valuable)
		{
			if (!Object.op_Implicit((Object)(object)valuable))
			{
				return 0f;
			}
			return DollarValueOriginalRef.Invoke(valuable);
		}

		internal static void DollarValueCurrentSet(ValuableObject valuable, float value)
		{
			if (Object.op_Implicit((Object)(object)valuable))
			{
				DollarValueCurrentRef.Invoke(valuable) = value;
			}
		}

		internal static PlayerAvatar LastPlayerGrabbing(PhysGrabObject obj)
		{
			if (!Object.op_Implicit((Object)(object)obj))
			{
				return null;
			}
			return LastPlayerGrabbingRef.Invoke(obj);
		}

		internal static PhotonView PhysPhotonView(PhysGrabObject obj)
		{
			if (!Object.op_Implicit((Object)(object)obj))
			{
				return null;
			}
			return PhysPhotonViewRef.Invoke(obj);
		}

		internal static bool IsPlayerDisabled(PlayerAvatar player)
		{
			if (Object.op_Implicit((Object)(object)player))
			{
				return PlayerDisabledRef.Invoke(player);
			}
			return false;
		}

		internal static bool IsPlayerLocal(PlayerAvatar player)
		{
			if (Object.op_Implicit((Object)(object)player))
			{
				return PlayerLocalRef.Invoke(player);
			}
			return false;
		}

		internal static bool IsPlayerDead(PlayerAvatar player)
		{
			if (Object.op_Implicit((Object)(object)player))
			{
				return PlayerDeadSetRef.Invoke(player);
			}
			return false;
		}

		internal static bool Try(string context, Action action)
		{
			try
			{
				action?.Invoke();
				return true;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[Taxman's Curse: Haunted Loot] " + context + " failed: " + ex.GetType().Name + ": " + ex.Message));
				return false;
			}
		}
	}
	internal sealed class LazyCurseDirector
	{
		private readonly CurseConfig config;

		private readonly CurseAnnouncer announcer = new CurseAnnouncer();

		private readonly CurseEffects effects = new CurseEffects();

		private readonly CurseState state = new CurseState();

		private int lastSelectedPlayerId;

		private float lastTransferAnnounceTime = -9999f;

		private string currentSessionKey = "";

		private int cursesStartedThisSession;

		internal LazyCurseDirector(CurseConfig config)
		{
			this.config = config;
		}

		internal static bool IsHostLike()
		{
			try
			{
				return SemiFunc.IsMasterClientOrSingleplayer();
			}
			catch
			{
				return !PhotonNetwork.InRoom || PhotonNetwork.IsMasterClient;
			}
		}

		internal void OnGrabStartedRpc(PhysGrabObject obj, int playerPhotonId)
		{
			if (!config.Enabled.Value)
			{
				return;
			}
			if (config.HostOnly.Value && !IsHostLike())
			{
				DebugLog("Touch ignored: not host/master.");
			}
			else
			{
				if (!Object.op_Implicit((Object)(object)obj))
				{
					return;
				}
				ValuableObject component = ((Component)obj).GetComponent<ValuableObject>();
				if (!Object.op_Implicit((Object)(object)component))
				{
					return;
				}
				PhysGrabber val = TryGetGrabber(playerPhotonId);
				PlayerAvatar val2 = (Object.op_Implicit((Object)(object)val) ? val.playerAvatar : null);
				if (!Object.op_Implicit((Object)(object)val2))
				{
					DebugLog($"Touch ignored: no valid player for photonViewId={playerPhotonId}.");
					return;
				}
				if (!IsLikelyGameplayTouch())
				{
					DebugLog("Touch ignored: no active run gameplay context detected.");
					return;
				}
				int playerId = GetPlayerId(val2);
				int objectId = GetObjectId(obj);
				string playerName = GetPlayerName(val2);
				string text = BuildGameplaySessionKey();
				Plugin.Log.LogInfo((object)string.Format("{0} Touch hook fired: player={1}, playerId={2}, valuable={3}, valuableId={4}.", "[Taxman's Curse: Haunted Loot]", playerName, playerId, ((Object)component).name, objectId));
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Current lazy session key: " + text));
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Previous lazy session key: " + FormatSessionKey(currentSessionKey)));
				if (!string.Equals(currentSessionKey, text, StringComparison.Ordinal))
				{
					Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] New gameplay session detected from touch. Old=" + FormatSessionKey(currentSessionKey) + ", New=" + text + "; resetting lazy curse state."));
					state.Reset();
					currentSessionKey = text;
					cursesStartedThisSession = 0;
					lastTransferAnnounceTime = -9999f;
					announcer.ClearTruckQueue("new curse session reset");
				}
				else if (state.Active)
				{
					Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Existing session reused, with reason: session key matched and lazy curse state is active.");
				}
				else
				{
					Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Existing session reused, with reason: session key matched but lazy curse state is inactive; starting first lazy session.");
				}
				if (!state.Active)
				{
					StartLazySession(val2, component, obj);
				}
				else if (state.Revealed)
				{
					Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Curse session already revealed, skipping new reveal, with session key=" + currentSessionKey + "."));
				}
				state.TotalTouches++;
				state.LastTouchPlayerId = playerId;
				state.LastTouchPlayerName = playerName;
				state.TouchedValuables.Add(objectId);
				HandleTouch(val2, playerId, playerName, component, objectId, obj);
			}
		}

		internal void ClearAnnouncementQueues(string reason)
		{
			announcer.ClearTruckQueue(reason);
		}

		private void StartLazySession(PlayerAvatar firstPlayer, ValuableObject firstValuable, PhysGrabObject firstObject)
		{
			int num = Mathf.Max(0, config.CursesPerLevel.Value);
			Plugin.Log.LogInfo((object)string.Format("{0} Curses this session: {1}/{2}", "[Taxman's Curse: Haunted Loot]", cursesStartedThisSession, num));
			if (cursesStartedThisSession >= num)
			{
				Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Curse roll skipped: CursesPerLevel limit reached.");
				return;
			}
			state.Reset();
			state.Active = true;
			cursesStartedThisSession++;
			Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Lazy curse session started from first valid valuable touch.");
			Plugin.Log.LogInfo((object)string.Format("{0} Curses this session: {1}/{2}", "[Taxman's Curse: Haunted Loot]", cursesStartedThisSession, num));
			List<CurseType> enabledPublicCurses = GetEnabledPublicCurses();
			if (enabledPublicCurses.Count == 0)
			{
				Plugin.Log.LogWarning((object)"[Taxman's Curse: Haunted Loot] Lazy curse session aborted: no public-safe curse types enabled.");
				state.Reset();
				return;
			}
			state.CurrentCurse = enabledPublicCurses[Random.Range(0, enabledPublicCurses.Count)];
			Plugin.Log.LogInfo((object)string.Format("{0} Selected curse type: {1}.", "[Taxman's Curse: Haunted Loot]", state.CurrentCurse));
			if (IsValuableCurse(state.CurrentCurse))
			{
				state.CursedValuableId = GetObjectId(firstObject);
				Plugin.Log.LogInfo((object)string.Format("{0} Selected haunted valuable lazily: {1}, id={2}.", "[Taxman's Curse: Haunted Loot]", ((Object)firstValuable).name, state.CursedValuableId));
				return;
			}
			PlayerAvatar player = SelectInitialPlayer(firstPlayer);
			state.CursedPlayerId = GetPlayerId(player);
			state.CursedPlayerName = GetPlayerName(player);
			lastSelectedPlayerId = state.CursedPlayerId;
			Plugin.Log.LogInfo((object)string.Format("{0} Selected cursed player lazily: {1}, id={2}.", "[Taxman's Curse: Haunted Loot]", state.CursedPlayerName, state.CursedPlayerId));
		}

		private void HandleTouch(PlayerAvatar player, int playerId, string playerName, ValuableObject valuable, int valuableId, PhysGrabObject obj)
		{
			//IL_01ce: Unknown result type (might be due to invalid IL or missing references)
			//IL_005c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Unknown result type (might be due to invalid IL or missing references)
			//IL_0196: Unknown result type (might be due to invalid IL or missing references)
			//IL_024b: Unknown result type (might be due to invalid IL or missing references)
			switch (state.CurrentCurse)
			{
			case CurseType.MimicValuable:
				if (state.CursedValuableId == valuableId)
				{
					Reveal(player, playerName, "Mimic Valuable first touch");
					TriggerDanger(((Component)obj).transform.position, "Mimic Valuable touch");
				}
				break;
			case CurseType.GoldenTrap:
				if (state.CursedValuableId == valuableId)
				{
					Reveal(player, playerName, "Golden Trap touch");
					TriggerDanger(((Component)obj).transform.position, "Golden Trap touch");
				}
				break;
			case CurseType.LastTouchCurse:
				if (state.CursedValuableId == valuableId)
				{
					if (!state.Revealed)
					{
						Reveal(player, playerName, "Last-Touch Curse first touch");
					}
					else if (state.CursedPlayerId != playerId && Time.time - lastTransferAnnounceTime >= 15f)
					{
						state.CursedPlayerId = playerId;
						state.CursedPlayerName = playerName;
						lastTransferAnnounceTime = Time.time;
						announcer.TryAnnounceTransfer(player, playerName, "The curse has moved to " + playerName + ".");
						Plugin.Log.LogInfo((object)string.Format("{0} Last-Touch Curse transfer to {1}, id={2}.", "[Taxman's Curse: Haunted Loot]", playerName, playerId));
					}
				}
				break;
			case CurseType.LootBait:
				if (state.CursedPlayerId == playerId)
				{
					Reveal(player, playerName, "Loot Bait touch");
					if (state.TotalTouches % TouchThreshold() == 0)
					{
						TriggerDanger(((Component)obj).transform.position, "Loot Bait touch threshold");
					}
				}
				break;
			case CurseType.ReverseLuck:
				if (state.CursedPlayerId == playerId)
				{
					Reveal(player, playerName, "Reverse Luck touch");
					RollReverseLuck(valuable, ((Component)obj).transform.position);
				}
				break;
			case CurseType.GlassTouch:
				if (state.CursedPlayerId == playerId)
				{
					Reveal(player, playerName, "Glass Touch touch");
					state.MarkedValuables.Add(valuableId);
					Plugin.Log.LogInfo((object)string.Format("{0} Glass Touch marked valuable id={1}, name={2}.", "[Taxman's Curse: Haunted Loot]", valuableId, ((Object)valuable).name));
				}
				else if (state.MarkedValuables.Contains(valuableId))
				{
					TriggerDanger(((Component)obj).transform.position, "Glass Touch haunted valuable touched");
				}
				break;
			case CurseType.Butterfingers:
			case CurseType.GreedTax:
				break;
			}
		}

		private void Reveal(PlayerAvatar player, string playerName, string reason)
		{
			if (!state.Revealed)
			{
				state.Revealed = true;
				state.RevealTime = Time.time;
				if (state.CursedPlayerId == 0)
				{
					state.CursedPlayerId = GetPlayerId(player);
				}
				if (string.IsNullOrWhiteSpace(state.CursedPlayerName))
				{
					state.CursedPlayerName = playerName;
				}
				Plugin.Log.LogInfo((object)string.Format("{0} First-touch reveal: player={1}, curse={2}, reason={3}.", "[Taxman's Curse: Haunted Loot]", playerName, state.CurrentCurse, reason));
				announcer.TryAnnounceReveal(player, playerName, state.CurrentCurse);
			}
		}

		private void TriggerDanger(Vector3 position, string reason)
		{
			//IL_0107: Unknown result type (might be due to invalid IL or missing references)
			CurseIntensity value = config.CurseIntensity.Value;
			if (state.TriggerCount >= value switch
			{
				CurseIntensity.Low => 2, 
				CurseIntensity.High => 5, 
				_ => 3, 
			})
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Curse trigger skipped for " + reason + ": max simple triggers reached."));
				return;
			}
			value = config.CurseIntensity.Value;
			if (Time.time - state.LastTriggerTime < value switch
			{
				CurseIntensity.Low => 60f, 
				CurseIntensity.High => 25f, 
				_ => 40f, 
			})
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] Curse trigger skipped for " + reason + ": cooldown active."));
				return;
			}
			state.TriggerCount++;
			state.LastTriggerTime = Time.time;
			Plugin.Log.LogInfo((object)string.Format("{0} Curse trigger #{1}: {2}.", "[Taxman's Curse: Haunted Loot]", state.TriggerCount, reason));
			if (effects.ApplyDangerPressure(position, 1, reason))
			{
				state.TotalPressureEvents++;
			}
		}

		private void RollReverseLuck(ValuableObject valuable, Vector3 position)
		{
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			int num = Random.Range(0, 100);
			if (num < 25)
			{
				if (effects.TryApplySmallBonus(valuable, "Reverse Luck good roll"))
				{
					state.TotalBonuses++;
				}
				Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Reverse Luck good roll.");
			}
			else if (num < 65)
			{
				Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Reverse Luck neutral roll.");
			}
			else
			{
				TriggerDanger(position, "Reverse Luck bad roll");
			}
		}

		private PlayerAvatar SelectInitialPlayer(PlayerAvatar firstPlayer)
		{
			if (!config.IncludeHost.Value && GameAccess.IsPlayerLocal(firstPlayer))
			{
				DebugLog("IncludeHost=false requested, but lazy public build avoids player-list lookup; first touching player selected.");
			}
			if (config.AvoidSamePlayerTwice.Value && GetPlayerId(firstPlayer) == lastSelectedPlayerId)
			{
				DebugLog("AvoidSamePlayerTwice requested, but lazy public build avoids player-list lookup; first touching player selected.");
			}
			return firstPlayer;
		}

		private List<CurseType> GetEnabledPublicCurses()
		{
			List<CurseType> list = new List<CurseType>();
			Add(config.EnableMimicValuable.Value, CurseType.MimicValuable);
			Add(config.EnableLootBait.Value, CurseType.LootBait);
			Add(config.EnableReverseLuck.Value, CurseType.ReverseLuck);
			Add(config.EnableLastTouchCurse.Value, CurseType.LastTouchCurse);
			Add(config.EnableGlassTouch.Value, CurseType.GlassTouch);
			Add(config.EnableGoldenTrap.Value, CurseType.GoldenTrap);
			return list;
			void Add(bool enabled, CurseType curse)
			{
				if (enabled)
				{
					list.Add(curse);
				}
			}
		}

		private static bool IsValuableCurse(CurseType curse)
		{
			if (curse != CurseType.MimicValuable && curse != CurseType.GoldenTrap)
			{
				return curse == CurseType.LastTouchCurse;
			}
			return true;
		}

		private int TouchThreshold()
		{
			return config.CurseIntensity.Value switch
			{
				CurseIntensity.Low => 4, 
				CurseIntensity.High => 2, 
				_ => 3, 
			};
		}

		private static PhysGrabber TryGetGrabber(int photonViewId)
		{
			try
			{
				if (photonViewId == 0)
				{
					return null;
				}
				PhotonView val = PhotonView.Find(photonViewId);
				return Object.op_Implicit((Object)(object)val) ? ((Component)val).GetComponent<PhysGrabber>() : null;
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[Taxman's Curse: Haunted Loot] Touch player lookup failed: " + ex.GetType().Name + ": " + ex.Message));
				return null;
			}
		}

		private static bool IsLikelyGameplayTouch()
		{
			try
			{
				if (SemiFunc.RunIsLobbyMenu())
				{
					return false;
				}
			}
			catch
			{
			}
			try
			{
				return SemiFunc.RunIsLevel();
			}
			catch
			{
				return (Object)(object)LevelGenerator.Instance != (Object)null;
			}
		}

		private static int GetPlayerId(PlayerAvatar player)
		{
			if (!Object.op_Implicit((Object)(object)player))
			{
				return 0;
			}
			if (!Object.op_Implicit((Object)(object)player.photonView))
			{
				return ((Object)player).GetInstanceID();
			}
			return player.photonView.ViewID;
		}

		private static int GetObjectId(PhysGrabObject obj)
		{
			if (!Object.op_Implicit((Object)(object)obj))
			{
				return 0;
			}
			PhotonView val = GameAccess.PhysPhotonView(obj);
			if (!Object.op_Implicit((Object)(object)val) || val.ViewID == 0)
			{
				return ((Object)obj).GetInstanceID();
			}
			return val.ViewID;
		}

		private static string BuildGameplaySessionKey()
		{
			//IL_0039: 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)
			string text = "singleplayer";
			try
			{
				if (PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null)
				{
					text = PhotonNetwork.CurrentRoom.Name ?? "unknown-room";
				}
			}
			catch
			{
				text = "unknown-room";
			}
			string text2 = "unknown-level";
			try
			{
				Scene activeScene = SceneManager.GetActiveScene();
				text2 = ((Scene)(ref activeScene)).name ?? "unknown-level";
			}
			catch
			{
			}
			int num = 0;
			try
			{
				if (Object.op_Implicit((Object)(object)RoundDirector.instance))
				{
					num = ((Object)RoundDirector.instance).GetInstanceID();
				}
			}
			catch
			{
			}
			int num2 = 0;
			try
			{
				if (Object.op_Implicit((Object)(object)LevelGenerator.Instance))
				{
					num2 = ((Object)LevelGenerator.Instance).GetInstanceID();
				}
			}
			catch
			{
			}
			return $"room={text}|scene={text2}|round={num}|levelGen={num2}";
		}

		private static string FormatSessionKey(string key)
		{
			if (!string.IsNullOrWhiteSpace(key))
			{
				return key;
			}
			return "<none>";
		}

		private static string GetPlayerName(PlayerAvatar player)
		{
			if (!Object.op_Implicit((Object)(object)player))
			{
				return "Unknown";
			}
			try
			{
				string text = SemiFunc.PlayerGetName(player);
				return string.IsNullOrWhiteSpace(text) ? ((Object)player).name : text;
			}
			catch
			{
				return ((Object)player).name;
			}
		}

		private void DebugLog(string message)
		{
			if (config.DebugEnabled)
			{
				Plugin.Log.LogInfo((object)("[Taxman's Curse: Haunted Loot] " + message));
			}
		}
	}
	internal static class Patches
	{
		internal static void ApplyTouchOnly(Harmony harmony)
		{
			if (harmony == null)
			{
				return;
			}
			try
			{
				harmony.CreateClassProcessor(typeof(PhysGrabObjectGrabStartedRpcPatch)).Patch();
				Plugin.Log.LogInfo((object)"[Taxman's Curse: Haunted Loot] Harmony patch applied: PhysGrabObject.GrabStartedRPC.");
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[Taxman's Curse: Haunted Loot] Harmony patch failed for PhysGrabObject.GrabStartedRPC: " + ex.GetType().Name + ": " + ex.Message));
			}
		}
	}
	[HarmonyPatch(typeof(PhysGrabObject), "GrabStartedRPC")]
	internal static class PhysGrabObjectGrabStartedRpcPatch
	{
		private static void Postfix(PhysGrabObject __instance, int playerPhotonID)
		{
			Plugin.OnGrabStartedRpc(__instance, playerPhotonID);
		}
	}
	[BepInPlugin("taxmanscurse.hauntedloot", "Taxman's Curse: Haunted Loot", "0.2.9")]
	public sealed class Plugin : BaseUnityPlugin
	{
		public const string PluginGuid = "taxmanscurse.hauntedloot";

		public const string PluginName = "Taxman's Curse: Haunted Loot";

		public const string PluginVersion = "0.2.9";

		public const string LogPrefix = "[Taxman's Curse: Haunted Loot]";

		internal static ManualLogSource Log;

		internal static CurseConfig ModConfig;

		internal static LazyCurseDirector Director;

		internal static Plugin Instance;

		private Harmony harmony;

		private void Awake()
		{
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0037: Expected O, but got Unknown
			Log = ((BaseUnityPlugin)this).Logger;
			Instance = this;
			ModConfig = new CurseConfig(((BaseUnityPlugin)this).Config);
			Director = null;
			harmony = new Harmony("taxmanscurse.hauntedloot");
			Patches.ApplyTouchOnly(harmony);
			((BaseUnityPlugin)this).Logger.LogInfo((object)("[Taxman's Curse: Haunted Loot] Runtime marker v0.2.9 loaded from " + Assembly.GetExecutingAssembly().Location));
			((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} Public config loaded. Enabled={1}, HostOnly={2}, DebugLogging={3}.", "[Taxman's Curse: Haunted Loot]", ModConfig.Enabled.Value, ModConfig.HostOnly.Value, ModConfig.DebugLogging.Value));
			((BaseUnityPlugin)this).Logger.LogInfo((object)string.Format("{0} TTS config loaded: EnableTTS={1}, TTSMode={2}, TTSCooldownSeconds={3:0.0}", "[Taxman's Curse: Haunted Loot]", ModConfig.EnableTTS.Value, ModConfig.TTSMode.Value, ModConfig.TTSCooldownSeconds.Value));
			((BaseUnityPlugin)this).Logger.LogInfo((object)"[Taxman's Curse: Haunted Loot] Internal announcements: chat/TTS enabled, truck retry enabled, bottom objective disabled, ForceTruckAnnouncement=false.");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"[Taxman's Curse: Haunted Loot] No separate synced global chat-feed channel found; using targeted vanilla PlayerAvatar.ChatMessageSendRPC.");
			((BaseUnityPlugin)this).Logger.LogInfo((object)"[Taxman's Curse: Haunted Loot] Public-safe hooks active: PhysGrabObject.GrabStartedRPC only. RunManager hooks are not applied.");
		}

		private void OnDestroy()
		{
			Director?.ClearAnnouncementQueues("plugin disable");
			Harmony obj = harmony;
			if (obj != null)
			{
				obj.UnpatchSelf();
			}
			if ((Object)(object)Instance == (Object)(object)this)
			{
				Instance = null;
			}
		}

		internal static Coroutine StartAnnouncementCoroutine(IEnumerator routine)
		{
			if (!Object.op_Implicit((Object)(object)Instance))
			{
				return null;
			}
			return ((MonoBehaviour)Instance).StartCoroutine(routine);
		}

		internal static void OnGrabStartedRpc(PhysGrabObject obj, int playerPhotonID)
		{
			CurseConfig modConfig = ModConfig;
			if (modConfig != null && modConfig.Enabled.Value && Object.op_Implicit((Object)(object)obj) && Object.op_Implicit((Object)(object)((Component)obj).GetComponent<ValuableObject>()))
			{
				if (Director == null)
				{
					Director = new LazyCurseDirector(ModConfig);
				}
				Director.OnGrabStartedRpc(obj, playerPhotonID);
			}
		}
	}
	internal enum CurseIntensity
	{
		Low,
		Normal,
		High
	}
	internal enum TTSMode
	{
		RevealOnly,
		RevealAndTransfer,
		AllImportant
	}
	internal sealed class CurseConfig
	{
		internal readonly ConfigEntry<bool> Enabled;

		internal readonly ConfigEntry<bool> HostOnly;

		internal readonly ConfigEntry<bool> DebugLogging;

		internal readonly ConfigEntry<int> CursesPerLevel;

		internal readonly ConfigEntry<bool> IncludeHost;

		internal readonly ConfigEntry<bool> AvoidSamePlayerTwice;

		internal readonly ConfigEntry<CurseIntensity> CurseIntensity;

		internal readonly ConfigEntry<bool> EnableTTS;

		internal readonly ConfigEntry<TTSMode> TTSMode;

		internal readonly ConfigEntry<float> TTSCooldownSeconds;

		internal readonly ConfigEntry<bool> EnableMimicValuable;

		internal readonly ConfigEntry<bool> EnableLootBait;

		internal readonly ConfigEntry<bool> EnableReverseLuck;

		internal readonly ConfigEntry<bool> EnableLastTouchCurse;

		internal readonly ConfigEntry<bool> EnableGlassTouch;

		internal readonly ConfigEntry<bool> EnableGoldenTrap;

		internal bool DebugEnabled => DebugLogging.Value;

		internal CurseConfig(ConfigFile config)
		{
			Enabled = config.Bind<bool>("General", "Enabled", true, "Enable Taxman's Curse: Haunted Loot.");
			HostOnly = config.Bind<bool>("General", "HostOnly", true, "Only the host/master runs curse logic.");
			DebugLogging = config.Bind<bool>("General", "DebugLogging", false, "Enable verbose host logging.");
			CursesPerLevel = config.Bind<int>("Curse", "CursesPerLevel", 1, "Number of curses per level. v0.2.9 supports one lazy touch curse.");
			IncludeHost = config.Bind<bool>("Curse", "IncludeHost", true, "Allow the host player to be selected.");
			AvoidSamePlayerTwice = config.Bind<bool>("Curse", "AvoidSamePlayerTwice", true, "Avoid selecting the same player on consecutive lazy sessions when possible.");
			CurseIntensity = config.Bind<CurseIntensity>("Curse", "CurseIntensity", TaxmansCurseHauntedLoot.CurseIntensity.Normal, "Overall touch curse intensity.");
			EnableTTS = config.Bind<bool>("TTS", "EnableTTS", true, "Send vanilla synced chat/TTS announcements.");
			TTSMode = config.Bind<TTSMode>("TTS", "TTSMode", TaxmansCurseHauntedLoot.TTSMode.RevealOnly, "RevealOnly, RevealAndTransfer, or AllImportant.");
			TTSCooldownSeconds = config.Bind<float>("TTS", "TTSCooldownSeconds", 8f, "Cooldown for non-reveal TTS announcements.");
			EnableMimicValuable = config.Bind<bool>("CurseTypes", "EnableMimicValuable", true, "Mimic Valuable: Looks like normal loot, but touching it triggers danger.");
			EnableLootBait = config.Bind<bool>("CurseTypes", "EnableLootBait", true, "Loot Bait: Every few valuable touches by the cursed player attracts danger.");
			EnableReverseLuck = config.Bind<bool>("CurseTypes", "EnableReverseLuck", true, "Reverse Luck: Valuable touches can help the team, do nothing, or attract danger.");
			EnableLastTouchCurse = config.Bind<bool>("CurseTypes", "EnableLastTouchCurse", true, "Last-Touch Curse: The last player to touch haunted loot becomes cursed.");
			EnableGlassTouch = config.Bind<bool>("CurseTypes", "EnableGlassTouch", true, "Glass Touch: Loot touched by the cursed player becomes haunted and unsafe.");
			EnableGoldenTrap = config.Bind<bool>("CurseTypes", "EnableGoldenTrap", true, "Golden Trap: Tempting treasure can trigger a dangerous trap.");
		}
	}
}