using System; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Logging; using HarmonyLib; using TMPro; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("TombstoneLock")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("TombstoneLock")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("7ECD0A4B-BBD9-4A43-BBD3-C4FFD289E99A")] [assembly: AssemblyFileVersion("1.0.5")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.5.0")] namespace TombstoneLock; internal sealed class PendingRequest { public long RequesterPlayerID; public string RequesterName; public ZDOID Tombstone; public DateTime Time; } public class ChatNotifier { private static void OpenChat() { Chat instance = Chat.instance; if (!((Object)(object)instance == (Object)null)) { AccessTools.Field(typeof(Chat), "m_hideTimer")?.SetValue(instance, 0f); object? obj = AccessTools.Field(typeof(Terminal), "m_chatWindow")?.GetValue(instance); object? obj2 = ((obj is Image) ? obj : null); if (obj2 != null) { ((Component)obj2).gameObject.SetActive(true); } object? obj3 = AccessTools.Field(typeof(Terminal), "m_input")?.GetValue(instance); TMP_InputField val = (TMP_InputField)((obj3 is TMP_InputField) ? obj3 : null); if ((Object)(object)val != (Object)null) { ((Component)val).gameObject.SetActive(true); val.ActivateInputField(); } } } public static void ShowInChat(string text, bool openChat = true) { Chat instance = Chat.instance; if ((Object)(object)instance != (Object)null) { OpenChat(); Localization instance2 = Localization.instance; ((Terminal)instance).AddString(((instance2 != null) ? instance2.Localize(text) : null) ?? text); } } } public class OnScreenNotifier { public static void Message(Player player, MessageType location, string message) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) Localization instance = Localization.instance; ((Character)player).Message(location, ((instance != null) ? instance.Localize(message) : null) ?? message, 0, (Sprite)null); } } [BepInPlugin("TombstoneLock", "TombstoneLock", "1.0.5")] public class TombstoneLockPlugin : BaseUnityPlugin { [HarmonyPatch(typeof(Game), "Start")] internal static class RpcRegistrationPatch { private static void Postfix() { MainManager.RegisterRpcs(); } } [HarmonyPatch(typeof(Localization), "SetupLanguage")] internal static class LocalizationSetupPatch { private static readonly MethodInfo AddWordMethod = AccessTools.Method(typeof(Localization), "AddWord", (Type[])null, (Type[])null); private static void Postfix(Localization __instance) { AddTranslations(__instance); } internal static void AddTranslations(Localization localization) { string selectedLanguage = localization.GetSelectedLanguage(); Assembly executingAssembly = Assembly.GetExecutingAssembly(); string text = "TombstoneLock.Translations." + selectedLanguage + ".properties"; if (executingAssembly.GetManifestResourceInfo(text) == null) { text = "TombstoneLock.Translations.English.properties"; } using Stream stream = executingAssembly.GetManifestResourceStream(text); if (stream == null) { Log.LogWarning((object)("Embedded translation not found: " + text)); return; } using StreamReader streamReader = new StreamReader(stream); string text2; while ((text2 = streamReader.ReadLine()) != null) { if (!string.IsNullOrWhiteSpace(text2) && !text2.TrimStart(Array.Empty()).StartsWith("#")) { int num = text2.IndexOf('='); if (num <= 0) { Log.LogWarning((object)("Invalid translation line in " + text + ": " + text2)); continue; } string key = text2.Substring(0, num).Trim(); string text3 = text2.Substring(num + 1).Replace("\\n", "\n"); AddWord(localization, key, text3); } } } private static void AddWord(Localization localization, string key, string text) { AddWordMethod.Invoke(localization, new object[2] { key, text }); } } [HarmonyPatch(typeof(TombStone), "Awake")] internal static class TombStoneAwakePatch { private static void Postfix(TombStone __instance) { MainManager.AssignOwnerOnCreate(__instance); } } [HarmonyPatch(typeof(TombStone), "Interact")] internal static class TombStoneInteractPatch { private static bool Prefix(TombStone __instance, Humanoid character) { return MainManager.HandleInteract(__instance, character); } } [HarmonyPatch(typeof(Terminal), "InitTerminal")] internal static class TerminalCommandsPatch { [Serializable] [CompilerGenerated] private sealed class <>c { public static readonly <>c <>9 = new <>c(); public static ConsoleEvent <>9__1_0; internal void b__1_0(ConsoleEventArgs args) { MainManager.AcceptPendingRequest(); } } private static bool commandRegistered; private static void Postfix() { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown if (commandRegistered) { return; } commandRegistered = true; object obj = <>c.<>9__1_0; if (obj == null) { ConsoleEvent val = delegate { MainManager.AcceptPendingRequest(); }; <>c.<>9__1_0 = val; obj = (object)val; } new ConsoleCommand("accept", "Accepts a pending tombstone unlock request.", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } } public const string PluginGuid = "TombstoneLock"; public const string PluginName = "TombstoneLock"; public const string PluginVersion = "1.0.5"; internal static ManualLogSource Log; private readonly Harmony harmony = new Harmony("TombstoneLock"); private void Awake() { Log = ((BaseUnityPlugin)this).Logger; harmony.PatchAll(Assembly.GetExecutingAssembly()); if (Localization.instance != null) { LocalizationSetupPatch.AddTranslations(Localization.instance); } Log.LogInfo((object)"TombstoneLock loaded"); } private void Update() { MainManager.Tick(); } } internal static class MainManager { public const string MsgRequestSent = "$msg_cantopen\n\n$tombstonelock_request_sent"; public const string MsgOwnerOffline = "$msg_cantopen\n\n$tombstonelock_owner_offline"; public const string MsgCannotSendRequestNow = "$msg_cantopen\n\n$tombstonelock_cannot_send_request_now"; public const string MsgOwnerNotification = "$tombstonelock_owner_notification"; public const string MsgNoPendingRequest = "$tombstonelock_no_pending_request"; public const string MsgRequestAccepted = "$tombstonelock_request_accepted"; public const string MsgRequestExpired = "$tombstonelock_request_expired"; public const string MsgRequestExpiredRequester = "$tombstonelock_request_expired_requester"; public const string MsgFriendConfirmation = "$tombstonelock_friend_confirmation"; private const string OwnerKey = "ownerID"; private const string LockDayKey = "lockDay"; private const int UnlockAfterDays = 10; internal const int RequestExpirySeconds = 30; private const int AntiSpamCooldownSeconds = 120; private const string RequestRpc = "RPC_RequestTombstoneUnlock"; private const string ConfirmRpc = "RPC_ConfirmTombstoneUnlock"; private const string ExpiredRpc = "RPC_TombstoneRequestExpired"; private static PendingRequest pendingRequest; private static DateTime lastSentRequestTime = DateTime.MinValue; private static ZRoutedRpc registeredInstance; internal static void RegisterRpcs() { if (ZRoutedRpc.instance == null) { LogDebug("RegisterRpcs: ZRoutedRpc.instance is null (networking not ready yet), skipping."); } else if (registeredInstance != ZRoutedRpc.instance) { ZRoutedRpc.instance.Register("RPC_RequestTombstoneUnlock", (Action)RPC_RequestTombstoneUnlock); ZRoutedRpc.instance.Register("RPC_ConfirmTombstoneUnlock", (Action)RPC_ConfirmTombstoneUnlock); ZRoutedRpc.instance.Register("RPC_TombstoneRequestExpired", (Action)RPC_TombstoneRequestExpired); registeredInstance = ZRoutedRpc.instance; LogInfo("Network RPCs registered for this session."); } } private static void LogInfo(string message) { ManualLogSource log = TombstoneLockPlugin.Log; if (log != null) { log.LogInfo((object)("[TombstoneLock] " + message)); } } private static void LogDebug(string message) { ManualLogSource log = TombstoneLockPlugin.Log; if (log != null) { log.LogDebug((object)("[TombstoneLock] " + message)); } } private static void LogWarning(string message) { ManualLogSource log = TombstoneLockPlugin.Log; if (log != null) { log.LogWarning((object)("[TombstoneLock] " + message)); } } internal static void AssignOwnerOnCreate(TombStone tombstone) { //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) ZNetView component = ((Component)tombstone).GetComponent(); ZDO val = ((component != null) ? component.GetZDO() : null); if (val == null || !component.IsOwner()) { LogDebug("AssignOwnerOnCreate: no valid ZDO or not the ZDO owner, skipping (this is normal for remote tombstones)."); return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { LogDebug("AssignOwnerOnCreate: no local player, skipping."); } else if (val.GetLong("ownerID", 0L) == 0L) { long num = CurrentDay(); val.Set("ownerID", localPlayer.GetPlayerID()); val.Set("lockDay", num); LogInfo($"Locked new tombstone {val.m_uid} to owner {localPlayer.GetPlayerID()} on in-game day {num}."); } else { LogDebug(string.Format("AssignOwnerOnCreate: tombstone {0} already has owner {1}, leaving as is.", val.m_uid, val.GetLong("ownerID", 0L))); } } private static long CurrentDay() { if (!((Object)(object)EnvMan.instance != (Object)null)) { return -1L; } return EnvMan.instance.GetDay(); } private static bool IsLockExpiredByAge(ZDO zdo) { long num = zdo.GetLong("lockDay", -1L); if (num < 0) { return false; } long num2 = CurrentDay(); if (num2 >= 0) { return num2 - num >= 10; } return false; } internal static bool HandleInteract(TombStone tombstone, Humanoid character) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_01a3: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) Player val = (Player)(object)((character is Player) ? character : null); if ((Object)(object)val == (Object)null) { return true; } ZNetView component = ((Component)tombstone).GetComponent(); ZDO val2 = ((component != null) ? component.GetZDO() : null); if (val2 == null) { LogDebug("HandleInteract: tombstone has no ZDO, letting the base game handle it."); return true; } long num = val2.GetLong("ownerID", 0L); long playerID = val.GetPlayerID(); LogDebug($"HandleInteract: tombstone {val2.m_uid}, owner={num}, interactingPlayer={playerID}."); if (num == 0L || playerID == num) { LogDebug("HandleInteract: allowed (tombstone unlocked or interacting player is the owner)."); return true; } if (IsLockExpiredByAge(val2)) { LogInfo($"HandleInteract: tombstone {val2.m_uid} auto-unlocked (locked for >= {10} in-game days)."); return true; } if ((DateTime.Now - lastSentRequestTime).TotalSeconds < 120.0) { int num2 = 2; LogDebug($"HandleInteract: blocked, unlock request on cooldown ({num2} min not elapsed since last request)."); string message = string.Format(Localization.instance.Localize("$msg_cantopen\n\n$tombstonelock_cannot_send_request_now"), num2); OnScreenNotifier.Message(val, (MessageType)2, message); return false; } if (IsPlayerOnline(num)) { lastSentRequestTime = DateTime.Now; OnScreenNotifier.Message(val, (MessageType)2, "$msg_cantopen\n\n$tombstonelock_request_sent"); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "RPC_RequestTombstoneUnlock", new object[3] { playerID, val.GetPlayerName(), val2.m_uid }); LogInfo($"HandleInteract: owner {num} online, broadcast unlock request for tombstone {val2.m_uid}."); } else { OnScreenNotifier.Message(val, (MessageType)2, "$msg_cantopen\n\n$tombstonelock_owner_offline"); LogInfo($"HandleInteract: owner {num} appears offline, no request sent for tombstone {val2.m_uid}."); } return false; } internal static void AcceptPendingRequest() { //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { LogDebug("AcceptPendingRequest: no local player, ignoring /accept."); return; } PurgeExpiredRequest(); PendingRequest pendingRequest = MainManager.pendingRequest; if (pendingRequest == null) { LogInfo("AcceptPendingRequest: /accept used but there is no pending request (none received or it expired)."); ChatNotifier.ShowInChat("$tombstonelock_no_pending_request"); return; } MainManager.pendingRequest = null; if (ZRoutedRpc.instance == null) { LogWarning("AcceptPendingRequest: ZRoutedRpc.instance is null, cannot broadcast the unlock confirmation."); return; } ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "RPC_ConfirmTombstoneUnlock", new object[3] { pendingRequest.RequesterPlayerID, pendingRequest.Tombstone, localPlayer.GetPlayerName() }); LogInfo($"AcceptPendingRequest: accepted request from {pendingRequest.RequesterName} ({pendingRequest.RequesterPlayerID}), broadcast unlock for tombstone {pendingRequest.Tombstone}."); ChatNotifier.ShowInChat("$tombstonelock_request_accepted"); } internal static void PurgeExpiredRequest() { if (pendingRequest != null && (DateTime.Now - pendingRequest.Time).TotalSeconds > 30.0) { LogDebug($"PurgeExpiredRequest: dropped expired request from {pendingRequest.RequesterPlayerID}."); pendingRequest = null; } } internal static void Tick() { if (pendingRequest != null && !((DateTime.Now - pendingRequest.Time).TotalSeconds <= 30.0)) { long requesterPlayerID = pendingRequest.RequesterPlayerID; pendingRequest = null; LogInfo($"Tick: pending request from {requesterPlayerID} expired after {30}s; notifying both players."); if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "RPC_TombstoneRequestExpired", new object[1] { requesterPlayerID }); } ChatNotifier.ShowInChat("$tombstonelock_request_expired", openChat: false); } } private static void RPC_RequestTombstoneUnlock(long sender, long requesterPlayerID, string requesterName, ZDOID tombstoneUid) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } ZDOMan instance = ZDOMan.instance; ZDO val = ((instance != null) ? instance.GetZDO(tombstoneUid) : null); if (val != null && localPlayer.GetPlayerID() == val.GetLong("ownerID", 0L)) { PurgeExpiredRequest(); if (pendingRequest == null) { string text = (string.IsNullOrEmpty(requesterName) ? "A friend" : requesterName); pendingRequest = new PendingRequest { RequesterPlayerID = requesterPlayerID, RequesterName = text, Tombstone = tombstoneUid, Time = DateTime.Now }; ChatNotifier.ShowInChat(string.Format(Localization.instance.Localize("$tombstonelock_owner_notification"), text)); } } } private static void RPC_ConfirmTombstoneUnlock(long sender, long requesterPlayerID, ZDOID tombstoneUid, string ownerName) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || localPlayer.GetPlayerID() != requesterPlayerID) { return; } GameObject val = (((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.FindInstance(tombstoneUid) : null); if (!((Object)(object)val == (Object)null)) { ZNetView component = val.gameObject.GetComponent(); if (!((Object)(object)component == (Object)null) && component.IsValid()) { component.ClaimOwnership(); component.GetZDO().Set("ownerID", 0L); OnScreenNotifier.Message(localPlayer, (MessageType)2, string.Format(Localization.instance.Localize("$tombstonelock_friend_confirmation"), ownerName)); } } } private static void RPC_TombstoneRequestExpired(long sender, long requesterPlayerID) { Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && localPlayer.GetPlayerID() == requesterPlayerID) { ChatNotifier.ShowInChat("$tombstonelock_request_expired_requester", openChat: false); } } private static bool IsPlayerOnline(long playerID) { if ((Object)(object)Player.m_localPlayer != (Object)null && Player.m_localPlayer.GetPlayerID() == playerID) { return true; } foreach (Player allPlayer in Player.GetAllPlayers()) { if (!((Object)(object)allPlayer == (Object)null) && allPlayer.GetPlayerID() == playerID) { return true; } } return false; } }