Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Merki v1.0.0
plugins/Merki/Merki.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using Ezomic.Core; using HarmonyLib; using Microsoft.CodeAnalysis; using Splatform; using UnityEngine; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.6.2", FrameworkDisplayName = ".NET Framework 4.6.2")] [assembly: AssemblyCompany("Thijssen Software")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyCopyright("Copyright (c) 2026 Robbin Thijssen")] [assembly: AssemblyDescription("The server decides who is on the map, and death is public.")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+735db79afb4b94adbba736070f348a6cb58ec2b5")] [assembly: AssemblyProduct("Merki")] [assembly: AssemblyTitle("Merki")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [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 Merki { internal static class DeathMarks { private class Grave { internal Vector3 Pos; internal string Name; internal long PlayerID; internal float DiedAt; internal readonly HashSet<long> Decided = new HashSet<long>(); internal readonly HashSet<long> Holding = new HashSet<long>(); } private class Mark { internal Vector3 Pos; internal string Name; internal PinData Pin; internal float Expires; } internal struct MarkLine { internal Vector3 Pos; internal string Name; internal bool Drawn; internal float SecondsLeft; } internal struct GraveLine { internal Vector3 Pos; internal string Name; internal float SecondsLeft; internal int Decided; internal int Holding; } private const string ReportRpc = "Merki_DeathReport"; private const string MarkRpc = "Merki_DeathMark"; private const int AnimationSalt = 438569; private static readonly Dictionary<long, Grave> Graves = new Dictionary<long, Grave>(); private static readonly Dictionary<long, Mark> Marks = new Dictionary<long, Mark>(); private static readonly List<long> Expired = new List<long>(); private static Minimap _loadedMap; private static bool _introKeyBound; private static int _introKey; private static float Now => Time.realtimeSinceStartup; private static bool On { get { if (MerkiConfig.Enabled.Value) { return MerkiConfig.DeathMarks.Value; } return false; } } internal static int DrawnCount => Marks.Count; internal static int HeldCount => Graves.Count; [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "Awake")] private static void ResetForWorld() { Marks.Clear(); Graves.Clear(); _loadedMap = null; } [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "OnNewConnection")] private static void RegisterOnConnection(ZNet __instance, ZNetPeer peer) { if (peer != null && peer.m_rpc != null) { if (__instance.IsServer()) { peer.m_rpc.Register<Vector3>("Merki_DeathReport", (Action<ZRpc, Vector3>)OnReport); } else { peer.m_rpc.Register<Vector3, string, long, float>("Merki_DeathMark", (Method<Vector3, string, long, float>)OnMark); } } } [HarmonyPostfix] [HarmonyPatch(typeof(Minimap), "LoadMapData")] private static void MapLoaded(Minimap __instance) { _loadedMap = __instance; } [HarmonyPostfix] [HarmonyPatch(typeof(Player), "OnDeath")] private static void Report(Player __instance) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) if (!On || (Object)(object)__instance != (Object)(object)Player.m_localPlayer || (Object)(object)ZNet.instance == (Object)null) { return; } try { Vector3 position = ((Component)__instance).transform.position; if (ZNet.instance.IsServer()) { Keep(ZNet.GetUID(), position, __instance.GetPlayerID(), __instance.GetPlayerName()); return; } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer != null && serverPeer.m_rpc != null) { serverPeer.m_rpc.Invoke("Merki_DeathReport", new object[1] { position }); } } catch (Exception ex) { MerkiPlugin.Log.LogWarning((object)("Reporting a death failed: " + ex)); } } private static void OnReport(ZRpc rpc, Vector3 pos) { //IL_009a: Unknown result type (might be due to invalid IL or missing references) try { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } ZNetPeer val = PeerOf(rpc); if (val == null) { return; } ZDO val2 = CharacterOf(val); long num = ((val2 != null) ? val2.GetLong(ZDOVars.s_playerID, 0L) : 0); if (num == 0L) { if (MerkiConfig.Verbose.Value) { MerkiPlugin.Log.LogInfo((object)(val.m_playerName + " reported a death with no character the server knows - no mark kept.")); } return; } string text = val2.GetString(ZDOVars.s_playerName, ""); if (string.IsNullOrEmpty(text)) { text = val.m_playerName; } Keep(val.m_uid, pos, num, text); } catch (Exception ex) { MerkiPlugin.Log.LogWarning((object)("Keeping a death mark failed: " + ex)); } } private unsafe static void Keep(long sender, Vector3 pos, long playerID, string playerName) { //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) if (!On) { return; } if (!SharesPosition(sender)) { if (MerkiConfig.Verbose.Value) { MerkiPlugin.Log.LogInfo((object)(playerName + " died with their position private - no mark kept.")); } return; } Grave grave = new Grave { Pos = pos, Name = playerName, PlayerID = playerID, DiedAt = Now }; grave.Decided.Add(sender); if (Graves.TryGetValue(playerID, out var value)) { grave.Holding.UnionWith(value.Holding); } Graves[playerID] = grave; if (MerkiConfig.Verbose.Value) { ManualLogSource log = MerkiPlugin.Log; string[] obj = new string[6] { playerName, " died at ", null, null, null, null }; Vector3 val = pos; obj[2] = ((object)(*(Vector3*)(&val))/*cast due to .constrained prefix*/).ToString(); obj[3] = " - mark kept for "; obj[4] = MerkiConfig.DeathMarkMinutes.Value.ToString(); obj[5] = " minutes."; log.LogInfo((object)string.Concat(obj)); } } private static bool SharesPosition(long sender) { if (MerkiConfig.ForcePublic.Value) { return true; } if (sender == ZNet.GetUID()) { return ZNet.instance.IsReferencePositionPublic(); } return ZNet.instance.GetPeer(sender)?.m_publicRefPos ?? false; } [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "RPC_ServerSyncedPlayerData")] private static void OfferOnReport(ZNet __instance, ZRpc rpc, List<ZNetPeer> ___m_peers) { //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_01f1: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) if (Graves.Count == 0 || !__instance.IsServer() || !On) { return; } try { foreach (ZNetPeer ___m_peer in ___m_peers) { if (___m_peer.m_rpc != rpc) { continue; } if (!___m_peer.IsReady() || ((ZDOID)(ref ___m_peer.m_characterID)).IsNone()) { break; } ZDO val = CharacterOf(___m_peer); if (Visibility.Scoping && (val == null || InIntro(val))) { break; } long num = ((val != null) ? val.GetLong(ZDOVars.s_playerID, 0L) : 0); { foreach (Grave value in Graves.Values) { if (!value.Decided.Add(___m_peer.m_uid) || (num != 0L && num == value.PlayerID)) { continue; } float num2 = Remaining(value); if (num2 > 0f && Visibility.InScope(value.Pos, ___m_peer.m_refPos)) { ___m_peer.m_rpc.Invoke("Merki_DeathMark", new object[4] { value.Pos, value.Name, value.PlayerID, num2 }); value.Holding.Add(___m_peer.m_uid); if (MerkiConfig.Verbose.Value) { MerkiPlugin.Log.LogInfo((object)("Sent " + value.Name + "'s mark to " + ___m_peer.m_playerName + ", " + Mathf.CeilToInt(num2) + "s left.")); } } else if (value.Holding.Remove(___m_peer.m_uid)) { ___m_peer.m_rpc.Invoke("Merki_DeathMark", new object[4] { Vector3.zero, "", value.PlayerID, 0f }); if (MerkiConfig.Verbose.Value) { MerkiPlugin.Log.LogInfo((object)("Took " + value.Name + "'s old mark off " + ___m_peer.m_playerName + "'s map.")); } } } break; } } } catch (Exception ex) { MerkiPlugin.Log.LogWarning((object)("Offering death marks failed: " + ex)); } } private static ZNetPeer PeerOf(ZRpc rpc) { foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer.m_rpc == rpc) { return peer; } } return null; } private static ZDO CharacterOf(ZNetPeer peer) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (((ZDOID)(ref peer.m_characterID)).IsNone() || ZDOMan.instance == null) { return null; } return ZDOMan.instance.GetZDO(peer.m_characterID); } private static bool InIntro(ZDO character) { if (!_introKeyBound) { _introKey = ZSyncAnimation.GetHash("intro") + 438569; _introKeyBound = true; } return character.GetInt(_introKey, 0) != 0; } private static float Remaining(Grave grave) { return grave.DiedAt + MerkiConfig.DeathMarkMinutes.Value * 60f - Now; } internal static List<MarkLine> Drawn() { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) List<MarkLine> list = new List<MarkLine>(); float now = Now; foreach (Mark value in Marks.Values) { list.Add(new MarkLine { Pos = value.Pos, Name = value.Name, Drawn = (value.Pin != null), SecondsLeft = value.Expires - now }); } return list; } internal static List<GraveLine> Held() { //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) List<GraveLine> list = new List<GraveLine>(); foreach (Grave value in Graves.Values) { list.Add(new GraveLine { Pos = value.Pos, Name = value.Name, SecondsLeft = Remaining(value), Decided = value.Decided.Count, Holding = value.Holding.Count }); } return list; } private static void OfferToHost() { //IL_0063: 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_007b: Unknown result type (might be due to invalid IL or missing references) Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || (Visibility.Scoping && ((Character)localPlayer).InIntro())) { return; } long uID = ZNet.GetUID(); foreach (Grave value in Graves.Values) { if (value.Decided.Add(uID)) { float num = Remaining(value); if (num > 0f && Visibility.InScope(value.Pos, ((Component)localPlayer).transform.position)) { Add(value.Pos, value.Name, value.PlayerID, num); value.Holding.Add(uID); } else if (value.Holding.Remove(uID)) { Withdraw(value.PlayerID); } } } } private static void OnMark(ZRpc rpc, Vector3 pos, string playerName, long playerID, float secondsLeft) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) try { if (!((Object)(object)ZNet.instance == (Object)null) && !ZNet.instance.IsServer()) { if (secondsLeft <= 0f) { Withdraw(playerID); } else { Add(pos, playerName, playerID, secondsLeft); } } } catch (Exception ex) { MerkiPlugin.Log.LogWarning((object)("Drawing a death mark failed: " + ex)); } } private unsafe static void Add(Vector3 pos, string playerName, long playerID, float secondsLeft) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) if (On && !((Object)(object)Minimap.instance == (Object)null) && !IsLocalCharacter(playerID)) { if (Marks.TryGetValue(playerID, out var value) && value.Pin != null) { Minimap.instance.RemovePin(value.Pin); } Mark mark = new Mark { Pos = pos, Name = playerName, Expires = Now + secondsLeft }; Marks[playerID] = mark; Draw(mark); if (MerkiConfig.Verbose.Value) { ManualLogSource log = MerkiPlugin.Log; string[] obj = new string[7] { playerName, " died at ", null, null, null, null, null }; Vector3 val = pos; obj[2] = ((object)(*(Vector3*)(&val))/*cast due to .constrained prefix*/).ToString(); obj[3] = ", "; obj[4] = Mathf.CeilToInt(secondsLeft).ToString(); obj[5] = "s left"; obj[6] = ((mark.Pin != null) ? " - pin added." : " - pin held until the map loads."); log.LogInfo((object)string.Concat(obj)); } } } private static void Withdraw(long playerID) { if (Marks.TryGetValue(playerID, out var value)) { if (value.Pin != null && (Object)(object)Minimap.instance != (Object)null) { Minimap.instance.RemovePin(value.Pin); } Marks.Remove(playerID); if (MerkiConfig.Verbose.Value) { MerkiPlugin.Log.LogInfo((object)(value.Name + " died again out of scope - old pin removed.")); } } } private static bool IsLocalCharacter(long playerID) { if ((Object)(object)Game.instance != (Object)null) { PlayerProfile playerProfile = Game.instance.GetPlayerProfile(); if (playerProfile != null && playerProfile.GetPlayerID() == playerID) { return true; } } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { return localPlayer.GetPlayerID() == playerID; } return false; } private static void Draw(Mark mark) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) if (mark.Pin == null && !((Object)(object)Minimap.instance == (Object)null) && !((Object)(object)_loadedMap != (Object)(object)Minimap.instance)) { mark.Pin = Minimap.instance.AddPin(mark.Pos, (PinType)4, mark.Name, false, false, 0L, default(PlatformUserID)); } } internal static void Tick() { float now = Now; bool flag = On; if (Graves.Count > 0) { if (!flag || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { Graves.Clear(); } else { Expired.Clear(); foreach (KeyValuePair<long, Grave> grafe in Graves) { if (Remaining(grafe.Value) <= 0f) { Expired.Add(grafe.Key); } } foreach (long item in Expired) { Graves.Remove(item); } if (Graves.Count > 0) { OfferToHost(); } } } if (Marks.Count == 0) { return; } if ((Object)(object)Minimap.instance == (Object)null) { Marks.Clear(); return; } Expired.Clear(); foreach (KeyValuePair<long, Mark> mark in Marks) { Mark value = mark.Value; if (!flag || now >= value.Expires) { if (value.Pin != null) { Minimap.instance.RemovePin(value.Pin); } Expired.Add(mark.Key); } else { Draw(value); } } foreach (long item2 in Expired) { Marks.Remove(item2); } } } internal static class DevConsole { [HarmonyPatch(typeof(Terminal), "InitTerminal")] internal static class Hook { private static void Postfix() { Register(); } } [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__OnCommand; } private static bool _registered; private static void Register() { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Expected O, but got Unknown if (!_registered) { _registered = true; object obj = <>O.<0>__OnCommand; if (obj == null) { ConsoleEvent val = OnCommand; <>O.<0>__OnCommand = val; obj = (object)val; } new ConsoleCommand("merki", "merki show | players | marks | graves - the rule in force here, who the map has, and the gravestones on it", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); MerkiPlugin.Log.LogInfo((object)"Console command 'merki' registered - read-only, so no devcommands needed and running it marks nobody as having cheated."); } } private static void OnCommand(ConsoleEventArgs args) { Terminal context = args.Context; if (!((Object)(object)context == (Object)null)) { switch ((args.Length > 1) ? args[1].ToLowerInvariant() : "") { case "show": Show(context); return; case "marks": Marks(context); return; case "graves": Graves(context); return; case "players": Players(context); return; } context.AddString("merki show - the rule here, the checkbox, and your own saved answer"); context.AddString("merki players - the player list as this machine has it"); context.AddString("merki marks - the gravestones drawn on THIS map right now"); context.AddString("merki graves - what the server is still holding (host or singleplayer)"); } } private static void Show(Terminal term) { if ((Object)(object)ZNet.instance == (Object)null) { term.AddString("merki: no world - join or load one first."); return; } bool flag = ZNet.instance.IsServer(); term.AddString("merki 1.0.0 authority=" + (flag ? "server" : "client") + " enabled=" + YesNo(MerkiConfig.Enabled.Value)); term.AddString("forced=" + YesNo(Visibility.ForcedHere) + " locked=" + YesNo(Visibility.LockedHere) + " checkbox=" + Checkbox() + " clickable=" + Clickable() + " stored=" + (Visibility.StoredPublic ? "public" : "private")); term.AddString("scope range=" + Num(MerkiConfig.Range.Value) + " samebiome=" + YesNo(MerkiConfig.SameBiome.Value) + " scoping=" + OnOff(Visibility.Scoping)); term.AddString("death marks=" + OnOff(MerkiConfig.DeathMarks.Value) + " minutes=" + Num(MerkiConfig.DeathMarkMinutes.Value) + " drawn=" + DeathMarks.DrawnCount + " held=" + DeathMarks.HeldCount); int onMap; string names; int num = PlayerCounts(out onMap, out names); term.AddString("players=" + num + " onmap=" + onMap + " names=" + names); if (num > 0 && onMap < num) { term.AddString("(" + (num - onMap) + " of them carry no position - hidden, or out of scope from where you are standing)"); } } private static string Checkbox() { if ((Object)(object)Minimap.instance == (Object)null || (Object)(object)Minimap.instance.m_publicPosition == (Object)null) { return "nomap"; } if (!Minimap.instance.m_publicPosition.isOn) { return "unticked"; } return "ticked"; } private static string Clickable() { if ((Object)(object)Minimap.instance == (Object)null || (Object)(object)Minimap.instance.m_publicPosition == (Object)null) { return "nomap"; } return YesNo(((Selectable)Minimap.instance.m_publicPosition).interactable); } private static int PlayerCounts(out int onMap, out string names) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) onMap = 0; names = "ok"; List<PlayerInfo> playerList = ZNet.instance.GetPlayerList(); if (playerList == null) { return 0; } bool flag = false; bool flag2 = false; foreach (PlayerInfo item in playerList) { if (item.m_publicPosition) { onMap++; } if (string.IsNullOrEmpty(item.m_name) || item.m_name.Trim().Length == 0) { flag = true; continue; } string name = item.m_name; for (int i = 0; i < name.Length; i++) { if (name[i] < ' ') { flag2 = true; break; } } } if (flag) { names = "blank"; } else if (flag2) { names = "odd"; } return playerList.Count; } private static void Players(Terminal term) { //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00b1: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: 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 ((Object)(object)ZNet.instance == (Object)null) { term.AddString("merki: no world."); return; } List<PlayerInfo> playerList = ZNet.instance.GetPlayerList(); if (playerList == null || playerList.Count == 0) { term.AddString("players=0 - nobody, not even you. A dedicated server adds no entry of its own."); return; } term.AddString("players=" + playerList.Count); foreach (PlayerInfo item in playerList) { term.AddString(" [" + item.m_name + "] onmap=" + YesNo(item.m_publicPosition) + (item.m_publicPosition ? (" at=" + Mathf.RoundToInt(item.m_position.x) + "," + Mathf.RoundToInt(item.m_position.z)) : "")); } } private static void Marks(Terminal term) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Unknown result type (might be due to invalid IL or missing references) if (DeathMarks.DrawnCount == 0) { term.AddString("drawn=0 - no gravestones on this map."); return; } term.AddString("drawn=" + DeathMarks.DrawnCount); Player localPlayer = Player.m_localPlayer; Vector3 val = (((Object)(object)localPlayer == (Object)null) ? Vector3.zero : ((Component)localPlayer).transform.position); foreach (DeathMarks.MarkLine item in DeathMarks.Drawn()) { term.AddString(" " + item.Name + " pin=" + YesNo(item.Drawn) + " left=" + Mathf.CeilToInt(item.SecondsLeft) + "s" + (((Object)(object)localPlayer == (Object)null) ? "" : (" away=" + Mathf.RoundToInt(Utils.DistanceXZ(val, item.Pos)) + "m")) + " at=" + Mathf.RoundToInt(item.Pos.x) + "," + Mathf.RoundToInt(item.Pos.z)); } } private static void Graves(Terminal term) { //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { term.AddString("held=? - only the server holds graves, and this is a client."); return; } if (DeathMarks.HeldCount == 0) { term.AddString("held=0 - the server is holding no deaths."); return; } term.AddString("held=" + DeathMarks.HeldCount); foreach (DeathMarks.GraveLine item in DeathMarks.Held()) { string[] obj = new string[12] { " ", item.Name, " left=", Mathf.CeilToInt(item.SecondsLeft).ToString(), "s told=", null, null, null, null, null, null, null }; int decided = item.Decided; obj[5] = decided.ToString(); obj[6] = " holding="; decided = item.Holding; obj[7] = decided.ToString(); obj[8] = " at="; obj[9] = Mathf.RoundToInt(item.Pos.x).ToString(); obj[10] = ","; obj[11] = Mathf.RoundToInt(item.Pos.z).ToString(); term.AddString(string.Concat(obj)); } } private static string YesNo(bool value) { if (!value) { return "no"; } return "yes"; } private static string OnOff(bool value) { if (!value) { return "off"; } return "on"; } private static string Num(float value) { return value.ToString("0.###", CultureInfo.InvariantCulture); } } internal static class MerkiConfig { internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<bool> ForcePublic; internal static ConfigEntry<bool> LockMapToggle; internal static ConfigEntry<float> Range; internal static ConfigEntry<bool> SameBiome; internal static ConfigEntry<bool> DeathMarks; internal static ConfigEntry<float> DeathMarkMinutes; internal static ConfigEntry<bool> Verbose; internal static void Bind(ConfigFile cfg) { //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00d9: Expected O, but got Unknown Enabled = cfg.Bind<bool>("Merki", "Enabled", true, "Off leaves the plugin loaded and changing nothing. Not 'unloaded' - a plugin cannot unload itself."); ForcePublic = cfg.Bind<bool>("Visibility", "ForcePublic", true, "Everyone is on everyone's map, whether they like it or not. The server overwrites the 'share position' flag each client sends, so unticking the box on the map does nothing - the box is not where the answer is kept. Each player's own choice is kept as it was, not overwritten, so off restores vanilla, where each player chooses."); LockMapToggle = cfg.Bind<bool>("Visibility", "LockMapToggle", true, "Grey out the 'share position' checkbox on the map so it reads honestly. This changes nothing about who can see whom; it only stops a player believing they are hidden. Requires ForcePublic."); Range = cfg.Bind<float>("Visibility", "Range", 0f, "How far away another player can be and still appear on your map, in metres. 0 means the whole world, which is the usual answer and the cheapest - at 0 the server sends its own untouched player list. A number here makes the map a local thing: you see who is near you and nobody else. Height is ignored, so a cave below you still counts as near. Measured from where each player last reported in. The game reports positions every two seconds and the list goes out every two seconds, so the edge lags by a few seconds of travel: a few steps on foot, tens of metres on a mount or a ship. Also a map rule, not an invisibility one - a player close enough for the game to stream them to you is on your connection anyway, so a very small Range hides them from the map only."); SameBiome = cfg.Bind<bool>("Visibility", "SameBiome", false, "Only show players who are in the same biome as you. Combines with Range - both have to pass. Note that a crypt or a cave reads as the biome on the surface above it, because that is where the game says its coordinates are; a player underground is therefore still in the Swamp."); DeathMarks = cfg.Bind<bool>("Death", "DeathMarks", true, "Put a gravestone on the other players' maps when a player dies, at the spot they died, with their name under it on the large map. Yours is already marked by the game, so you are never sent your own. Only a player's latest death is shown: dying again moves their gravestone rather than adding a second, or takes it off your map if the new spot is one you would not be shown. The server remembers each mark until it expires, so a player who joins or reconnects later still gets it, with only the time that is left. It follows the same rule as the map: a player whose position is private sends no mark, and Range and SameBiome decide who receives one, measured from where you are when the death happens, or where you first stand if you join afterwards - for a new character, where the valkyrie puts you down, not where it picks you up. Nothing is saved: a restarted server has forgotten every mark, and your own map drops them when you log out."); DeathMarkMinutes = cfg.Bind<float>("Death", "DeathMarkMinutes", 30f, new ConfigDescription("How long a gravestone stays on the map, in minutes, from 1 to 30. Counted from the death on the server's clock, so a player who joins twenty minutes after somebody died sees that mark for the last ten, not for a fresh thirty. Long enough to go and help is the point; a gravestone that stays turns the map into a graveyard by the second evening.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 30f), Array.Empty<object>())); Verbose = cfg.Bind<bool>("Merki", "Verbose", false, "Write what was forced and what was hidden to BepInEx/LogOutput.log. Off unless something looks wrong; on a busy server it is chatty."); } } [BepInPlugin("ezomic.valheim.merki", "Merki", "1.0.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class MerkiPlugin : BaseUnityPlugin { public const string PluginGuid = "ezomic.valheim.merki"; public const string PluginName = "Merki"; public const string PluginVersion = "1.0.0"; public const string PluginAuthor = "Robbin Thijssen"; private const string CoreGuid = "ezomic.valheim.core"; internal static ManualLogSource Log; internal static bool CorePresent; private Harmony _harmony; private void Awake() { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; MerkiConfig.Bind(((BaseUnityPlugin)this).Config); TryRegisterWithCore(); ((BaseUnityPlugin)this).Config.SettingChanged += Visibility.OnSettingChanged; _harmony = new Harmony("ezomic.valheim.merki"); _harmony.PatchAll(typeof(Visibility)); _harmony.PatchAll(typeof(DeathMarks)); _harmony.PatchAll(typeof(DevConsole.Hook)); Log.LogInfo((object)"Merki 1.0.0 by Robbin Thijssen - ready."); } private void Update() { DeathMarks.Tick(); } private void TryRegisterWithCore() { CorePresent = Chainloader.PluginInfos.ContainsKey("ezomic.valheim.core"); if (!CorePresent) { Log.LogInfo((object)"Core not installed - running standalone, without the version gate. Forced visibility still holds; death marks will be missing for anyone who joins without this mod."); } else { RegisterWithCore(); } } [MethodImpl(MethodImplOptions.NoInlining)] private void RegisterWithCore() { Suite.Register("ezomic.valheim.merki", "Merki", "1.0.0", ((BaseUnityPlugin)this).Config, (Requirement)0, (Assembly)null); Suite.Sync((ConfigEntryBase[])(object)new ConfigEntryBase[7] { (ConfigEntryBase)MerkiConfig.Enabled, (ConfigEntryBase)MerkiConfig.ForcePublic, (ConfigEntryBase)MerkiConfig.LockMapToggle, (ConfigEntryBase)MerkiConfig.Range, (ConfigEntryBase)MerkiConfig.SameBiome, (ConfigEntryBase)MerkiConfig.DeathMarks, (ConfigEntryBase)MerkiConfig.DeathMarkMinutes }); Suite.Local((ConfigEntryBase)(object)MerkiConfig.Verbose); } private void OnDestroy() { if (_harmony != null) { _harmony.UnpatchSelf(); } } } internal static class Visibility { private const string RuleRpc = "Merki_Rule"; private static readonly MethodInfo UpdatePlayerListMethod = AccessTools.Method(typeof(ZNet), "UpdatePlayerList", (Type[])null, (Type[])null); private static readonly MethodInfo UpdatePlayerHistoryMethod = AccessTools.Method(typeof(ZNet), "UpdatePlayerHistory", (Type[])null, (Type[])null); private static bool _hostForces; private static bool _hostLocks; private static FieldRef<ZNet, bool> _storedPublicRef; private static bool _heldHostFlag; private static bool _holdingHostFlag; private static bool _savingMapData; internal static bool Scoping { get { if (!(MerkiConfig.Range.Value > 0f)) { return MerkiConfig.SameBiome.Value; } return true; } } internal static bool ForcedHere { get { if ((Object)(object)ZNet.instance == (Object)null || !MerkiConfig.Enabled.Value) { return false; } if (ZNet.instance.IsServer()) { return MerkiConfig.ForcePublic.Value; } return _hostForces; } } internal static bool LockedHere { get { if (!ForcedHere) { return false; } if (!ZNet.instance.IsServer()) { return _hostLocks; } return MerkiConfig.LockMapToggle.Value; } } internal static bool StoredPublic { get { if ((Object)(object)ZNet.instance == (Object)null) { return false; } try { if (_storedPublicRef == null) { _storedPublicRef = AccessTools.FieldRefAccess<ZNet, bool>("m_publicReferencePosition"); } return _storedPublicRef.Invoke(ZNet.instance); } catch { return false; } } } [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "Awake")] private static void RegisterRuleRpc() { _hostForces = false; _hostLocks = false; if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.Register<bool, bool>("Merki_Rule", (Action<long, bool, bool>)OnRule); } } [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "RPC_CharacterID")] private static void TellPeerTheRule(ZNet __instance, ZRpc rpc, List<ZNetPeer> ___m_peers) { if (!__instance.IsServer() || ZRoutedRpc.instance == null) { return; } foreach (ZNetPeer ___m_peer in ___m_peers) { if (___m_peer.m_rpc == rpc) { if (___m_peer.IsReady()) { ZRoutedRpc.instance.InvokeRoutedRPC(___m_peer.m_uid, "Merki_Rule", new object[2] { ForcedHere, LockedHere }); } break; } } } internal static void OnSettingChanged(object sender, SettingChangedEventArgs args) { ConfigEntryBase changedSetting = args.ChangedSetting; if (((object)changedSetting == MerkiConfig.Enabled || (object)changedSetting == MerkiConfig.ForcePublic || (object)changedSetting == MerkiConfig.LockMapToggle) && !((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(0L, "Merki_Rule", new object[2] { ForcedHere, LockedHere }); if ((Object)(object)Minimap.instance != (Object)null) { ApplyToggle(Minimap.instance); } } } private static void OnRule(long sender, bool forces, bool locks) { if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return; } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer != null && serverPeer.m_uid == sender) { _hostForces = forces; _hostLocks = locks; if (MerkiConfig.Verbose.Value) { MerkiPlugin.Log.LogInfo((object)("Server rule: forced public " + forces + ", checkbox locked " + locks + ".")); } if ((Object)(object)Minimap.instance != (Object)null) { ApplyToggle(Minimap.instance); } } } [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "RPC_ServerSyncedPlayerData")] private static void ForcePeerPublic(ZNet __instance, ZRpc rpc, List<ZNetPeer> ___m_peers) { if (!MerkiConfig.Enabled.Value || !MerkiConfig.ForcePublic.Value || !__instance.IsServer()) { return; } foreach (ZNetPeer ___m_peer in ___m_peers) { if (___m_peer.m_rpc == rpc) { if (!___m_peer.m_publicRefPos && MerkiConfig.Verbose.Value) { MerkiPlugin.Log.LogInfo((object)("Forcing public position for " + ___m_peer.m_playerName + ".")); } ___m_peer.m_publicRefPos = true; break; } } } [HarmonyPrefix] [HarmonyPatch(typeof(ZNet), "UpdatePlayerList")] private static void ForceHostEntry(ZNet __instance, ref bool ___m_publicReferencePosition) { _holdingHostFlag = false; if (__instance.IsServer() && ForcedHere) { _heldHostFlag = ___m_publicReferencePosition; _holdingHostFlag = true; ___m_publicReferencePosition = true; } } [HarmonyFinalizer] [HarmonyPatch(typeof(ZNet), "UpdatePlayerList")] private static void RestoreHostFlag(ref bool ___m_publicReferencePosition) { if (_holdingHostFlag) { ___m_publicReferencePosition = _heldHostFlag; _holdingHostFlag = false; } } [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "IsReferencePositionPublic")] private static void ShowForced(ref bool __result) { if (!(_savingMapData | __result) && ForcedHere) { __result = true; } } [HarmonyPrefix] [HarmonyPatch(typeof(Minimap), "GetMapData")] private static void BeginMapSave() { _savingMapData = true; } [HarmonyFinalizer] [HarmonyPatch(typeof(Minimap), "GetMapData")] private static void EndMapSave() { _savingMapData = false; } [HarmonyPrefix] [HarmonyPatch(typeof(Minimap), "OnTogglePublicPosition")] private static bool KeepOwnChoice() { return !ForcedHere; } [HarmonyPostfix] [HarmonyPatch(typeof(Minimap), "SetMapMode")] private static void LockMapToggle(Minimap __instance) { ApplyToggle(__instance); } private static void ApplyToggle(Minimap map) { if (!((Object)(object)map.m_publicPosition == (Object)null)) { bool lockedHere = LockedHere; if (lockedHere) { map.m_publicPosition.isOn = true; } ((Selectable)map.m_publicPosition).interactable = !lockedHere; } } internal static bool InScope(Vector3 who, Vector3 viewer) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) float value = MerkiConfig.Range.Value; if (value > 0f) { float num = who.x - viewer.x; float num2 = who.z - viewer.z; if (num * num + num2 * num2 > value * value) { return false; } } if (MerkiConfig.SameBiome.Value && WorldGenerator.instance != null && WorldGenerator.instance.GetBiome(who) != WorldGenerator.instance.GetBiome(viewer)) { return false; } return true; } [HarmonyPrefix] [HarmonyPatch(typeof(ZNet), "SendPlayerList")] private unsafe static bool ScopedPlayerList(ZNet __instance, List<PlayerInfo> ___m_players, List<ZNetPeer> ___m_peers) { //IL_0057: 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_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Expected O, but got Unknown //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0086: 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_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: 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) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) if (!MerkiConfig.Enabled.Value || !Scoping) { return true; } if (!__instance.IsServer()) { return true; } UpdatePlayerListMethod.Invoke(__instance, null); if (___m_peers.Count <= 0) { return false; } foreach (ZNetPeer ___m_peer in ___m_peers) { if (!___m_peer.IsReady()) { continue; } Vector3 refPos = ___m_peer.m_refPos; ZPackage val = new ZPackage(); val.Write(___m_players.Count); foreach (PlayerInfo ___m_player in ___m_players) { val.Write(___m_player.m_name); val.Write(___m_player.m_characterID); PlatformUserID id = ___m_player.m_userInfo.m_id; val.Write(((object)(*(PlatformUserID*)(&id))/*cast due to .constrained prefix*/).ToString()); val.Write(___m_player.m_userInfo.m_displayName); val.Write(___m_player.m_userInfo.m_serverAssignedDisplayName); val.Write(___m_player.m_userInfo.m_playfabId); bool flag = ___m_player.m_publicPosition && InScope(___m_player.m_position, refPos); val.Write(flag); if (flag) { val.Write(___m_player.m_position); } } ___m_peer.m_rpc.Invoke("PlayerList", new object[1] { val }); } UpdatePlayerHistoryMethod.Invoke(__instance, null); return false; } [HarmonyPostfix] [HarmonyPatch(typeof(ZNet), "GetOtherPublicPlayers")] private static void ScopedHostView(ZNet __instance, List<PlayerInfo> playerList) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) if (!MerkiConfig.Enabled.Value || !Scoping || !__instance.IsServer()) { return; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null) { return; } Vector3 position = ((Component)localPlayer).transform.position; for (int num = playerList.Count - 1; num >= 0; num--) { if (!InScope(playerList[num].m_position, position)) { playerList.RemoveAt(num); } } } } }