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 d__24 : IEnumerator, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public CurseAnnouncer <>4__this; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public 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(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 isTypingRef; private MethodInfo forceCompleteChatMessageMethod; private bool loggedObjectiveDiscovery; private readonly Dictionary pendingTruckAnnouncements = new Dictionary(); private readonly Dictionary truckCooldownUntil = new Dictionary(); 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 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 GetAnnouncementTargets() { List list = new List(); 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 ""; } 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(d__24))] private IEnumerator TruckRetryLoop() { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new 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("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()); 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 EnemyActionAmountRef = AccessTools.FieldRefAccess("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 TouchedValuables = new HashSet(); public readonly HashSet MarkedValuables = new HashSet(); 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 DollarValueCurrentRef = AccessTools.FieldRefAccess("dollarValueCurrent"); private static readonly FieldRef DollarValueOriginalRef = AccessTools.FieldRefAccess("dollarValueOriginal"); private static readonly FieldRef LastPlayerGrabbingRef = AccessTools.FieldRefAccess("lastPlayerGrabbing"); private static readonly FieldRef PhysPhotonViewRef = AccessTools.FieldRefAccess("photonView"); private static readonly FieldRef PlayerDisabledRef = AccessTools.FieldRefAccess("isDisabled"); private static readonly FieldRef PlayerLocalRef = AccessTools.FieldRefAccess("isLocal"); private static readonly FieldRef PlayerDeadSetRef = AccessTools.FieldRefAccess("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(); 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 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 GetEnabledPublicCurses() { List list = new List(); 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() : 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 ""; } 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())) { 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 Enabled; internal readonly ConfigEntry HostOnly; internal readonly ConfigEntry DebugLogging; internal readonly ConfigEntry CursesPerLevel; internal readonly ConfigEntry IncludeHost; internal readonly ConfigEntry AvoidSamePlayerTwice; internal readonly ConfigEntry CurseIntensity; internal readonly ConfigEntry EnableTTS; internal readonly ConfigEntry TTSMode; internal readonly ConfigEntry TTSCooldownSeconds; internal readonly ConfigEntry EnableMimicValuable; internal readonly ConfigEntry EnableLootBait; internal readonly ConfigEntry EnableReverseLuck; internal readonly ConfigEntry EnableLastTouchCurse; internal readonly ConfigEntry EnableGlassTouch; internal readonly ConfigEntry EnableGoldenTrap; internal bool DebugEnabled => DebugLogging.Value; internal CurseConfig(ConfigFile config) { Enabled = config.Bind("General", "Enabled", true, "Enable Taxman's Curse: Haunted Loot."); HostOnly = config.Bind("General", "HostOnly", true, "Only the host/master runs curse logic."); DebugLogging = config.Bind("General", "DebugLogging", false, "Enable verbose host logging."); CursesPerLevel = config.Bind("Curse", "CursesPerLevel", 1, "Number of curses per level. v0.2.9 supports one lazy touch curse."); IncludeHost = config.Bind("Curse", "IncludeHost", true, "Allow the host player to be selected."); AvoidSamePlayerTwice = config.Bind("Curse", "AvoidSamePlayerTwice", true, "Avoid selecting the same player on consecutive lazy sessions when possible."); CurseIntensity = config.Bind("Curse", "CurseIntensity", TaxmansCurseHauntedLoot.CurseIntensity.Normal, "Overall touch curse intensity."); EnableTTS = config.Bind("TTS", "EnableTTS", true, "Send vanilla synced chat/TTS announcements."); TTSMode = config.Bind("TTS", "TTSMode", TaxmansCurseHauntedLoot.TTSMode.RevealOnly, "RevealOnly, RevealAndTransfer, or AllImportant."); TTSCooldownSeconds = config.Bind("TTS", "TTSCooldownSeconds", 8f, "Cooldown for non-reveal TTS announcements."); EnableMimicValuable = config.Bind("CurseTypes", "EnableMimicValuable", true, "Mimic Valuable: Looks like normal loot, but touching it triggers danger."); EnableLootBait = config.Bind("CurseTypes", "EnableLootBait", true, "Loot Bait: Every few valuable touches by the cursed player attracts danger."); EnableReverseLuck = config.Bind("CurseTypes", "EnableReverseLuck", true, "Reverse Luck: Valuable touches can help the team, do nothing, or attract danger."); EnableLastTouchCurse = config.Bind("CurseTypes", "EnableLastTouchCurse", true, "Last-Touch Curse: The last player to touch haunted loot becomes cursed."); EnableGlassTouch = config.Bind("CurseTypes", "EnableGlassTouch", true, "Glass Touch: Loot touched by the cursed player becomes haunted and unsafe."); EnableGoldenTrap = config.Bind("CurseTypes", "EnableGoldenTrap", true, "Golden Trap: Tempting treasure can trigger a dangerous trap."); } } }