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 RavenEye v0.2.2
plugins/RavenEye.dll
Decompiled 17 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using RavenIron.RavenEye.Config; using RavenIron.RavenEye.Core; using RavenIron.RavenEye.Net; using RavenIron.RavenEye.Server; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("Raven Iron")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.2.0")] [assembly: AssemblyInformationalVersion("0.2.2+7ac39e573be1dfa612d5705f056b8a29dcfcfae0")] [assembly: AssemblyProduct("RavenEye")] [assembly: AssemblyTitle("RavenEye")] [assembly: AssemblyVersion("0.2.2.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 RavenIron.RavenEye { [BepInPlugin("com.raveniron.raveneye", "RavenEye", "0.2.2")] [BepInProcess("valheim.exe")] [BepInProcess("valheim_server.exe")] public class RavenEye : BaseUnityPlugin { public const string PluginId = "com.raveniron.raveneye"; public const string PluginName = "RavenEye"; public const string PluginVersion = "0.2.2"; private Harmony _harmony; public static RavenEye Instance { get; private set; } public static ManualLogSource Log { get; private set; } public static bool HasRenderer => (int)SystemInfo.graphicsDeviceType != 4; private void Awake() { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; ModConfig.Bind(((BaseUnityPlugin)this).Config); ((Component)this).gameObject.AddComponent<RavenEyeTick>(); _harmony = new Harmony("com.raveniron.raveneye"); List<string> list = PatchInstall.Each(AccessTools.GetTypesFromAssembly(typeof(RavenEye).Assembly), delegate(Type t) { if (t.GetCustomAttributes(typeof(HarmonyPatch), inherit: false).Length != 0) { _harmony.CreateClassProcessor(t).Patch(); } }, delegate(Type t, Exception ex) { Log.LogError((object)("patch " + t.Name + " could not be installed (" + ex.GetType().Name + ": " + ex.Message + "); that part of the mod is off, the rest carries on. A Valheim update probably moved its target.")); }); Log.LogInfo((object)(string.Format("{0} v{1} loaded — renderer={2}, ", "RavenEye", "0.2.2", HasRenderer) + $"patches={_harmony.GetPatchedMethods().Count()}" + ((list.Count > 0) ? string.Format(" ({0} FAILED: {1})", list.Count, string.Join(", ", list)) : "") + ", role is decided when a world loads.")); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } _harmony = null; Instance = null; } } internal static class BuildVersion { public const string Value = "0.2.2"; } } namespace RavenIron.RavenEye.Server { public static class AdminGate { private static bool _failureLogged; public static bool IsAdmin(ZNet znet, ZNetPeer peer) { if ((Object)(object)znet == (Object)null || peer == null) { return false; } string text; try { ISocket socket = peer.m_socket; text = ((socket != null) ? socket.GetHostName() : null); } catch { return false; } if (string.IsNullOrEmpty(text)) { return false; } try { return Check(znet, text); } catch (Exception ex) { if (!_failureLogged) { _failureLogged = true; RavenEye.Log.LogError((object)("AdminGate: ZNet.IsAdmin(string) threw " + ex.GetType().Name + ": " + ex.Message + ". Nobody will be granted the map. If this is a MissingMethodException, Valheim's API moved.")); } return false; } } [MethodImpl(MethodImplOptions.NoInlining)] private static bool Check(ZNet znet, string host) { return znet.IsAdmin(host); } public static int AdminListCount(ZNet znet) { try { return ((znet == null) ? ((int?)null) : znet.GetAdminList()?.Count) ?? (-1); } catch { return -1; } } } } namespace RavenIron.RavenEye.Patches { [HarmonyPatch(typeof(Achievements), "IsCleanNoMap")] public static class Patch_Achievements_IsCleanNoMap { private static int _warned; private static void Postfix(ref bool __result) { try { if (__result) { __result = Grant.CleanNoMap(__result, RavenEyeTick.EvaluateGrant(ZNet.instance)); } } catch (Exception ex) { __result = false; if (_warned++ < 3) { RavenEye.Log.LogWarning((object)("IsCleanNoMap postfix threw (" + ex.GetType().Name + ": " + ex.Message + "); no-map credit withheld.")); } } } } [HarmonyPatch(typeof(Game), "UpdateNoMap")] public static class Patch_Game_UpdateNoMap { private static int _warned; private static void Postfix() { try { if (!RavenEyeTick.EvaluateGrant(ZNet.instance)) { return; } Minimap instance = Minimap.instance; if (!((Object)(object)instance == (Object)null)) { bool flag = false; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { flag = PlatformPrefs.GetFloat("mapenabled_" + localPlayer.GetPlayerName(), 1f) == 0f; } Game.m_noMap = flag; instance.SetMapMode((MapMode)(!flag)); } } catch (Exception ex) { if (_warned++ < 3) { RavenEye.Log.LogWarning((object)("UpdateNoMap postfix threw (" + ex.GetType().Name + ": " + ex.Message + "); vanilla's decision stands.")); } } } } [HarmonyPatch(typeof(Terminal), "InitTerminal")] public static class Patch_Terminal_RavenEye { [CompilerGenerated] private static class <>O { public static ConsoleEvent <0>__Run; } private static void Postfix() { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown try { object obj = <>O.<0>__Run; if (obj == null) { ConsoleEvent val = Run; <>O.<0>__Run = val; obj = (object)val; } new ConsoleCommand("raveneye", "RavenEye: status | roster | map on|off", (ConsoleEvent)obj, false, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } catch (Exception ex) { RavenEye.Log.LogWarning((object)("raveneye console: registration failed: " + ex.Message)); } } private static void Run(ConsoleEventArgs args) { try { switch ((args.Args.Length > 1) ? args.Args[1].ToLowerInvariant() : "status") { case "status": Status(args); break; case "roster": Roster(args); break; case "map": Map(args); break; default: Help(args); break; } } catch (Exception ex) { Say(args, "raveneye: " + ex.Message); RavenEye.Log.LogWarning((object)$"raveneye console threw: {ex}"); } } private static void Help(ConsoleEventArgs args) { Say(args, "raveneye status — role, grant, and in words why the map is or is not showing"); Say(args, "raveneye roster — every player the roster carries (admins and hosts)"); Say(args, "raveneye map on|off — use, or refuse, the map the server grants this client"); } private static void Status(ConsoleEventArgs args) { ZNet instance = ZNet.instance; Say(args, "RavenEye v0.2.2 — role=" + RavenEyeTick.Role() + ", renderer=" + (RavenEye.HasRenderer ? "yes" : "no")); if ((Object)(object)instance == (Object)null) { Say(args, " no world loaded."); return; } bool flag = (Object)(object)ZoneSystem.instance != (Object)null && ZoneSystem.instance.GetGlobalKey((GlobalKeys)31); Say(args, " world NoMap key: " + (flag ? "set (no-map world)" : "not set (this world has a map)")); if (instance.IsServer()) { Say(args, string.Format(" server: Enabled={0}, interval {1}s, ", ModConfig.ServerEnabled.Value, F(ModConfig.IntervalSeconds.Value, "0.##")) + "grace " + F(ModConfig.GraceSeconds.Value, "0") + "s, deathGrace " + F(ModConfig.DeathGraceSeconds.Value, "0") + "s, " + $"RevealWhenMapEnabled={ModConfig.RevealWhenMapEnabled.Value}"); Say(args, (RosterSync.ClosedGate.Length > 0) ? (" gate: CLOSED — " + RosterSync.ClosedGate) : " gate: open"); int num = AdminGate.AdminListCount(instance); Say(args, $" adminlist.txt entries: {num}" + (RosterSync.AdminListUnreadable ? " <- reads as EMPTY with players online: treated as a failed read, nobody is demoted; re-save the file if it is not actually empty" : "")); if (RosterSync.LastDropped > 0) { Say(args, string.Format(" wire cap: {0} entr{1} over the {2}-entry cap were not sent (deaths first)", RosterSync.LastDropped, (RosterSync.LastDropped == 1) ? "y" : "ies", 64)); } if (RosterSync.LastSendTime < 0f) { Say(args, " roster: never built yet (first cadence pending)"); } else if (RosterSync.ClosedGate.Length > 0) { Say(args, " roster: none — gate closed, last cadence " + Age(RosterSync.LastSendTime) + " ago"); } else { Say(args, string.Format(" roster: {0} entr{1} ", RosterSync.LastRosterSize, (RosterSync.LastRosterSize == 1) ? "y" : "ies") + string.Format("({0} dead snapshot{1}), ", RosterSync.DeadSnapshots, (RosterSync.DeadSnapshots == 1) ? "" : "s") + $"sent to {RosterSync.LastSendAdmins} admin(s), {RosterSync.GrantedPeerCount} granted, {Age(RosterSync.LastSendTime)} ago"); } } if (!RavenEye.HasRenderer) { return; } Grant.Reason r = RavenEyeTick.ExplainGrant(instance); bool flag2 = Grant.IsGranted(r); Say(args, " grant: " + (flag2 ? "YES" : "no") + " — " + RavenEyeTick.Words(r)); if (!instance.IsServer()) { Say(args, " receiver registered on this connection: " + (RosterSync.IsRegisteredOnCurrentConnection(instance) ? "yes" : "NO")); if (RosterSync.LastReceipt < 0f && RosterSync.LastRevoke < 0f) { Say(args, " roster: never received since this join"); } else { Say(args, string.Format(" roster: {0} entr{1}, ", RosterSync.Current(instance).Count, (RosterSync.Current(instance).Count == 1) ? "y" : "ies") + $"{RosterSync.LastAppended} appended to vanilla's pins last frame; " + $"{RosterSync.PacketsReceived} received, {RosterSync.RevokesReceived} revoke(s), {RosterSync.PacketsRejected} rejected"); } Say(args, string.Format(" format: this build v{0}; server RavenEye {1}", 1, (RosterSync.ServerVersion.Length > 0) ? ("v" + RosterSync.ServerVersion) : "unknown — no packet yet") + ((RosterSync.PacketsRejected > 0) ? ("; last rejection: " + RosterSync.LastRejectReason) : "")); } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null) { bool flag3 = RavenEyeTick.PersonalOptOut(localPlayer); Say(args, flag3 ? (" character opt-out: THIS CHARACTER'S OWN MAP IS OFF (vanilla `nomap`, pref " + RavenEyeTick.PersonalKey(localPlayer) + " = 0) — `raveneye map on` clears it") : " character opt-out: none"); } Minimap instance2 = Minimap.instance; Say(args, string.Format(" Game.m_noMap={0} Minimap mode={1} corrections this session: {2} (zero is normal)", Game.m_noMap, ((Object)(object)instance2 != (Object)null) ? ((object)Unsafe.As<MapMode, MapMode>(ref instance2.m_mode)/*cast due to .constrained prefix*/).ToString() : "no minimap", RavenEyeTick.Reapplies)); } private static void Roster(ConsoleEventArgs args) { //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_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { Say(args, "raveneye: no world loaded."); return; } if (!instance.IsServer() && !RavenEyeTick.EvaluateGrant(instance)) { Say(args, "raveneye: no roster — this client is not granted. See `raveneye status`."); return; } IList<RosterEntry> list = RosterSync.Current(instance); Say(args, string.Format("raveneye: {0} entr{1}", list.Count, (list.Count == 1) ? "y" : "ies")); for (int i = 0; i < list.Count; i++) { RosterEntry rosterEntry = list[i]; Vector3 position = rosterEntry.Position; Say(args, " " + rosterEntry.Name + (rosterEntry.Dead ? " (dead — last known)" : "") + " at (" + F(position.x, "0") + ", " + F(position.y, "0") + ", " + F(position.z, "0") + ")"); } } private static void Map(ConsoleEventArgs args) { string text = ((args.Args.Length > 2) ? args.Args[2].ToLowerInvariant() : ""); if (!(text == "on")) { if (!(text == "off")) { Say(args, "raveneye map is " + (ModConfig.ShowMap.Value ? "on" : "off") + ". Use `raveneye map on` or `raveneye map off`."); return; } ModConfig.ShowMap.Value = false; } else { ModConfig.ShowMap.Value = true; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && RavenEyeTick.PersonalOptOut(localPlayer)) { PlatformPrefs.SetFloat(RavenEyeTick.PersonalKey(localPlayer), 1f); Say(args, "raveneye: cleared this character's own `nomap` opt-out as well."); } } Say(args, "raveneye: map " + (ModConfig.ShowMap.Value ? "on" : "off") + " for this client" + (ModConfig.ShowMap.Value ? " — it shows while the server's grant is current." : ".")); } private static string F(float v, string fmt) { return v.ToString(fmt, CultureInfo.InvariantCulture); } private static string Age(float since) { return F(Time.realtimeSinceStartup - since, "0.#") + "s"; } private static void Say(ConsoleEventArgs args, string text) { if ((Object)(object)args?.Context != (Object)null) { args.Context.AddString(text); } else { RavenEye.Log.LogInfo((object)text); } } } [HarmonyPatch(typeof(ZNet), "GetOtherPublicPlayers")] public static class Patch_ZNet_GetOtherPublicPlayers { private static int _warned; private static void Postfix(ZNet __instance, List<PlayerInfo> playerList) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) try { if (playerList != null && !((Object)(object)__instance == (Object)null)) { if (!RavenEyeTick.EvaluateGrant(__instance)) { RosterSync.LastAppended = 0; } else { RosterSync.LastAppended = RosterMerge.Append(playerList, RosterSync.Current(__instance), __instance.LocalPlayerCharacterID); } } } catch (Exception ex) { if (_warned++ < 3) { RavenEye.Log.LogWarning((object)("GetOtherPublicPlayers postfix threw (" + ex.GetType().Name + ": " + ex.Message + "); vanilla's list stands.")); } } } } } namespace RavenIron.RavenEye.Net { public struct RosterEntry { public string Name; public ZDOID Id; public Vector3 Position; public bool Dead; } public struct RosterHeader { public string ServerVersion; public bool Granted; public float GraceSeconds; } public static class RosterPacket { public const int FormatVersion = 1; public const int MaxEntries = 64; public static void Write(ZPackage pkg, RosterHeader header, IList<RosterEntry> entries) { //IL_00a6: 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) if (pkg == null) { throw new ArgumentNullException("pkg"); } int num = (header.Granted ? (entries?.Count ?? 0) : 0); if (num > 64) { throw new ArgumentException($"roster has {num} entries; the wire format allows {64}"); } pkg.Write(1); pkg.Write(header.ServerVersion ?? ""); pkg.Write(header.Granted); pkg.Write(header.GraceSeconds); pkg.Write(num); for (int i = 0; i < num; i++) { RosterEntry rosterEntry = entries[i]; pkg.Write(rosterEntry.Name ?? ""); pkg.Write(rosterEntry.Id); pkg.Write(rosterEntry.Position); pkg.Write(rosterEntry.Dead); } } public static RosterHeader Read(ZPackage pkg, List<RosterEntry> into) { //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0118: Unknown result type (might be due to invalid IL or missing references) //IL_011d: Unknown result type (might be due to invalid IL or missing references) //IL_0135: Unknown result type (might be due to invalid IL or missing references) //IL_0148: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) if (pkg == null) { throw new ArgumentNullException("pkg"); } if (into == null) { throw new ArgumentNullException("into"); } int num = pkg.ReadInt(); if (num != 1) { string arg = ((num > 1) ? "this client" : "the server"); throw new FormatException($"roster format v{num}; this build reads v{1} — update RavenEye on {arg}"); } RosterHeader result = new RosterHeader { ServerVersion = pkg.ReadString(), Granted = pkg.ReadBool(), GraceSeconds = pkg.ReadSingle() }; if (!IsFinite(result.GraceSeconds)) { throw new FormatException("roster grace is not a finite number"); } int num2 = pkg.ReadInt(); if (num2 < 0 || num2 > 64) { throw new FormatException($"roster count {num2} is outside 0..{64}"); } if (!result.Granted && num2 != 0) { throw new FormatException("a revoke carries no entries"); } List<RosterEntry> list = new List<RosterEntry>(num2); for (int i = 0; i < num2; i++) { RosterEntry item = new RosterEntry { Name = pkg.ReadString(), Id = pkg.ReadZDOID(), Position = pkg.ReadVector3(), Dead = pkg.ReadBool() }; if (!IsFinite(item.Position.x) || !IsFinite(item.Position.y) || !IsFinite(item.Position.z)) { throw new FormatException($"roster entry {i} has a non-finite position"); } list.Add(item); } into.Clear(); into.AddRange(list); return result; } public static bool IsFinite(float f) { if (!float.IsNaN(f)) { return !float.IsInfinity(f); } return false; } } public static class RosterSync { public const string RpcName = "com.raveniron.raveneye.roster"; private static readonly List<RosterEntry> _cache = new List<RosterEntry>(16); private static readonly List<RosterEntry> _scratch = new List<RosterEntry>(16); private static ZRpc _registeredOn; private const long HostKey = long.MinValue; private static readonly List<RosterEntry> _roster = new List<RosterEntry>(16); private static readonly List<RosterEntry> _wire = new List<RosterEntry>(16); private static readonly List<ZNetPeer> _peerScratch = new List<ZNetPeer>(16); private static readonly List<ZNetPeer> _demoteScratch = new List<ZNetPeer>(4); private static readonly HashSet<long> _grantedPeers = new HashSet<long>(); private static readonly List<long> _uidScratch = new List<long>(16); private static readonly List<long> _presentScratch = new List<long>(16); private static readonly DeathLedger _ledger = new DeathLedger(); private static bool _emptyAdminListWarned; private static bool _overflowLogged; private static int _packetFailuresLogged; public static float LastReceipt { get; private set; } = -1f; public static float LastRevoke { get; private set; } = -1f; public static float LastGrace { get; private set; } public static string ServerVersion { get; private set; } = ""; public static int PacketsReceived { get; private set; } public static int RevokesReceived { get; private set; } public static int PacketsRejected { get; private set; } public static string LastRejectReason { get; private set; } = ""; public static int LastAppended { get; set; } public static float LastSendTime { get; private set; } = -1f; public static int LastSendAdmins { get; private set; } public static int LastRosterSize { get; private set; } public static int DeadSnapshots { get; private set; } public static int LastDropped { get; private set; } public static int GrantedPeerCount => _grantedPeers.Count; public static string ClosedGate { get; private set; } = ""; public static bool AdminListUnreadable { get; private set; } public static void ResetSession() { _cache.Clear(); _scratch.Clear(); _registeredOn = null; _roster.Clear(); _wire.Clear(); _peerScratch.Clear(); _demoteScratch.Clear(); _grantedPeers.Clear(); _uidScratch.Clear(); _presentScratch.Clear(); _ledger.Clear(); _emptyAdminListWarned = false; _overflowLogged = false; _packetFailuresLogged = 0; LastReceipt = -1f; LastRevoke = -1f; LastGrace = 0f; ServerVersion = ""; PacketsReceived = 0; RevokesReceived = 0; PacketsRejected = 0; LastRejectReason = ""; LastAppended = 0; LastSendTime = -1f; LastSendAdmins = 0; LastRosterSize = 0; DeadSnapshots = 0; LastDropped = 0; ClosedGate = ""; AdminListUnreadable = false; } public static IList<RosterEntry> Current(ZNet znet) { if ((Object)(object)znet != (Object)null && znet.IsServer()) { return _roster; } return _cache; } public static void EnsureRegistered(ZNet znet) { if ((Object)(object)znet == (Object)null || znet.IsServer()) { return; } ZRpc serverRPC; try { serverRPC = znet.GetServerRPC(); } catch { return; } if (serverRPC == null || serverRPC == _registeredOn) { return; } try { serverRPC.Register<ZPackage>("com.raveniron.raveneye.roster", (Action<ZRpc, ZPackage>)RPC_Roster); _registeredOn = serverRPC; } catch (Exception ex) { RavenEye.Log.LogWarning((object)("RosterSync: register on the server socket failed: " + ex.Message)); _registeredOn = serverRPC; } } public static bool IsRegisteredOnCurrentConnection(ZNet znet) { if ((Object)(object)znet == (Object)null || znet.IsServer()) { return false; } try { return _registeredOn != null && _registeredOn == znet.GetServerRPC(); } catch { return false; } } private static void RPC_Roster(ZRpc rpc, ZPackage pkg) { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || instance.IsServer()) { return; } try { RosterHeader rosterHeader = RosterPacket.Read(pkg, _scratch); ServerVersion = rosterHeader.ServerVersion ?? ""; if (rosterHeader.Granted) { _cache.Clear(); _cache.AddRange(_scratch); LastGrace = rosterHeader.GraceSeconds; LastReceipt = Time.realtimeSinceStartup; PacketsReceived++; } else { _cache.Clear(); LastReceipt = -1f; LastRevoke = Time.realtimeSinceStartup; RevokesReceived++; } } catch (Exception ex) { PacketsRejected++; string text = ex.GetType().Name + ": " + ex.Message; if (PacketsRejected <= 3 || text != LastRejectReason) { RavenEye.Log.LogWarning((object)("RosterSync: rejected a roster packet (" + text + ").")); } LastRejectReason = text; } } public static bool RevealActive(bool revealWhenMapEnabled) { if (revealWhenMapEnabled) { return true; } ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance != (Object)null) { return instance.GetGlobalKey((GlobalKeys)31); } return false; } public static void BuildRoster(ZNet znet, List<RosterEntry> into, float now, float deathGraceSeconds) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) into.Clear(); _presentScratch.Clear(); if (!znet.IsDedicated()) { ZDOID localPlayerCharacterID = znet.LocalPlayerCharacterID; Observe(long.MinValue, localPlayerCharacterID, HostPlayerName(), znet.GetReferencePosition(), into, now); } List<ZNetPeer> peers = znet.GetPeers(); if (peers != null) { for (int i = 0; i < peers.Count; i++) { ZNetPeer val = peers[i]; if (val != null && val.IsReady()) { Observe(val.m_uid, val.m_characterID, val.m_playerName ?? "", val.m_refPos, into, now); } } } _ledger.Retain(_presentScratch); DeadSnapshots = _ledger.Emit(into, now, deathGraceSeconds); LastRosterSize = into.Count; } private static void Observe(long key, ZDOID id, string name, Vector3 refPos, List<RosterEntry> into, float now) { //IL_003e: 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_0046: 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) //IL_0048: 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) _presentScratch.Add(key); if (((ZDOID)(ref id)).IsNone()) { _ledger.Observe(key, hasCharacter: false, default(RosterEntry), now); return; } RosterEntry rosterEntry = new RosterEntry { Name = name, Id = id, Position = PositionOf(id, refPos), Dead = false }; into.Add(rosterEntry); _ledger.Observe(key, hasCharacter: true, rosterEntry, now); } private static Vector3 PositionOf(ZDOID id, Vector3 fallback) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_001a: 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) try { ZDOMan instance = ZDOMan.instance; if (instance != null) { ZDO zDO = instance.GetZDO(id); if (zDO != null) { return zDO.GetPosition(); } } } catch { } return fallback; } private static string HostPlayerName() { try { Game instance = Game.instance; object obj; if (instance == null) { obj = null; } else { PlayerProfile playerProfile = instance.GetPlayerProfile(); obj = ((playerProfile != null) ? playerProfile.GetName() : null); } if (obj == null) { obj = ""; } return (string)obj; } catch { return ""; } } public static int SendToAdmins(ZNet znet, bool enabled, float graceSeconds, float deathGraceSeconds, bool revealWhenMapEnabled, bool verbose) { float now = (LastSendTime = Time.realtimeSinceStartup); List<ZNetPeer> peers = znet.GetPeers(); _peerScratch.Clear(); if (peers != null) { _peerScratch.AddRange(peers); } string text = ((!enabled) ? "Server.Enabled is off" : ((!RevealActive(revealWhenMapEnabled)) ? "the world has a map and RevealWhenMapEnabled is off" : "")); if (text.Length > 0) { if (ClosedGate != text) { RavenEye.Log.LogInfo((object)("roster: gate closed — " + text + ".")); } ClosedGate = text; _roster.Clear(); _ledger.Clear(); LastRosterSize = 0; DeadSnapshots = 0; RevokeEveryone(graceSeconds, "gate closed"); LastSendAdmins = 0; return 0; } if (ClosedGate.Length > 0) { RavenEye.Log.LogInfo((object)"roster: gate open again."); } ClosedGate = ""; BuildRoster(znet, _roster, now, deathGraceSeconds); ZPackage val = null; bool flag = false; int num = 0; _uidScratch.Clear(); _demoteScratch.Clear(); for (int i = 0; i < _peerScratch.Count; i++) { ZNetPeer val2 = _peerScratch[i]; if (val2 == null || !val2.IsReady()) { continue; } _uidScratch.Add(val2.m_uid); if (!AdminGate.IsAdmin(znet, val2)) { if (_grantedPeers.Contains(val2.m_uid)) { _demoteScratch.Add(val2); } continue; } if (val == null && !flag) { try { val = BuildGrantPacket(graceSeconds); } catch (Exception ex) { flag = true; if (_packetFailuresLogged++ < 3) { RavenEye.Log.LogWarning((object)("roster: could not build the packet (" + ex.GetType().Name + ": " + ex.Message + "); no grant this cadence.")); } } } if (val != null) { try { val2.m_rpc.Invoke("com.raveniron.raveneye.roster", new object[1] { val }); num++; } catch (Exception ex2) { RavenEye.Log.LogWarning((object)("roster: send to " + Describe(val2) + " failed: " + ex2.Message)); continue; } if (_grantedPeers.Add(val2.m_uid)) { RavenEye.Log.LogInfo((object)("roster: " + Describe(val2) + " is an admin — map granted.")); } } } int num2 = AdminGate.AdminListCount(znet); AdminListUnreadable = num2 <= 0 && _uidScratch.Count > 0; WarnIfAdminListEmpty(num2, _uidScratch.Count); if (!AdminListUnreadable) { for (int j = 0; j < _demoteScratch.Count; j++) { ZNetPeer val3 = _demoteScratch[j]; _grantedPeers.Remove(val3.m_uid); SendRevoke(val3, graceSeconds); RavenEye.Log.LogInfo((object)("roster: " + Describe(val3) + " is no longer an admin — map revoked.")); } } if (_grantedPeers.Count > 0) { _grantedPeers.RemoveWhere((long uid) => !_uidScratch.Contains(uid)); } LastSendAdmins = num; if (verbose) { RavenEye.Log.LogInfo((object)string.Format("roster: {0} entr{1} ({2} dead) sent to {3} admin(s).", _roster.Count, (_roster.Count == 1) ? "y" : "ies", DeadSnapshots, num)); } return num; } private static ZPackage BuildGrantPacket(float graceSeconds) { //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Expected O, but got Unknown //IL_00c9: Expected O, but got Unknown _wire.Clear(); int num = Math.Min(_roster.Count, 64); for (int i = 0; i < num; i++) { _wire.Add(_roster[i]); } LastDropped = _roster.Count - num; if (LastDropped > 0 && !_overflowLogged) { _overflowLogged = true; RavenEye.Log.LogWarning((object)$"roster: {_roster.Count} entries exceed the wire cap of {64}; the first {num} are sent (deaths dropped first). Logged once."); } ZPackage val = new ZPackage(); RosterPacket.Write(val, new RosterHeader { ServerVersion = "0.2.2", Granted = true, GraceSeconds = graceSeconds }, _wire); return val; } private static void RevokeEveryone(float graceSeconds, string why) { if (_grantedPeers.Count == 0) { return; } for (int i = 0; i < _peerScratch.Count; i++) { ZNetPeer val = _peerScratch[i]; if (val != null && val.IsReady() && _grantedPeers.Contains(val.m_uid)) { SendRevoke(val, graceSeconds); RavenEye.Log.LogInfo((object)("roster: " + Describe(val) + " — map revoked (" + why + ").")); } } _grantedPeers.Clear(); } private static void SendRevoke(ZNetPeer peer, float graceSeconds) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown try { ZPackage val = new ZPackage(); RosterPacket.Write(val, new RosterHeader { ServerVersion = "0.2.2", Granted = false, GraceSeconds = graceSeconds }, null); peer.m_rpc.Invoke("com.raveniron.raveneye.roster", new object[1] { val }); } catch (Exception ex) { RavenEye.Log.LogWarning((object)("roster: revoke to " + Describe(peer) + " failed: " + ex.Message)); } } private static void WarnIfAdminListEmpty(int admins, int peerCount) { if (admins <= 0 && peerCount > 0) { if (!_emptyAdminListWarned) { _emptyAdminListWarned = true; RavenEye.Log.LogWarning((object)($"roster: adminlist.txt reads as EMPTY while {peerCount} player(s) are online — no grants, and no " + "revokes either (this is what a failed read looks like, not a de-admin). If the file is not actually empty, the game failed to read it (open in an editor?) and will only retry when its timestamp changes: re-save the file. Granted admins keep their map for the grace window.")); } } else if (admins > 0 && _emptyAdminListWarned) { _emptyAdminListWarned = false; RavenEye.Log.LogInfo((object)string.Format("roster: adminlist.txt reads {0} entr{1} again.", admins, (admins == 1) ? "y" : "ies")); } } private static string Describe(ZNetPeer peer) { string text; try { ISocket socket = peer.m_socket; text = ((socket != null) ? socket.GetHostName() : null) ?? "?"; } catch { text = "?"; } return peer.m_playerName + " (" + text + ")"; } } } namespace RavenIron.RavenEye.Core { public sealed class DeathLedger { private struct Death { public RosterEntry Entry; public float DiedAt; } private readonly Dictionary<long, RosterEntry> _live = new Dictionary<long, RosterEntry>(16); private readonly Dictionary<long, Death> _dead = new Dictionary<long, Death>(16); private readonly List<long> _scratch = new List<long>(16); public int LiveCount => _live.Count; public int DeadCount => _dead.Count; public void Observe(long key, bool hasCharacter, RosterEntry live, float now) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) RosterEntry value; bool flag = _live.TryGetValue(key, out value); if (hasCharacter) { if (flag && value.Id != live.Id) { _dead[key] = new Death { Entry = value, DiedAt = now }; } live.Dead = false; _live[key] = live; } else if (flag) { _dead[key] = new Death { Entry = value, DiedAt = now }; _live.Remove(key); } } public int Emit(List<RosterEntry> into, float now, float graceSeconds) { if (graceSeconds <= 0f) { _dead.Clear(); return 0; } int num = 0; _scratch.Clear(); foreach (KeyValuePair<long, Death> item in _dead) { if (now - item.Value.DiedAt > graceSeconds) { _scratch.Add(item.Key); continue; } RosterEntry entry = item.Value.Entry; entry.Dead = true; into?.Add(entry); num++; } for (int i = 0; i < _scratch.Count; i++) { _dead.Remove(_scratch[i]); } return num; } public void Retain(List<long> present) { Prune(_live, present); Prune(_dead, present); } public void Clear() { _live.Clear(); _dead.Clear(); } private void Prune<T>(Dictionary<long, T> store, List<long> present) { if (store.Count == 0) { return; } _scratch.Clear(); foreach (long key in store.Keys) { if (present == null || !present.Contains(key)) { _scratch.Add(key); } } for (int i = 0; i < _scratch.Count; i++) { store.Remove(_scratch[i]); } } } public static class Grant { public enum Reason { Granted, GrantedAsAuthority, ClientOff, NoRenderer, NotConnected, Revoked, NeverReceived, Stale } public const float MinGraceSeconds = 10f; public const float MaxGraceSeconds = 120f; public const float Never = -1f; public static float EffectiveGrace(float graceSeconds) { if (float.IsNaN(graceSeconds) || graceSeconds <= 0f) { return 10f; } if (graceSeconds < 10f) { return 10f; } if (graceSeconds > 120f) { return 120f; } return graceSeconds; } public static bool IsFresh(float now, float lastReceipt, float graceSeconds) { if (lastReceipt < 0f) { return false; } float num = now - lastReceipt; if (num < 0f) { return false; } return num <= EffectiveGrace(graceSeconds); } public static Reason Explain(bool showMap, bool hasRenderer, bool isAuthority, bool connected, float now, float lastReceipt, float lastRevoke, float graceSeconds) { if (!showMap) { return Reason.ClientOff; } if (!hasRenderer) { return Reason.NoRenderer; } if (isAuthority) { return Reason.GrantedAsAuthority; } if (!connected) { return Reason.NotConnected; } if (IsFresh(now, lastReceipt, graceSeconds)) { return Reason.Granted; } if (lastReceipt < 0f) { if (!(lastRevoke >= 0f)) { return Reason.NeverReceived; } return Reason.Revoked; } return Reason.Stale; } public static bool IsGranted(Reason r) { if (r != Reason.Granted) { return r == Reason.GrantedAsAuthority; } return true; } public static bool Evaluate(bool showMap, bool hasRenderer, bool isAuthority, bool connected, float now, float lastReceipt, float graceSeconds) { return IsGranted(Explain(showMap, hasRenderer, isAuthority, connected, now, lastReceipt, -1f, graceSeconds)); } public static bool DesiredNoMap(bool granted, bool worldNoMap, bool personalOptOut) { if (!granted) { return worldNoMap || personalOptOut; } return personalOptOut; } public static bool CleanNoMap(bool vanillaClean, bool granted) { if (vanillaClean) { return !granted; } return false; } } public sealed class GrantTracker { public bool Granted { get; private set; } public bool Update(bool granted) { if (granted == Granted) { return false; } Granted = granted; return true; } public void Reset() { Granted = false; } } public static class PatchInstall { public static List<string> Each(IEnumerable<Type> classes, Action<Type> install, Action<Type, Exception> onFailure) { List<string> list = new List<string>(); foreach (Type @class in classes) { try { install(@class); } catch (Exception arg) { list.Add(@class.Name); try { onFailure?.Invoke(@class, arg); } catch { } } } return list; } } public class RavenEyeTick : MonoBehaviour { private const float ConvergeEvery = 0.5f; private readonly GrantTracker _grant = new GrantTracker(); private float _sendAccumulator; private float _convergeAccumulator; private bool _hadWorld; private bool _roleLogged; private int _reapplies; private int _sendFailuresLogged; private int _convergeFailuresLogged; public static bool GrantedNow { get; private set; } public static int Reapplies { get; private set; } public static string Role() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { return "no world"; } if (instance.IsDedicated()) { return "dedicated server"; } if (instance.IsServer()) { return "listen host"; } return "client"; } public static bool IsAuthorityGranting(ZNet znet) { if ((Object)(object)znet != (Object)null && znet.IsServer() && ModConfig.ServerEnabled.Value) { return RosterSync.RevealActive(ModConfig.RevealWhenMapEnabled.Value); } return false; } public static Grant.Reason ExplainGrant(ZNet znet) { if ((Object)(object)znet == (Object)null) { return Grant.Reason.NotConnected; } bool isAuthority = IsAuthorityGranting(znet); bool connected = znet.IsServer() || znet.GetServerPeer() != null; return Grant.Explain(ModConfig.ShowMap.Value, RavenEye.HasRenderer, isAuthority, connected, Time.realtimeSinceStartup, RosterSync.LastReceipt, RosterSync.LastRevoke, RosterSync.LastGrace); } public static bool EvaluateGrant(ZNet znet) { return Grant.IsGranted(ExplainGrant(znet)); } public static bool PersonalOptOut(Player local) { if ((Object)(object)local == (Object)null) { return false; } return PlatformPrefs.GetFloat(PersonalKey(local), 1f) == 0f; } public static string PersonalKey(Player local) { return "mapenabled_" + local.GetPlayerName(); } private void Update() { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { if (_hadWorld) { _hadWorld = false; _roleLogged = false; _sendAccumulator = 0f; _convergeAccumulator = 0f; _grant.Reset(); GrantedNow = false; RosterSync.ResetSession(); } return; } _hadWorld = true; if (!_roleLogged) { _roleLogged = true; string arg = (instance.IsServer() ? $"admin list entries={AdminGate.AdminListCount(instance)}" : "admin list is the server's job"); RavenEye.Log.LogInfo((object)($"RavenEye online — role={Role()}, renderer={RavenEye.HasRenderer}, " + "server feature=" + (ModConfig.ServerEnabled.Value ? "on" : "off") + ", " + $"interval={ModConfig.IntervalSeconds.Value:0.##}s, grace={ModConfig.GraceSeconds.Value:0}s, " + $"deathGrace={ModConfig.DeathGraceSeconds.Value:0}s, " + $"revealWhenMapEnabled={ModConfig.RevealWhenMapEnabled.Value}, " + $"client showMap={ModConfig.ShowMap.Value}, {arg}.")); } if (instance.IsServer()) { ServerTick(instance, Time.deltaTime); } else { RosterSync.EnsureRegistered(instance); } if (RavenEye.HasRenderer) { ClientTick(instance, Time.deltaTime); } } private void ServerTick(ZNet znet, float dt) { _sendAccumulator += dt; float num = Mathf.Max(0.5f, ModConfig.IntervalSeconds.Value); if (_sendAccumulator < num) { return; } _sendAccumulator = 0f; try { RosterSync.SendToAdmins(znet, ModConfig.ServerEnabled.Value, ModConfig.GraceSeconds.Value, ModConfig.DeathGraceSeconds.Value, ModConfig.RevealWhenMapEnabled.Value, ModConfig.VerboseLogging.Value); } catch (Exception ex) { if (_sendFailuresLogged++ < 3) { RavenEye.Log.LogWarning((object)("roster send failed (" + ex.GetType().Name + ": " + ex.Message + "); silence revokes within the grace window.")); } } } private void ClientTick(ZNet znet, float dt) { //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Invalid comparison between Unknown and I4 //IL_0102: Unknown result type (might be due to invalid IL or missing references) Grant.Reason r = ExplainGrant(znet); bool flag = (GrantedNow = Grant.IsGranted(r)); bool num = _grant.Update(flag); if (num) { RavenEye.Log.LogInfo((object)(flag ? ("admin map GRANTED — " + Words(r) + ".") : ("admin map not granted — " + Words(r) + "."))); } _convergeAccumulator += dt; if (!num && _convergeAccumulator < 0.5f) { return; } _convergeAccumulator = 0f; Player localPlayer = Player.m_localPlayer; Minimap instance = Minimap.instance; if ((Object)(object)localPlayer == (Object)null || (Object)(object)instance == (Object)null) { return; } try { bool worldNoMap = (Object)(object)ZoneSystem.instance != (Object)null && ZoneSystem.instance.GetGlobalKey((GlobalKeys)31); bool flag3 = Grant.DesiredNoMap(flag, worldNoMap, PersonalOptOut(localPlayer)); if (Game.m_noMap == flag3 && (!flag3 || (int)instance.m_mode == 0)) { return; } Game.m_noMap = flag3; if (!flag3) { if ((int)instance.m_mode == 0) { instance.SetMapMode((MapMode)1); } } else { instance.SetMapMode((MapMode)0); } Reapplies = ++_reapplies; if (_reapplies <= 5 || _reapplies % 100 == 0) { RavenEye.Log.LogInfo((object)$"map flag corrected (noMap {!flag3} → {flag3}, {Words(r)}); corrections so far: {_reapplies}."); } } catch (Exception ex) { if (_convergeFailuresLogged++ < 3) { RavenEye.Log.LogWarning((object)("map convergence threw (" + ex.GetType().Name + ": " + ex.Message + "); will retry.")); } } } public static string Words(Grant.Reason r) { return r switch { Grant.Reason.Granted => $"server roster {Time.realtimeSinceStartup - RosterSync.LastReceipt:0.#}s ago, grace {Grant.EffectiveGrace(RosterSync.LastGrace):0}s", Grant.Reason.GrantedAsAuthority => "this is the host and the server gates are open", Grant.Reason.ClientOff => "Client.ShowMap is off (`raveneye map on`)", Grant.Reason.NoRenderer => "no renderer (headless)", Grant.Reason.NotConnected => "not connected to a server", Grant.Reason.Revoked => $"the server revoked it {Time.realtimeSinceStartup - RosterSync.LastRevoke:0}s ago (removed from adminlist.txt, or the server's gate closed)", Grant.Reason.NeverReceived => "no roster has arrived since this join (not on the server's adminlist.txt, or the server is not running RavenEye)", Grant.Reason.Stale => $"stale — last roster {Time.realtimeSinceStartup - RosterSync.LastReceipt:0}s ago, past the {Grant.EffectiveGrace(RosterSync.LastGrace):0}s grace (server stalled or stopped)", _ => r.ToString(), }; } } public static class RosterMerge { public const string DeadSuffix = " (dead)"; public static int Append(List<PlayerInfo> target, IList<RosterEntry> roster, ZDOID self) { //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_0037: 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_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) if (target == null || roster == null) { return 0; } int num = 0; for (int i = 0; i < roster.Count; i++) { RosterEntry rosterEntry = roster[i]; if (!((ZDOID)(ref rosterEntry.Id)).IsNone() && !(rosterEntry.Id == self) && !ContainsCharacter(target, rosterEntry.Id)) { target.Add(new PlayerInfo { m_name = rosterEntry.Name + (rosterEntry.Dead ? " (dead)" : ""), m_characterID = rosterEntry.Id, m_position = rosterEntry.Position, m_publicPosition = true }); num++; } } return num; } private static bool ContainsCharacter(List<PlayerInfo> list, ZDOID id) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < list.Count; i++) { if (list[i].m_characterID == id) { return true; } } return false; } } } namespace RavenIron.RavenEye.Config { public static class ModConfig { public static ConfigEntry<bool> ServerEnabled; public static ConfigEntry<float> IntervalSeconds; public static ConfigEntry<float> GraceSeconds; public static ConfigEntry<float> DeathGraceSeconds; public static ConfigEntry<bool> RevealWhenMapEnabled; public static ConfigEntry<bool> VerboseLogging; public static ConfigEntry<bool> ShowMap; public static void Bind(ConfigFile cfg) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Expected O, but got Unknown //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Expected O, but got Unknown //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Expected O, but got Unknown //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Expected O, but got Unknown //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Expected O, but got Unknown //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Expected O, but got Unknown //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Expected O, but got Unknown ServerEnabled = cfg.Bind<bool>("Server", "Enabled", true, new ConfigDescription("Send admins the position of every online player. Read on the SERVER (dedicated or listen host); a client's copy of this value does nothing.", (AcceptableValueBase)null, Array.Empty<object>())); IntervalSeconds = cfg.Bind<float>("Server", "IntervalSeconds", 2f, new ConfigDescription("Seconds between roster sends. Vanilla refreshes player positions every 2 s, so a smaller value only costs bandwidth; the ceiling stays under the shortest grace window so a live server can never be mistaken for a silent one. Read on the SERVER.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.5f, 5f), Array.Empty<object>())); GraceSeconds = cfg.Bind<float>("Server", "GraceSeconds", 60f, new ConfigDescription("If the server goes SILENT (crash, stall, network), an admin's map survives this many seconds after the last roster before it relocks. A demotion does not wait for this: it is sent explicitly and relocks within one interval. Clients clamp what they receive to 10..120. Read on the SERVER.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 120f), Array.Empty<object>())); DeathGraceSeconds = cfg.Bind<float>("Server", "DeathGraceSeconds", 180f, new ConfigDescription("After a player dies, keep their last known position on the admins' map, marked (dead), for this many seconds — so an admin can answer 'where did I die?' on a world with no map. 0 disables. Read on the SERVER.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 600f), Array.Empty<object>())); RevealWhenMapEnabled = cfg.Bind<bool>("Server", "RevealWhenMapEnabled", false, new ConfigDescription("On a world WITH a map, also show admins the players who turned position sharing off. Off by default: out of the box RavenEye is a no-map tool and does nothing on a world that has a map, where players' share-position choice stands. Read on the SERVER.", (AcceptableValueBase)null, Array.Empty<object>())); VerboseLogging = cfg.Bind<bool>("Server", "VerboseLogging", false, new ConfigDescription("Log every roster send. Grants and revocations are logged regardless. Read on the SERVER.", (AcceptableValueBase)null, Array.Empty<object>())); ShowMap = cfg.Bind<bool>("Client", "ShowMap", true, new ConfigDescription("Use the map when the server grants it to this client. Set false to keep the vanilla no-map experience anyway (an admin playing honestly). `raveneye map on|off` toggles this at runtime. Read on the CLIENT.", (AcceptableValueBase)null, Array.Empty<object>())); } } }