Decompiled source of TombstoneLock v1.0.5

TombstoneLock.dll

Decompiled 8 hours ago
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<char>()).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 <Postfix>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 = "<b>$msg_cantopen\n\n$tombstonelock_request_sent</b>";

	public const string MsgOwnerOffline = "<b>$msg_cantopen\n\n$tombstonelock_owner_offline</b>";

	public const string MsgCannotSendRequestNow = "<b>$msg_cantopen\n\n$tombstonelock_cannot_send_request_now</b>";

	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<long, string, ZDOID>("RPC_RequestTombstoneUnlock", (Action<long, long, string, ZDOID>)RPC_RequestTombstoneUnlock);
			ZRoutedRpc.instance.Register<long, ZDOID, string>("RPC_ConfirmTombstoneUnlock", (Action<long, long, ZDOID, string>)RPC_ConfirmTombstoneUnlock);
			ZRoutedRpc.instance.Register<long>("RPC_TombstoneRequestExpired", (Action<long, long>)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<ZNetView>();
		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<ZNetView>();
		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("<b>$msg_cantopen\n\n$tombstonelock_cannot_send_request_now</b>"), num2);
			OnScreenNotifier.Message(val, (MessageType)2, message);
			return false;
		}
		if (IsPlayerOnline(num))
		{
			lastSentRequestTime = DateTime.Now;
			OnScreenNotifier.Message(val, (MessageType)2, "<b>$msg_cantopen\n\n$tombstonelock_request_sent</b>");
			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, "<b>$msg_cantopen\n\n$tombstonelock_owner_offline</b>");
			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<ZNetView>();
			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;
	}
}