Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of TaxmansCurseHauntedLoot v0.2.9
TaxmansCurseHauntedLoot.dll
Decompiled a day agousing 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."); } } }