using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Logging; using HarmonyLib; using IronLabs.SharedLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("IronLabs.SealedTombstone")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("IronLabs.SealedTombstone")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("37181DCE-EEA5-4CC8-8DF3-32B9AB883DE8")] [assembly: AssemblyFileVersion("1.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyVersion("1.0.0.2687")] namespace IronLabs.SealedTombstone { internal sealed class PendingRequest { internal long RequesterPlayerId { get; set; } internal string RequesterName { get; set; } internal ZDOID TombstoneId { get; set; } internal DateTime CreatedAt { get; set; } } internal static class RecentAttackers { private static readonly TimeSpan Window = TimeSpan.FromMinutes(2.0); private static readonly Dictionary LastHits = new Dictionary(); private static long[] _deathSnapshot = new long[0]; internal static void Record(Player victim, HitData hit) { Character obj = ((hit != null) ? hit.GetAttacker() : null); Player val = (Player)(object)((obj is Player) ? obj : null); if (!((Object)(object)victim != (Object)(object)Player.m_localPlayer) && !((Object)(object)val == (Object)null)) { long playerID = val.GetPlayerID(); if (playerID != 0L && playerID != victim.GetPlayerID()) { LastHits[playerID] = DateTime.UtcNow; } } } internal static void SnapshotForDeath(Player player) { if (!((Object)(object)player != (Object)(object)Player.m_localPlayer)) { DateTime threshold = DateTime.UtcNow - Window; _deathSnapshot = (from entry in LastHits where entry.Value >= threshold select entry.Key).ToArray(); LastHits.Clear(); SealedTombstonePlugin.Log.LogDebug($"Captured {_deathSnapshot.Length} recent PvP attackers."); } } internal static string ConsumeSnapshot() { string result = string.Join(",", _deathSnapshot.Select((long id) => id.ToString(CultureInfo.InvariantCulture))); _deathSnapshot = new long[0]; return result; } internal static bool Contains(string serializedIds, long playerId) { if (string.IsNullOrEmpty(serializedIds)) { return false; } string expected = playerId.ToString(CultureInfo.InvariantCulture); return serializedIds.Split(new char[1] { ',' }).Any((string id) => id == expected); } internal static void Reset() { LastHits.Clear(); _deathSnapshot = new long[0]; } } internal static class TombstoneAccess { private const string LockDayKey = "IronLabs.SealedTombstone.LockDay"; private const string BlockedPlayersKey = "IronLabs.SealedTombstone.BlockedPlayers"; private const string RequestRpc = "IronLabs_SealedTombstone_Request"; private const string DecisionRpc = "IronLabs_SealedTombstone_Decision"; private const int UnlockAfterDays = 10; private const int RequestExpirySeconds = 30; private const int RequestCooldownSeconds = 120; private static PendingRequest _pendingRequest; private static DateTime _lastRequestAt = DateTime.MinValue; private static ZRoutedRpc _registeredRpc; internal static void RegisterRpcs() { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null && instance != _registeredRpc) { instance.Register("IronLabs_SealedTombstone_Request", (Method)ReceiveRequest); instance.Register("IronLabs_SealedTombstone_Decision", (Method)ReceiveDecision); _registeredRpc = instance; SealedTombstonePlugin.Log.LogDebug("Tombstone access RPCs registered for the current session."); } } internal static void ResetSession() { _pendingRequest = null; _lastRequestAt = DateTime.MinValue; _registeredRpc = null; RecentAttackers.Reset(); } internal static void RecordLockDay(TombStone tombstone, long ownerId) { //IL_005b: 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 (ownerId != 0L && val != null && component.IsOwner()) { val.Set("IronLabs.SealedTombstone.LockDay", CurrentDay()); val.Set("IronLabs.SealedTombstone.BlockedPlayers", RecentAttackers.ConsumeSnapshot()); SealedTombstonePlugin.Log.LogDebug($"Locked tombstone {val.m_uid} for player {ownerId}."); } } internal static bool AllowInteraction(TombStone tombstone, Humanoid character) { //IL_00b1: Unknown result type (might be due to invalid IL or missing references) Player val = (Player)(object)((character is Player) ? character : null); ZNetView component = ((Component)tombstone).GetComponent(); ZDO val2 = ((component != null) ? component.GetZDO() : null); if ((Object)(object)val == (Object)null || val2 == null) { return true; } long num = val2.GetLong(ZDOVars.s_owner, 0L); if (num == 0L || num == val.GetPlayerID() || IsExpired(val2)) { return true; } if (IsBlocked(val2, val.GetPlayerID())) { ((Character)val).Message((MessageType)2, "You cannot request access to this tombstone.", 0, (Sprite)null); SealedTombstonePlugin.Log.LogDebug($"Blocked tombstone request from recent attacker {val.GetPlayerID()}."); return false; } RequestAccess(val, num, val2.m_uid); return false; } internal static void Tick() { if (_pendingRequest != null && HasExpired(_pendingRequest)) { SealedTombstonePlugin.Log.LogDebug($"Request from {_pendingRequest.RequesterPlayerId} expired."); ClosePopup(); SendDecision(_pendingRequest, accepted: false); _pendingRequest = null; } } private static void RequestAccess(Player requester, long ownerId, ZDOID tombstoneId) { //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) if ((DateTime.UtcNow - _lastRequestAt).TotalSeconds < 120.0) { ((Character)requester).Message((MessageType)2, "Please wait before sending another request.", 0, (Sprite)null); return; } if (ZRoutedRpc.instance == null) { ((Character)requester).Message((MessageType)2, "The tombstone owner is offline.", 0, (Sprite)null); return; } _lastRequestAt = DateTime.UtcNow; ((Character)requester).Message((MessageType)2, "Access request sent to the tombstone owner.", 0, (Sprite)null); ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "IronLabs_SealedTombstone_Request", new object[4] { requester.GetPlayerID(), requester.GetPlayerName(), tombstoneId, ownerId }); SealedTombstonePlugin.Log.LogInfo($"Player {requester.GetPlayerID()} requested access to {tombstoneId}."); } private static void ReceiveRequest(long sender, long requesterId, string requesterName, ZDOID tombstoneId, long ownerId) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; ZDOMan instance = ZDOMan.instance; ZDO tombstone = ((instance != null) ? instance.GetZDO(tombstoneId) : null); if (!((Object)(object)localPlayer == (Object)null) && localPlayer.GetPlayerID() == ownerId && _pendingRequest == null && !IsBlocked(tombstone, requesterId)) { _pendingRequest = new PendingRequest { RequesterPlayerId = requesterId, RequesterName = SafeName(requesterName), TombstoneId = tombstoneId, CreatedAt = DateTime.UtcNow }; ShowDecisionPopup(_pendingRequest); } } private static void ShowDecisionPopup(PendingRequest request) { //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Expected O, but got Unknown //IL_0079: Expected O, but got Unknown //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Expected O, but got Unknown if (!UnifiedPopup.IsAvailable()) { SealedTombstonePlugin.Log.LogWarning("The unlock request was rejected because the VV popup is unavailable."); Decide(request, accepted: false); return; } string text = "Allow " + request.RequesterName + " to loot this tombstone?"; UnifiedPopup.Push((PopupBase)new YesNoPopup("Tombstone access request", text, (PopupButtonCallback)delegate { Decide(request, accepted: true); }, (PopupButtonCallback)delegate { Decide(request, accepted: false); }, false)); } private static void Decide(PendingRequest request, bool accepted) { if (request != _pendingRequest || HasExpired(request)) { ClosePopup(); _pendingRequest = null; return; } ClosePopup(); _pendingRequest = null; SendDecision(request, accepted); Player localPlayer = Player.m_localPlayer; if (localPlayer != null) { ((Character)localPlayer).Message((MessageType)2, accepted ? "Tombstone access granted." : "Tombstone access denied.", 0, (Sprite)null); } } private static void SendDecision(PendingRequest request, bool accepted) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if (!((Object)(object)localPlayer == (Object)null) && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.Everybody, "IronLabs_SealedTombstone_Decision", new object[4] { request.RequesterPlayerId, request.TombstoneId, accepted, localPlayer.GetPlayerName() }); SealedTombstonePlugin.Log.LogInfo($"Tombstone request from {request.RequesterPlayerId} accepted={accepted}."); } } private static void ReceiveDecision(long sender, long requesterId, ZDOID tombstoneId, bool accepted, string ownerName) { //IL_002c: 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() == requesterId) { if (accepted) { Unlock(tombstoneId); } string text = (accepted ? (SafeName(ownerName) + " granted access to the tombstone.") : (SafeName(ownerName) + " denied or did not answer the request.")); ((Character)localPlayer).Message((MessageType)2, text, 0, (Sprite)null); } } private static void Unlock(ZDOID tombstoneId) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) ZNetScene instance = ZNetScene.instance; object obj; if (instance == null) { obj = null; } else { GameObject obj2 = instance.FindInstance(tombstoneId); obj = ((obj2 != null) ? obj2.GetComponent() : null); } ZNetView val = (ZNetView)obj; if ((Object)(object)val == (Object)null || !val.IsValid()) { SealedTombstonePlugin.Log.LogWarning($"Cannot unlock unavailable tombstone {tombstoneId}."); return; } val.ClaimOwnership(); val.GetZDO().Set(ZDOVars.s_owner, 0L); SealedTombstonePlugin.Log.LogInfo($"Tombstone {tombstoneId} was unlocked."); } private static bool IsBlocked(ZDO tombstone, long playerId) { return tombstone != null && RecentAttackers.Contains(tombstone.GetString("IronLabs.SealedTombstone.BlockedPlayers", ""), playerId); } private static bool IsExpired(ZDO tombstone) { long num = tombstone.GetLong("IronLabs.SealedTombstone.LockDay", -1L); long num2 = CurrentDay(); return num >= 0 && num2 >= 0 && num2 - num >= 10; } private static long CurrentDay() { return ((Object)(object)EnvMan.instance != (Object)null) ? EnvMan.instance.GetDay() : (-1); } private static bool HasExpired(PendingRequest request) { return (DateTime.UtcNow - request.CreatedAt).TotalSeconds > 30.0; } private static string SafeName(string name) { if (string.IsNullOrWhiteSpace(name)) { return "A player"; } string text = name.Replace("<", string.Empty).Replace(">", string.Empty); return (text.Length <= 64) ? text : text.Substring(0, 64); } private static void ClosePopup() { if (UnifiedPopup.IsAvailable() && UnifiedPopup.IsVisible()) { UnifiedPopup.Pop(); } } } [BepInPlugin("IronLabs.SealedTombstone", "IronLabs.SealedTombstone", "1.0.0")] public sealed class SealedTombstonePlugin : IronLabsPlugin { private const string PluginGuid = "IronLabs.SealedTombstone"; private const string PluginName = "IronLabs.SealedTombstone"; private const string PluginVersion = "1.0.0"; internal static ModLog Log { get; private set; } private void Awake() { Log = InitializePlugin("IronLabs.SealedTombstone"); Log.LogInfo("IronLabs.SealedTombstone 1.0.0 is loaded."); } private void Update() { TombstoneAccess.Tick(); } private void OnDestroy() { TombstoneAccess.ResetSession(); ShutdownPlugin(); Log = null; } } [HarmonyPatch(typeof(Game), "Start")] internal static class RpcRegistrationPatch { private static void Postfix() { TombstoneAccess.RegisterRpcs(); } } [HarmonyPatch(typeof(TombStone), "Setup")] internal static class TombstoneSetupPatch { private static void Postfix(TombStone __instance, long ownerUID) { TombstoneAccess.RecordLockDay(__instance, ownerUID); } } [HarmonyPatch(typeof(TombStone), "Interact")] internal static class TombstoneInteractPatch { private static bool Prefix(TombStone __instance, Humanoid character) { return TombstoneAccess.AllowInteraction(__instance, character); } } [HarmonyPatch(typeof(Player), "OnDamaged")] internal static class PlayerDamagedPatch { private static void Postfix(Player __instance, HitData hit) { RecentAttackers.Record(__instance, hit); } } [HarmonyPatch(typeof(Player), "OnDeath")] internal static class PlayerDeathPatch { private static void Prefix(Player __instance) { RecentAttackers.SnapshotForDeath(__instance); } } } namespace IronLabs.SharedLib { public abstract class IronLabsPlugin : BaseUnityPlugin { private Harmony _harmony; private bool _patchesApplied; protected ModLog InitializePlugin(string pluginGuid) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Expected O, but got Unknown ModLog modLog = new ModLog(((BaseUnityPlugin)this).Logger); Version version = ((object)this).GetType().Assembly.GetName().Version; modLog.LogInfo($"AssemblyVersion: {version}."); _harmony = new Harmony(pluginGuid); PatchOwnNamespace(modLog); return modLog; } protected void PatchOwnNamespace(ModLog log) { if (_patchesApplied) { log.LogDebug("Harmony patches are already active; skipping registration."); return; } string text = ((object)this).GetType().Namespace; Type[] types = Assembly.GetExecutingAssembly().GetTypes(); foreach (Type type in types) { if (type.Namespace == text) { _harmony.CreateClassProcessor(type).Patch(); } } _patchesApplied = true; log.LogDebug("Harmony patches were applied for the plugin namespace."); } protected void ShutdownPlugin() { if (_patchesApplied) { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _patchesApplied = false; } } } public sealed class ModLog { private readonly ManualLogSource _logger; public ModLog(ManualLogSource logger) { _logger = logger; } public void LogFatal(object message) { Write((LogLevel)1, message); } public void LogError(object message) { Write((LogLevel)2, message); } public void LogWarning(object message) { Write((LogLevel)4, message); } public void LogMessage(object message) { Write((LogLevel)8, message); } public void LogInfo(object message) { Write((LogLevel)16, message); } public void LogDebug(object message) { Write((LogLevel)32, message); } public void Log(LogLevel level, object message) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) Write(level, message); } private void Write(LogLevel level, object message) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) string arg = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); _logger.Log(level, (object)$"[{arg}] {message}"); } } }