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 PlayerTitles v1.2.0
PlayerTitles.dll
Decompiled a week agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using BepInEx; using HarmonyLib; using Jotunn; using Jotunn.Entities; using Jotunn.Managers; using Jotunn.Utils; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using PlayerTitles.Commands; using PlayerTitles.Diagnostics; using PlayerTitles.Networking; using PlayerTitles.Patches; using PlayerTitles.Storage; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: AssemblyCompany("PlayerTitles")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+6065a16c54c76ea2fa502c9ec24bb30f7da28c68")] [assembly: AssemblyProduct("PlayerTitles")] [assembly: AssemblyTitle("PlayerTitles")] [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 PlayerTitles { [BepInPlugin("com.playertitles.valheim", "PlayerTitles", "1.2.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [NetworkCompatibility(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string ModGuid = "com.playertitles.valheim"; public const string ModName = "PlayerTitles"; public const string ModVersion = "1.2.0"; private static GameObject tickerHost; private Harmony harmony; public static Plugin Instance { get; private set; } public TitleStore TitleStore { get; private set; } public NameCache NameCache { get; private set; } public RpcManager RpcManager { get; private set; } private void Awake() { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Expected O, but got Unknown //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Expected O, but got Unknown Instance = this; string text = Path.Combine(Paths.ConfigPath, "PlayerTitles"); Directory.CreateDirectory(text); TitleStore = new TitleStore(text); NameCache = new NameCache(text); RpcManager = new RpcManager(); tickerHost = new GameObject("PlayerTitles_TickerHost"); Object.DontDestroyOnLoad((Object)(object)tickerHost); tickerHost.AddComponent<RegistrationTicker>(); new PlayerTitlesCommands(); harmony = new Harmony("com.playertitles.valheim"); harmony.PatchAll(); ((BaseUnityPlugin)this).Logger.LogInfo((object)"PlayerTitles v1.2.0 loaded."); } } } namespace PlayerTitles.Storage { internal static class AtomicFile { public static void WriteAllText(string path, string contents) { string text = path + ".tmp"; File.WriteAllText(text, contents, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); if (File.Exists(path)) { File.Replace(text, path, null); } else { File.Move(text, path); } } } public class NameCache { private readonly string filePath; private Dictionary<string, NameCacheEntry> entries = new Dictionary<string, NameCacheEntry>(); public NameCache(string configDir) { filePath = Path.Combine(configDir, "namecache.json"); Load(); } public void Load() { if (!File.Exists(filePath)) { entries = new Dictionary<string, NameCacheEntry>(); Save(); } else { string text = File.ReadAllText(filePath); entries = (string.IsNullOrWhiteSpace(text) ? new Dictionary<string, NameCacheEntry>() : (JsonConvert.DeserializeObject<Dictionary<string, NameCacheEntry>>(text) ?? new Dictionary<string, NameCacheEntry>())); } } public void Save() { string contents = JsonConvert.SerializeObject((object)entries, (Formatting)1); AtomicFile.WriteAllText(filePath, contents); } public string UpdateEntry(string name, string steamId, DateTime lastSeenUtc) { string result = null; bool hadCollision = false; if (entries.TryGetValue(name, out var value)) { hadCollision = value.HadCollision; if (value.SteamId != steamId) { result = value.SteamId; hadCollision = true; } } entries[name] = new NameCacheEntry(steamId, lastSeenUtc, hadCollision); Save(); return result; } public bool TryResolve(string name, out NameCacheEntry entry) { return entries.TryGetValue(name, out entry); } public bool TryResolveNameBySteamId(string steamId, out string name) { name = ""; DateTime dateTime = DateTime.MinValue; bool flag = false; foreach (KeyValuePair<string, NameCacheEntry> entry in entries) { if (!(entry.Value.SteamId != steamId) && (!flag || !(entry.Value.LastSeenUtc <= dateTime))) { flag = true; dateTime = entry.Value.LastSeenUtc; name = entry.Key; } } return flag; } public IReadOnlyDictionary<string, NameCacheEntry> GetAll() { return entries; } } public class NameCacheEntry { public string SteamId { get; set; } public DateTime LastSeenUtc { get; set; } public bool HadCollision { get; set; } public NameCacheEntry() { } public NameCacheEntry(string steamId, DateTime lastSeenUtc, bool hadCollision = false) { SteamId = steamId; LastSeenUtc = lastSeenUtc; HadCollision = hadCollision; } } public class TitleEntry { public const string PositionPrefix = "above"; public const string PositionSuffix = "below"; public const string DefaultPosition = "above"; public const string DefaultColor = "#FFFFFF"; public string Title { get; set; } public string Position { get; set; } public string Color { get; set; } public TitleEntry() { } public TitleEntry(string title, string position, string color) { Title = title; Position = position; Color = color; } public static bool TryNormalisePosition(string raw, out string normalised) { if (string.Equals(raw, "above", StringComparison.OrdinalIgnoreCase) || string.Equals(raw, "prefix", StringComparison.OrdinalIgnoreCase)) { normalised = "above"; return true; } if (string.Equals(raw, "below", StringComparison.OrdinalIgnoreCase) || string.Equals(raw, "suffix", StringComparison.OrdinalIgnoreCase)) { normalised = "below"; return true; } normalised = "above"; return false; } } public class TitleStore { private readonly string filePath; private Dictionary<string, TitleEntry> titles = new Dictionary<string, TitleEntry>(); public TitleStore(string configDir) { filePath = Path.Combine(configDir, "titles.json"); Load(); } public void Load() { if (!File.Exists(filePath)) { titles = new Dictionary<string, TitleEntry>(); Save(); } else { string text = File.ReadAllText(filePath); titles = (string.IsNullOrWhiteSpace(text) ? new Dictionary<string, TitleEntry>() : (JsonConvert.DeserializeObject<Dictionary<string, TitleEntry>>(text) ?? new Dictionary<string, TitleEntry>())); } } public void Save() { string contents = JsonConvert.SerializeObject((object)titles, (Formatting)1); AtomicFile.WriteAllText(filePath, contents); } public bool TryGet(string steamId, out TitleEntry entry) { return titles.TryGetValue(steamId, out entry); } public void Set(string steamId, TitleEntry entry) { titles[steamId] = entry; Save(); } public bool Remove(string steamId) { if (!titles.Remove(steamId)) { return false; } Save(); return true; } public IReadOnlyDictionary<string, TitleEntry> GetAll() { return titles; } } } namespace PlayerTitles.Patches { [HarmonyPatch(typeof(ZNet), "RPC_CharacterID")] public static class CharacterIdTagPatch { public static class PendingTags { private class PendingTag { public ZDOID CharacterId; public string SteamId; public string PlayerName; public float ElapsedSeconds; public float SinceLastAttempt; public int Attempts; } private const float TimeoutSeconds = 5f; private const float RetryIntervalSeconds = 0.2f; private static readonly Dictionary<ZDOID, PendingTag> pending = new Dictionary<ZDOID, PendingTag>(); private static readonly List<ZDOID> completed = new List<ZDOID>(); public static void Enqueue(ZDOID characterId, string steamId, string playerName) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0058: 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) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0085: 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) if (!((ZDOID)(ref characterId)).IsNone()) { if (pending.TryGetValue(characterId, out var value)) { value.SteamId = steamId; value.PlayerName = playerName; Logger.LogInfo((object)$"PlayerTitles: PendingTags outcome=already-queued zdoid={characterId} attempts={value.Attempts} elapsed={value.ElapsedSeconds:0.00}s"); return; } pending[characterId] = new PendingTag { CharacterId = characterId, SteamId = steamId, PlayerName = playerName }; Logger.LogInfo((object)$"PlayerTitles: PendingTags outcome=enqueued zdoid={characterId} playerName={playerName} queueSize={pending.Count} -- ZDO not replicated to the server yet, retrying for {5f:0}s"); } } public static void Cancel(ZDOID characterId) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) if (pending.Remove(characterId)) { Logger.LogInfo((object)$"PlayerTitles: PendingTags outcome=cancelled zdoid={characterId} -- superseded"); } } public static void Tick(float deltaTime) { //IL_00d7: 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) //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_0207: Unknown result type (might be due to invalid IL or missing references) //IL_020c: Unknown result type (might be due to invalid IL or missing references) //IL_0213: Unknown result type (might be due to invalid IL or missing references) //IL_0190: Unknown result type (might be due to invalid IL or missing references) //IL_01d1: Unknown result type (might be due to invalid IL or missing references) if (pending.Count == 0) { return; } if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { Logger.LogInfo((object)$"PlayerTitles: PendingTags outcome=discarded count={pending.Count} -- no longer server"); pending.Clear(); return; } completed.Clear(); foreach (KeyValuePair<ZDOID, PendingTag> item in pending) { PendingTag value = item.Value; value.ElapsedSeconds += deltaTime; value.SinceLastAttempt += deltaTime; if (!(value.SinceLastAttempt < 0.2f) || value.Attempts <= 0) { value.SinceLastAttempt = 0f; value.Attempts++; ZDO val = ((ZDOMan.instance != null) ? ZDOMan.instance.GetZDO(value.CharacterId) : null); if (val != null) { val.Set("PlayerTitles_SteamID", value.SteamId); Logger.LogInfo((object)string.Format("PlayerTitles: PendingTags outcome=resolved zdoid={0} playerName={1} attempt={2} elapsed={3:0.00}s readback='{4}' ownerUid={5}", value.CharacterId, value.PlayerName, value.Attempts, value.ElapsedSeconds, val.GetString("PlayerTitles_SteamID", "<empty>"), val.GetOwner())); completed.Add(item.Key); } else if (value.ElapsedSeconds >= 5f) { Logger.LogWarning((object)$"PlayerTitles: GAVE UP tagging SteamID onto character ZDO {value.CharacterId} (player '{value.PlayerName}') after {value.Attempts} attempts over {value.ElapsedSeconds:0.00}s -- the ZDO never replicated to the server. This character will show no title until it next spawns."); completed.Add(item.Key); } } } foreach (ZDOID item2 in completed) { pending.Remove(item2); } completed.Clear(); } } public const string SteamIdZdoKey = "PlayerTitles_SteamID"; [HarmonyPostfix] private static void Postfix(ZNet __instance, ZRpc rpc, ZDOID characterID) { //IL_0019: 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_006c: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: 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_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) if (!__instance.IsServer()) { return; } ZNetPeer peer = PeerLookup.GetPeer(__instance, rpc); if (peer == null) { Logger.LogInfo((object)$"PlayerTitles: RPC_CharacterID branch=peer-null characterID={characterID}"); return; } if (peer.m_socket == null) { Logger.LogWarning((object)$"PlayerTitles: RPC_CharacterID -- peer {peer.m_uid} has no socket, cannot resolve SteamID for {characterID}"); return; } string hostName = peer.m_socket.GetHostName(); if (string.IsNullOrEmpty(hostName)) { Logger.LogInfo((object)$"PlayerTitles: RPC_CharacterID branch=no-steamid characterID={characterID} playerName={peer.m_playerName}"); return; } if (((ZDOID)(ref characterID)).IsNone()) { Logger.LogInfo((object)$"PlayerTitles: RPC_CharacterID branch=zdoid-none peer={peer.m_uid} playerName={peer.m_playerName} -- character cleared (respawn/logout), nothing to tag"); PendingTags.Cancel(characterID); return; } ZDO zDO = ZDOMan.instance.GetZDO(characterID); if (zDO == null) { Logger.LogInfo((object)$"PlayerTitles: RPC_CharacterID postSet zdoid={characterID} outcome=enqueued readback='<no zdo>' -- deferred to PendingTags"); PendingTags.Enqueue(characterID, hostName, peer.m_playerName); } else { zDO.Set("PlayerTitles_SteamID", hostName); Logger.LogInfo((object)string.Format("PlayerTitles: RPC_CharacterID postSet zdoid={0} outcome=tagged-direct readback='{1}' ownerUid={2}", characterID, zDO.GetString("PlayerTitles_SteamID", "<empty>"), zDO.GetOwner())); PendingTags.Cancel(characterID); } } } [HarmonyPatch(typeof(Terminal), "AddString", new Type[] { typeof(PlatformUserID), typeof(string), typeof(Type), typeof(bool) })] public static class ChatTitlePatch { [HarmonyPrefix] private static bool Prefix(Terminal __instance, PlatformUserID user, string text, Type type, bool timestamp) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0024: 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) PlayerInfo val = default(PlayerInfo); if (!ZNet.TryGetPlayerByPlatformUserID(user, ref val)) { return true; } TitleEntry titleEntry = ResolveTitle(val.m_name, val.m_characterID); if (titleEntry == null) { return true; } __instance.AddString(DecorateName(val.m_name, titleEntry), text, type, timestamp); return false; } private static string DecorateName(string name, TitleEntry title) { string text = "<color=" + title.Color + ">" + title.Title + "</color>"; if (!string.Equals(title.Position, "below", StringComparison.OrdinalIgnoreCase)) { return text + " " + name; } return name + " " + text; } private static TitleEntry ResolveTitle(string name, ZDOID characterId) { //IL_002d: Unknown result type (might be due to invalid IL or missing references) if (RpcManager.Instance.ClientTitlesByName.TryGetValue(name, out var value)) { return value; } if (((ZDOID)(ref characterId)).IsNone() || ZDOMan.instance == null) { return null; } ZDO zDO = ZDOMan.instance.GetZDO(characterId); if (zDO == null) { return null; } string text = zDO.GetString("PlayerTitles_SteamID", ""); if (string.IsNullOrEmpty(text)) { return null; } RpcManager.Instance.ClientTitles.TryGetValue(text, out var value2); return value2; } } [HarmonyPatch(typeof(Player), "GetHoverName")] public static class NameplateTitlePatch { [HarmonyPostfix] private static void Postfix(Player __instance, ref string __result) { RpcManager instance = RpcManager.Instance; if (instance != null && (instance.ClientTitles.Count != 0 || instance.ClientTitlesByName.Count != 0) && !((Object)(object)__instance == (Object)null) && !string.IsNullOrEmpty(__result)) { TitleEntry titleEntry = ResolveTitle(__instance, __result); if (titleEntry != null) { __result = (string.Equals(titleEntry.Position, "below", StringComparison.OrdinalIgnoreCase) ? (__result + " <color=" + titleEntry.Color + ">" + titleEntry.Title + "</color>") : ("<color=" + titleEntry.Color + ">" + titleEntry.Title + "</color> " + __result)); } } } private static TitleEntry ResolveTitle(Player player, string hoverName) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: 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) RpcManager instance = RpcManager.Instance; if (instance.ClientTitlesByName.TryGetValue(hoverName, out var value)) { return value; } if (ZDOMan.instance == null) { return null; } ZDOID zDOID = ((Character)player).GetZDOID(); if (((ZDOID)(ref zDOID)).IsNone()) { return null; } ZDO zDO = ZDOMan.instance.GetZDO(zDOID); if (zDO == null) { return null; } string text = zDO.GetString("PlayerTitles_SteamID", ""); if (string.IsNullOrEmpty(text)) { return null; } instance.ClientTitles.TryGetValue(text, out var value2); return value2; } } [HarmonyPatch(typeof(ZNet), "RPC_PeerInfo")] public static class PeerConnectedPatch { [HarmonyPostfix] private static void Postfix(ZNet __instance, ZRpc rpc) { if (!__instance.IsServer()) { return; } ZNetPeer peer = PeerLookup.GetPeer(__instance, rpc); if (peer == null || string.IsNullOrEmpty(peer.m_playerName)) { return; } if (peer.m_socket == null) { Logger.LogWarning((object)$"PlayerTitles: RPC_PeerInfo -- peer {peer.m_uid} ('{peer.m_playerName}') has no socket, cannot cache name -> SteamID"); return; } string hostName = peer.m_socket.GetHostName(); if (!string.IsNullOrEmpty(hostName)) { string playerName = peer.m_playerName; string text = Plugin.Instance.NameCache.UpdateEntry(playerName, hostName, DateTime.UtcNow); if (text != null) { Logger.LogWarning((object)("PlayerTitles: name cache collision for '" + playerName + "' -- previously SteamID " + text + ", now " + hostName + ". Most-recent-wins applied.")); } Logger.LogInfo((object)("PlayerTitles: name cache updated -- '" + playerName + "' -> " + hostName)); Plugin.Instance.RpcManager.PushFullStateTo(peer.m_uid); } } } public static class PeerLookup { public static ZNetPeer GetPeer(ZNet znet, ZRpc rpc) { if ((Object)(object)znet == (Object)null || rpc == null) { return null; } List<ZNetPeer> connectedPeers = znet.GetConnectedPeers(); if (connectedPeers == null) { return null; } for (int i = 0; i < connectedPeers.Count; i++) { ZNetPeer val = connectedPeers[i]; if (val != null && val.m_rpc == rpc) { return val; } } return null; } } } namespace PlayerTitles.Networking { public class RegistrationTicker : MonoBehaviour { private float inWorldSeconds; private bool loggedRegistrationTimeout; private bool loggedWaitingForWorld; private bool loggedTagRetryFailure; private bool loggedRegistrationFailure; private void Update() { try { RetryRegistration(); } catch (Exception arg) { if (!loggedRegistrationFailure) { loggedRegistrationFailure = true; Logger.LogError((object)$"PlayerTitles: RPC registration retry threw -- suppressing further reports of this. {arg}"); } } RetryPendingSteamIdTags(); } private void RetryRegistration() { RpcManager instance = RpcManager.Instance; if (instance == null || instance.Registered) { return; } instance.Register(); if (instance.Registered) { Logger.LogInfo((object)"PlayerTitles: RegistrationTicker -- RpcManager registered"); inWorldSeconds = 0f; loggedRegistrationTimeout = false; loggedWaitingForWorld = false; } else if ((Object)(object)ZNet.instance == (Object)null) { inWorldSeconds = 0f; loggedRegistrationTimeout = false; if (!loggedWaitingForWorld) { loggedWaitingForWorld = true; Logger.LogInfo((object)"PlayerTitles: RegistrationTicker -- not in a world yet, deferring RPC registration (normal at the main menu)"); } } else { loggedWaitingForWorld = false; inWorldSeconds += Time.deltaTime; if (inWorldSeconds >= 30f && !loggedRegistrationTimeout) { loggedRegistrationTimeout = true; Logger.LogWarning((object)"PlayerTitles: RegistrationTicker -- RpcManager STILL UNREGISTERED 30s after joining a world. Inbound RPCs will not be received on this machine."); } } } private void RetryPendingSteamIdTags() { try { CharacterIdTagPatch.PendingTags.Tick(Time.deltaTime); } catch (Exception arg) { if (!loggedTagRetryFailure) { loggedTagRetryFailure = true; Logger.LogError((object)$"PlayerTitles: SteamID tag retry threw -- suppressing further reports of this. {arg}"); } } } } public class RpcManager { public const string RpcRequestAssign = "PlayerTitles_RequestAssign"; public const string RpcRequestRevoke = "PlayerTitles_RequestRevoke"; public const string RpcRequestList = "PlayerTitles_RequestList"; public const string RpcCommandResult = "PlayerTitles_CommandResult"; public const string RpcListResult = "PlayerTitles_ListResult"; public const string RpcSyncAll = "PlayerTitles_SyncAll"; public const string RpcTitleChanged = "PlayerTitles_TitleChanged"; public const string RpcRequestDebugZdo = "PlayerTitles_RequestDebugZdo"; public readonly Dictionary<string, TitleEntry> ClientTitles = new Dictionary<string, TitleEntry>(); public readonly Dictionary<string, TitleEntry> ClientTitlesByName = new Dictionary<string, TitleEntry>(); private readonly Dictionary<string, string> nameBySteamId = new Dictionary<string, string>(); private bool registered; private ZRoutedRpc registeredOn; public static RpcManager Instance { get; private set; } public bool Registered { get { if (registered) { return registeredOn == ZRoutedRpc.instance; } return false; } } public RpcManager() { Instance = this; PrefabManager.OnVanillaPrefabsAvailable += Register; } public void Register() { if (!Registered && ZRoutedRpc.instance != null) { ZRoutedRpc.instance.Register<ZPackage>("PlayerTitles_RequestAssign", (Action<long, ZPackage>)RPC_RequestAssign); ZRoutedRpc.instance.Register<ZPackage>("PlayerTitles_RequestRevoke", (Action<long, ZPackage>)RPC_RequestRevoke); ZRoutedRpc.instance.Register<ZPackage>("PlayerTitles_RequestList", (Action<long, ZPackage>)RPC_RequestList); ZRoutedRpc.instance.Register<ZPackage>("PlayerTitles_CommandResult", (Action<long, ZPackage>)RPC_CommandResult); ZRoutedRpc.instance.Register<ZPackage>("PlayerTitles_ListResult", (Action<long, ZPackage>)RPC_ListResult); ZRoutedRpc.instance.Register<ZPackage>("PlayerTitles_SyncAll", (Action<long, ZPackage>)RPC_SyncAll); ZRoutedRpc.instance.Register<ZPackage>("PlayerTitles_TitleChanged", (Action<long, ZPackage>)RPC_TitleChanged); ZRoutedRpc.instance.Register<ZPackage>("PlayerTitles_RequestDebugZdo", (Action<long, ZPackage>)RPC_RequestDebugZdo); registeredOn = ZRoutedRpc.instance; registered = true; Logger.LogInfo((object)"PlayerTitles: RPCs registered"); } } public void SendRequestAssign(string playerName, string title, string position, string color) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { Register(); ZPackage val = new ZPackage(); val.Write(playerName); val.Write(title); val.Write(position); val.Write(color); ZRoutedRpc.instance.InvokeRoutedRPC("PlayerTitles_RequestAssign", new object[1] { val }); } } public void SendRequestRevoke(string playerName) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { Register(); ZPackage val = new ZPackage(); val.Write(playerName); ZRoutedRpc.instance.InvokeRoutedRPC("PlayerTitles_RequestRevoke", new object[1] { val }); } } public void SendRequestList() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { Register(); ZRoutedRpc.instance.InvokeRoutedRPC("PlayerTitles_RequestList", new object[1] { (object)new ZPackage() }); } } public void SendRequestDebugZdo(string token) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { Register(); ZPackage val = new ZPackage(); val.Write(token); ZRoutedRpc.instance.InvokeRoutedRPC("PlayerTitles_RequestDebugZdo", new object[1] { val }); } } private bool IsSenderAdmin(long sender) { if ((Object)(object)ZNet.instance == (Object)null) { Logger.LogInfo((object)$"PlayerTitles: IsSenderAdmin branch=znet-null sender={sender} playerName=N/A steamId=N/A result=false"); return false; } if (ZNet.instance.IsServer() && sender == ZNet.GetUID()) { Logger.LogInfo((object)$"PlayerTitles: IsSenderAdmin branch=self-routed-server sender={sender} playerName=N/A steamId=N/A result=true"); return true; } ZNetPeer peer = ZNet.instance.GetPeer(sender); if (peer == null) { Logger.LogInfo((object)$"PlayerTitles: IsSenderAdmin branch=peer-null sender={sender} playerName=N/A steamId=N/A result=false"); Logger.LogWarning((object)$"PlayerTitles: IsSenderAdmin - no peer for sender {sender} (not server UID {ZNet.GetUID()}) -- rejecting"); return false; } if (peer.m_socket == null) { Logger.LogInfo((object)$"PlayerTitles: IsSenderAdmin branch=peer-null-socket sender={sender} playerName={peer.m_playerName} steamId=N/A result=false"); Logger.LogWarning((object)$"PlayerTitles: IsSenderAdmin - peer {sender} has no socket -- rejecting"); return false; } string hostName = peer.m_socket.GetHostName(); bool flag = ZNet.instance.IsAdmin(hostName); Logger.LogInfo((object)string.Format("PlayerTitles: IsSenderAdmin branch={0} sender={1} playerName={2} steamId={3} result={4}", flag ? "peer-found-admin" : "peer-found-not-admin", sender, peer.m_playerName, hostName, flag)); return flag; } private static bool IsFromServer(long sender) { if ((Object)(object)ZNet.instance == (Object)null) { return false; } if (ZNet.instance.IsServer()) { return sender == ZNet.GetUID(); } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer != null) { return sender == serverPeer.m_uid; } return false; } private void SendResult(long targetPeer, string message) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { Register(); ZPackage val = new ZPackage(); val.Write(message); ZRoutedRpc.instance.InvokeRoutedRPC(targetPeer, "PlayerTitles_CommandResult", new object[1] { val }); } } private void RPC_RequestAssign(long sender, ZPackage pkg) { Logger.LogInfo((object)$"PlayerTitles: RPC_RequestAssign from sender {sender}"); string text = pkg.ReadString(); string text2 = pkg.ReadString(); string text3 = pkg.ReadString(); string text4 = pkg.ReadString(); if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { if (!IsSenderAdmin(sender)) { Logger.LogWarning((object)$"PlayerTitles: RPC_RequestAssign rejected -- sender {sender} is not admin"); SendResult(sender, "You must be a server admin to use this command."); return; } if (!TryResolveSteamId(text, out var steamId, out var error)) { SendResult(sender, error); return; } TitleEntry entry = new TitleEntry(text2, text3, text4); Plugin.Instance.TitleStore.Set(steamId, entry); BroadcastTitleChanged(steamId, entry); string text5 = "Assigned title '" + text2 + "' to '" + text + "' (position: " + text3 + ", color: " + text4 + ")."; Logger.LogInfo((object)("PlayerTitles: " + text5)); SendResult(sender, text5); } } private void RPC_RequestRevoke(long sender, ZPackage pkg) { Logger.LogInfo((object)$"PlayerTitles: RPC_RequestRevoke from sender {sender}"); string text = pkg.ReadString(); if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { if (!IsSenderAdmin(sender)) { Logger.LogWarning((object)$"PlayerTitles: RPC_RequestRevoke rejected -- sender {sender} is not admin"); SendResult(sender, "You must be a server admin to use this command."); return; } if (!TryResolveSteamId(text, out var steamId, out var error)) { SendResult(sender, error); return; } if (!Plugin.Instance.TitleStore.TryGet(steamId, out var _)) { SendResult(sender, "'" + text + "' has no title to revoke."); return; } Plugin.Instance.TitleStore.Remove(steamId); BroadcastTitleRemoved(steamId); string text2 = "Revoked title from '" + text + "'."; Logger.LogInfo((object)("PlayerTitles: " + text2)); SendResult(sender, text2); } } private void RPC_RequestList(long sender, ZPackage pkg) { //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Expected O, but got Unknown Logger.LogInfo((object)$"PlayerTitles: RPC_RequestList from sender {sender}"); if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } if (!IsSenderAdmin(sender)) { Logger.LogWarning((object)$"PlayerTitles: RPC_RequestList rejected -- sender {sender} is not admin"); SendResult(sender, "You must be a server admin to use this command."); return; } List<string> list = new List<string>(); IReadOnlyDictionary<string, TitleEntry> all = Plugin.Instance.TitleStore.GetAll(); if (all.Count == 0) { list.Add("No titles are currently awarded."); } else { foreach (KeyValuePair<string, TitleEntry> item in all) { string key = item.Key; TitleEntry value = item.Value; string name; string text = (Plugin.Instance.NameCache.TryResolveNameBySteamId(key, out name) ? name : "<unknown>"); list.Add(text + " | " + key + " | \"" + value.Title + "\" | " + value.Position + " | " + value.Color); } } ZPackage val = new ZPackage(); val.Write(list.Count); foreach (string item2 in list) { val.Write(item2); } if (ZRoutedRpc.instance != null) { Register(); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "PlayerTitles_ListResult", new object[1] { val }); } } private void RPC_RequestDebugZdo(long sender, ZPackage pkg) { string text = pkg.ReadString(); if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer()) { if (!IsSenderAdmin(sender)) { Logger.LogWarning((object)$"PlayerTitles: RPC_RequestDebugZdo REJECTED -- sender {sender} is not admin. token={text}. NO SERVER DUMP WAS PRODUCED for this token (this is an admin-gate rejection, not an empty result)."); SendResult(sender, "playertitles_debugzdo: server refused the dump -- you are not a server admin (token " + text + ")."); return; } ZdoDump.Begin(text, $"server-relayed(sender={sender})"); ZdoDump.DumpPeers(text); ZdoDump.DumpLocalPlayers(text, isServer: true); ZdoDump.End(text); SendResult(sender, "playertitles_debugzdo: server-side dump written to the server log (token " + text + ")."); } } private bool TryResolveSteamId(string playerName, out string steamId, out string error) { if (!Plugin.Instance.NameCache.TryResolve(playerName, out var entry)) { steamId = null; error = "No player named '" + playerName + "' has connected to this server. Title not assigned."; return false; } if (entry.HadCollision) { Logger.LogWarning((object)("PlayerTitles: '" + playerName + "' has been used by more than one SteamID -- resolving to the most recently seen SteamID " + entry.SteamId + ".")); } steamId = entry.SteamId; error = null; return true; } private static string ResolveNameForSteamId(string steamId) { if (!Plugin.Instance.NameCache.TryResolveNameBySteamId(steamId, out var name)) { return ""; } return name; } public void PushFullStateTo(long targetPeer) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Expected O, but got Unknown if (ZRoutedRpc.instance == null) { return; } Register(); IReadOnlyDictionary<string, TitleEntry> all = Plugin.Instance.TitleStore.GetAll(); ZPackage val = new ZPackage(); val.Write(all.Count); foreach (KeyValuePair<string, TitleEntry> item in all) { val.Write(item.Key); val.Write(ResolveNameForSteamId(item.Key)); val.Write(item.Value.Title); val.Write(item.Value.Position); val.Write(item.Value.Color); } ZRoutedRpc.instance.InvokeRoutedRPC(targetPeer, "PlayerTitles_SyncAll", new object[1] { val }); } private void BroadcastTitleChanged(string steamId, TitleEntry entry) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { Register(); ZPackage val = new ZPackage(); val.Write(steamId); val.Write(ResolveNameForSteamId(steamId)); val.Write(false); val.Write(entry.Title); val.Write(entry.Position); val.Write(entry.Color); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "PlayerTitles_TitleChanged", new object[1] { val }); } } private void BroadcastTitleRemoved(string steamId) { //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Expected O, but got Unknown if (ZRoutedRpc.instance != null) { Register(); ZPackage val = new ZPackage(); val.Write(steamId); val.Write(ResolveNameForSteamId(steamId)); val.Write(true); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "PlayerTitles_TitleChanged", new object[1] { val }); } } private void RPC_CommandResult(long sender, ZPackage pkg) { if (!IsFromServer(sender)) { Logger.LogWarning((object)$"PlayerTitles: RPC_CommandResult rejected -- sender {sender} is not the server"); return; } string text = pkg.ReadString(); if ((Object)(object)Console.instance != (Object)null) { Console.instance.Print(text); } Logger.LogInfo((object)("PlayerTitles: " + text)); } private void RPC_ListResult(long sender, ZPackage pkg) { if (!IsFromServer(sender)) { Logger.LogWarning((object)$"PlayerTitles: RPC_ListResult rejected -- sender {sender} is not the server"); return; } int num = pkg.ReadInt(); for (int i = 0; i < num; i++) { string text = pkg.ReadString(); if ((Object)(object)Console.instance != (Object)null) { Console.instance.Print(text); } } } private void RPC_SyncAll(long sender, ZPackage pkg) { if (!IsFromServer(sender)) { Logger.LogWarning((object)$"PlayerTitles: RPC_SyncAll rejected -- sender {sender} is not the server"); return; } Logger.LogInfo((object)"PlayerTitles: RPC_SyncAll received"); int num = pkg.ReadInt(); ClientTitles.Clear(); ClientTitlesByName.Clear(); nameBySteamId.Clear(); for (int i = 0; i < num; i++) { string key = pkg.ReadString(); string text = pkg.ReadString(); string title = pkg.ReadString(); string position = pkg.ReadString(); string color = pkg.ReadString(); TitleEntry value = new TitleEntry(title, position, color); ClientTitles[key] = value; if (!string.IsNullOrEmpty(text)) { ClientTitlesByName[text] = value; nameBySteamId[key] = text; } } Logger.LogInfo((object)$"PlayerTitles: synced {num} title(s) from server"); } private void RPC_TitleChanged(long sender, ZPackage pkg) { if (!IsFromServer(sender)) { Logger.LogWarning((object)$"PlayerTitles: RPC_TitleChanged rejected -- sender {sender} is not the server"); return; } string text = pkg.ReadString(); string text2 = pkg.ReadString(); if (pkg.ReadBool()) { ClientTitles.Remove(text); string value; string text3 = ((!string.IsNullOrEmpty(text2)) ? text2 : (nameBySteamId.TryGetValue(text, out value) ? value : null)); if (text3 != null) { ClientTitlesByName.Remove(text3); } nameBySteamId.Remove(text); Logger.LogInfo((object)("PlayerTitles: title removed for " + text)); return; } string title = pkg.ReadString(); string position = pkg.ReadString(); string color = pkg.ReadString(); TitleEntry value2 = new TitleEntry(title, position, color); ClientTitles[text] = value2; if (!string.IsNullOrEmpty(text2)) { ClientTitlesByName[text2] = value2; nameBySteamId[text] = text2; } Logger.LogInfo((object)("PlayerTitles: title updated for " + text)); } } } namespace PlayerTitles.Diagnostics { public static class ZdoDump { public static string NewToken() { return Guid.NewGuid().ToString("N").Substring(0, 8); } public static void Begin(string token, string origin) { Logger.LogInfo((object)("[PlayerTitles][DIAG] ===== debugzdo BEGIN token=" + token + " origin=" + origin + " =====")); Logger.LogInfo((object)string.Format("[PlayerTitles][DIAG] token={0} header znetNonNull={1} isServer={2} myUid={3} zdoManNonNull={4} zdoKey={5}", token, (Object)(object)ZNet.instance != (Object)null, (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer(), ((Object)(object)ZNet.instance != (Object)null) ? ZNet.GetUID() : 0, ZDOMan.instance != null, "PlayerTitles_SteamID")); } public static void End(string token) { Logger.LogInfo((object)("[PlayerTitles][DIAG] ===== debugzdo END token=" + token + " =====")); } public static void DumpPeers(string token) { //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ZNet.instance == (Object)null) { Logger.LogInfo((object)("[PlayerTitles][DIAG] token=" + token + " peers -- ZNet.instance is null, nothing to dump")); return; } long uID = ZNet.GetUID(); List<ZNetPeer> connectedPeers = ZNet.instance.GetConnectedPeers(); Logger.LogInfo((object)$"[PlayerTitles][DIAG] token={token} peers count={connectedPeers?.Count ?? 0} myUid={uID}"); if (connectedPeers == null) { return; } foreach (ZNetPeer item in connectedPeers) { if (item == null) { Logger.LogInfo((object)("[PlayerTitles][DIAG] token=" + token + " peer <null entry>")); continue; } string text = ((item.m_socket != null) ? item.m_socket.GetHostName() : "<null socket>"); ZDO val = null; if (ZDOMan.instance != null) { val = ZDOMan.instance.GetZDO(item.m_characterID); } string text2 = ((val != null) ? val.GetString("PlayerTitles_SteamID", "<empty>") : "<no zdo>"); string text3 = ((val != null) ? val.GetOwner().ToString() : "<no zdo>"); string text4 = ((val != null) ? (val.GetOwner() == uID).ToString() : "<no zdo>"); Logger.LogInfo((object)$"[PlayerTitles][DIAG] token={token} peer uid={item.m_uid} name={item.m_playerName} steamId={text} characterID={item.m_characterID} zdoNonNull={val != null} ownerUid={text3} ownerIsMe={text4} myUid={uID} tag='{text2}'"); } } public static void DumpLocalPlayers(string token, bool isServer) { //IL_00ea: Unknown result type (might be due to invalid IL or missing references) List<Player> allPlayers = Player.GetAllPlayers(); Logger.LogInfo((object)$"[PlayerTitles][DIAG] token={token} GetAllPlayers count={allPlayers?.Count ?? 0} isServer={isServer}"); if (allPlayers == null) { return; } long num = (((Object)(object)ZNet.instance != (Object)null) ? ZNet.GetUID() : 0); foreach (Player item in allPlayers) { if ((Object)(object)item == (Object)null) { Logger.LogInfo((object)("[PlayerTitles][DIAG] token=" + token + " player <null entry>")); continue; } ZNetView component = ((Component)item).GetComponent<ZNetView>(); bool flag = (Object)(object)component != (Object)null; ZDO val = (flag ? component.GetZDO() : null); if (val == null) { Logger.LogInfo((object)$"[PlayerTitles][DIAG] token={token} player name={item.GetPlayerName()} nviewNonNull={flag} zdoNonNull=false"); continue; } Logger.LogInfo((object)string.Format("[PlayerTitles][DIAG] token={0} player name={1} nviewNonNull={2} zdoNonNull=true zdoid={3} ownerUid={4} isOwner={5} myUid={6} tag='{7}'", token, item.GetPlayerName(), flag, val.m_uid, val.GetOwner(), val.IsOwner(), num, val.GetString("PlayerTitles_SteamID", "<empty>"))); } } } } namespace PlayerTitles.Commands { public class PlayerTitlesCommands { private class AssignCommand : ConsoleCommand { public override string Name => "playertitles_assign"; public override string Help => "Assign a title. Usage: playertitles_assign <playername> [--pos=prefix|suffix] [--color=#RRGGBB] <title text> (prefix puts the title before the name, suffix after it, in both the nameplate and chat; the legacy spellings above|below still work)"; public override bool IsCheat => false; public override bool IsSecret => false; public override bool OnlyServer => false; public override void Run(string[] args) { if (!CheckAdmin()) { return; } if (!TryParseAssignArgs(args, out var playerName, out var title, out var position, out var color)) { if ((Object)(object)Console.instance != (Object)null) { Console.instance.Print(((ConsoleCommand)this).Help); } else { Logger.LogInfo((object)("PlayerTitles: " + ((ConsoleCommand)this).Help)); } return; } Logger.LogInfo((object)("PlayerTitles: playertitles_assign '" + playerName + "' pos=" + position + " color=" + color + " title=\"" + title + "\"")); RpcManager.Instance.SendRequestAssign(playerName, title, position, color); } } private class RevokeCommand : ConsoleCommand { public override string Name => "playertitles_revoke"; public override string Help => "Revoke a title. Usage: playertitles_revoke <playername>"; public override bool IsCheat => false; public override bool IsSecret => false; public override bool OnlyServer => false; public override void Run(string[] args) { if (!CheckAdmin()) { return; } if (args.Length < 1) { if ((Object)(object)Console.instance != (Object)null) { Console.instance.Print(((ConsoleCommand)this).Help); } else { Logger.LogInfo((object)("PlayerTitles: " + ((ConsoleCommand)this).Help)); } } else { string text = args[0]; Logger.LogInfo((object)("PlayerTitles: playertitles_revoke '" + text + "'")); RpcManager.Instance.SendRequestRevoke(text); } } } private class ListCommand : ConsoleCommand { public override string Name => "playertitles_list"; public override string Help => "List every currently awarded title."; public override bool IsCheat => false; public override bool IsSecret => false; public override bool OnlyServer => false; public override void Run(string[] args) { if (CheckAdmin()) { Logger.LogInfo((object)"PlayerTitles: playertitles_list"); RpcManager.Instance.SendRequestList(); } } } private class DebugZdoCommand : ConsoleCommand { public override string Name => "playertitles_debugzdo"; public override string Help => "Diagnostic: dump SteamID ZDO tag state locally, and (from a client) ask the server to dump its own view into the server log."; public override bool IsCheat => false; public override bool IsSecret => false; public override bool OnlyServer => false; public override void Run(string[] args) { bool flag = (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer(); string text = ZdoDump.NewToken(); ZdoDump.Begin(text, flag ? "server-local" : "client-local"); if ((Object)(object)ZNet.instance == (Object)null) { Logger.LogInfo((object)("[PlayerTitles][DIAG] token=" + text + " ZNet.instance is null -- not in a world. Nothing further to dump.")); ZdoDump.End(text); return; } if (flag) { ZdoDump.DumpPeers(text); } ZdoDump.DumpLocalPlayers(text, flag); ZdoDump.End(text); if (!flag) { Logger.LogInfo((object)("[PlayerTitles][DIAG] token=" + text + " requesting server-side dump -- look for the same token in the SERVER log")); RpcManager.Instance.SendRequestDebugZdo(text); } } } private static readonly Regex ColorPattern = new Regex("^#[0-9A-Fa-f]{6}$", RegexOptions.Compiled); public PlayerTitlesCommands() { CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new AssignCommand()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new RevokeCommand()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new ListCommand()); CommandManager.Instance.AddConsoleCommand((ConsoleCommand)(object)new DebugZdoCommand()); } private static bool CheckAdmin() { if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { return true; } if (!SynchronizationManager.Instance.PlayerIsAdmin) { if ((Object)(object)Console.instance != (Object)null) { Console.instance.Print("You must be a server admin to use this command."); } else { Logger.LogInfo((object)"PlayerTitles: You must be a server admin to use this command."); } return false; } return true; } private static bool TryParseAssignArgs(string[] args, out string playerName, out string title, out string position, out string color) { playerName = null; title = null; position = "above"; color = "#FFFFFF"; if (args.Length < 2) { return false; } playerName = args[0]; string text = null; string text2 = null; bool flag = false; bool flag2 = false; int num = 1; while (num < args.Length) { string text3 = args[num]; if (!flag && text3.StartsWith("--pos=", StringComparison.OrdinalIgnoreCase)) { text = text3.Substring("--pos=".Length); flag = true; num++; continue; } if (flag2 || !text3.StartsWith("--color=", StringComparison.OrdinalIgnoreCase)) { break; } text2 = text3.Substring("--color=".Length); flag2 = true; num++; } if (num >= args.Length) { return false; } title = string.Join(" ", args.Skip(num)); if (flag && !TitleEntry.TryNormalisePosition(text, out position)) { string text4 = "Invalid --pos value '" + text + "' -- expected prefix|suffix (or the legacy above|below). Defaulting to 'above' (prefix)."; if ((Object)(object)Console.instance != (Object)null) { Console.instance.Print(text4); } Logger.LogWarning((object)("PlayerTitles: " + text4)); } if (flag2) { if (ColorPattern.IsMatch(text2)) { color = text2; } else { string text5 = "Invalid --color value '" + text2 + "' -- defaulting to '#FFFFFF'."; if ((Object)(object)Console.instance != (Object)null) { Console.instance.Print(text5); } Logger.LogWarning((object)("PlayerTitles: " + text5)); color = "#FFFFFF"; } } return true; } } }