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 LagWatch v0.3.7
LagWatch.dll
Decompiled 15 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] [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 LagWatch { [BepInPlugin("captain.LagWatch", "Lag Watch", "0.3.7")] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "captain.LagWatch"; public const string Version = "0.3.7"; internal const string Protocol = "2"; internal static ManualLogSource Log; internal static ConfigEntry<float> SampleIntervalSec; internal static ConfigEntry<float> ZdoScanIntervalSec; internal static ConfigEntry<bool> LogSamples; internal static ConfigEntry<bool> AutoWarnEnabled; internal static ConfigEntry<float> WarnCooldownSec; internal static ConfigEntry<float> PingWarnMs; internal static ConfigEntry<float> PingBadMs; internal static ConfigEntry<float> ZdoWarn; internal static ConfigEntry<float> ZdoBad; internal static ConfigEntry<float> FpsWarn; internal static ConfigEntry<float> FpsBad; internal static ConfigEntry<float> QueueWarnKB; internal static ConfigEntry<float> QueueBadKB; internal static ConfigEntry<float> TrafficWarnKBs; internal static ConfigEntry<float> TrafficBadKBs; internal static ConfigEntry<float> ServerCpuWarn; internal static ConfigEntry<float> ServerCpuBad; internal static ConfigEntry<float> ServerFrameWarnMs; internal static ConfigEntry<float> ServerFrameBadMs; internal static ConfigEntry<bool> AllowServerClear; internal static ConfigEntry<int> ClearBatchPerFrame; internal static ConfigEntry<bool> DiagEnabled; internal static ConfigEntry<float> DiagReportIntervalMin; internal static ConfigEntry<bool> DiagIncludeWarnings; internal static ConfigEntry<float> DiagHitchMs; internal static ConfigEntry<int> DiagMaxDistinctErrors; internal static ConfigEntry<int> DiagMaxLogsPerSec; internal static ConfigEntry<int> DiagRetainDays; internal static ConfigEntry<float> DiagMaxFileMB; internal static ConfigEntry<KeyboardShortcut> PanelHotkey; internal static ConfigEntry<bool> ReportEnabled; internal static ConfigEntry<float> ReportIntervalSec; private Harmony _harmony; private void Awake() { //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Expected O, but got Unknown //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Expected O, but got Unknown //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Expected O, but got Unknown //IL_02b3: Unknown result type (might be due to invalid IL or missing references) //IL_02bd: Expected O, but got Unknown //IL_0310: Unknown result type (might be due to invalid IL or missing references) //IL_031a: Expected O, but got Unknown //IL_036d: Unknown result type (might be due to invalid IL or missing references) //IL_0377: Expected O, but got Unknown //IL_03a7: Unknown result type (might be due to invalid IL or missing references) //IL_03b1: Expected O, but got Unknown //IL_03e1: Unknown result type (might be due to invalid IL or missing references) //IL_03eb: Expected O, but got Unknown //IL_0413: Unknown result type (might be due to invalid IL or missing references) //IL_041d: Expected O, but got Unknown //IL_0450: Unknown result type (might be due to invalid IL or missing references) //IL_045a: Expected O, but got Unknown //IL_0509: Unknown result type (might be due to invalid IL or missing references) //IL_056b: Unknown result type (might be due to invalid IL or missing references) //IL_0575: Expected O, but got Unknown //IL_0580: Unknown result type (might be due to invalid IL or missing references) //IL_058a: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; SampleIntervalSec = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "SampleIntervalSec", 5f, new ConfigDescription("Seconds between per-peer ping/traffic samples.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 60f), Array.Empty<object>())); ZdoScanIntervalSec = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "ZdoScanIntervalSec", 30f, new ConfigDescription("Seconds between full ZDO ownership scans (costly on large worlds).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 600f), Array.Empty<object>())); LogSamples = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "LogSamples", false, "Write every sample to the server log (for verification)."); AutoWarnEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Server", "AutoWarnEnabled", false, "Automatically send the notice to a player whose status stays red. Admins can also toggle this in the panel."); WarnCooldownSec = ((BaseUnityPlugin)this).Config.Bind<float>("Server", "WarnCooldownSec", 120f, new ConfigDescription("Minimum seconds between notices to the same player (manual and automatic).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(10f, 3600f), Array.Empty<object>())); PingWarnMs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "PingWarnMs", 150f, "Ping (ms) at which a player turns orange."); PingBadMs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "PingBadMs", 300f, "Ping (ms) at which a player turns red."); ZdoWarn = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "OwnedObjectsWarn", 3000f, "Owned ZDO count for orange."); ZdoBad = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "OwnedObjectsBad", 8000f, "Owned ZDO count for red."); FpsWarn = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "FpsWarn", 30f, "Client average FPS at or below this is orange (client-reported)."); FpsBad = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "FpsBad", 15f, "Client average FPS at or below this is red (client-reported)."); QueueWarnKB = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "SendQueueWarnKB", 50f, "Server send queue toward the player (KB) for orange."); QueueBadKB = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "SendQueueBadKB", 200f, "Server send queue toward the player (KB) for red."); TrafficWarnKBs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "TrafficWarnKBs", 150f, "In+out traffic (KB/s) for orange."); TrafficBadKBs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "TrafficBadKBs", 400f, "In+out traffic (KB/s) for red."); AllowServerClear = ((BaseUnityPlugin)this).Config.Bind<bool>("Maintenance", "AllowServerClear", true, "Allow admins to run Server Clear from the panel (deletes ground items, hostile monsters and temporary debris after a preview and confirmation). Set to false to disable it completely."); ClearBatchPerFrame = ((BaseUnityPlugin)this).Config.Bind<int>("Maintenance", "ClearBatchPerFrame", 100, new ConfigDescription("Objects deleted per frame during Server Clear. Lower values spread the work out and avoid server stalls.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(10, 2000), Array.Empty<object>())); DiagEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "Enabled", false, "Record server load factors and frequent error messages to BepInEx/LagWatch/diag-YYYY-MM-DD.jsonl for later analysis. Off = no listener and no per-frame work. Can be toggled while running."); DiagReportIntervalMin = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "ReportIntervalMin", 10f, new ConfigDescription("Minutes between written records (one line per record).", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 120f), Array.Empty<object>())); DiagIncludeWarnings = ((BaseUnityPlugin)this).Config.Bind<bool>("Diagnostics", "IncludeWarnings", false, "Also count warnings, not only errors. Applied when diagnostics start."); DiagHitchMs = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "HitchMs", 100f, new ConfigDescription("A single frame at or above this many ms counts as a hitch.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(30f, 2000f), Array.Empty<object>())); DiagMaxDistinctErrors = ((BaseUnityPlugin)this).Config.Bind<int>("Diagnostics", "MaxDistinctErrors", 300, new ConfigDescription("Cap on distinct error messages tracked (memory limit). Applied when diagnostics start.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(50, 2000), Array.Empty<object>())); DiagMaxLogsPerSec = ((BaseUnityPlugin)this).Config.Bind<int>("Diagnostics", "MaxLogsPerSec", 500, new ConfigDescription("Log events processed per second; the rest are only counted as dropped (protects the server during error storms). Applied when diagnostics start.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(50, 10000), Array.Empty<object>())); DiagRetainDays = ((BaseUnityPlugin)this).Config.Bind<int>("Diagnostics", "RetainDays", 7, new ConfigDescription("Delete record files older than this many days.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(1, 90), Array.Empty<object>())); DiagMaxFileMB = ((BaseUnityPlugin)this).Config.Bind<float>("Diagnostics", "MaxFileMB", 5f, new ConfigDescription("Stop writing to today's file after it reaches this size.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 100f), Array.Empty<object>())); ServerCpuWarn = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "ServerCpuWarn", 85f, "Server process CPU for Fair (yellow). 100 = one full core, so values above 100 mean several cores are busy."); ServerCpuBad = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "ServerCpuBad", 130f, "Server process CPU for Struggling (red)."); ServerFrameWarnMs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "ServerFrameWarnMs", 50f, "Average server frame time (ms) for Fair (yellow)."); ServerFrameBadMs = ((BaseUnityPlugin)this).Config.Bind<float>("Thresholds", "ServerFrameBadMs", 100f, "Average server frame time (ms) for Struggling (red)."); PanelHotkey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Client", "PanelHotkey", new KeyboardShortcut((KeyCode)291, Array.Empty<KeyCode>()), "Opens the lag panel (admins only)."); ReportEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Client", "ReportEnabled", true, "Send your FPS and installed mod list to the server so admins can diagnose lag."); ReportIntervalSec = ((BaseUnityPlugin)this).Config.Bind<float>("Client", "ReportIntervalSec", 5f, new ConfigDescription("Seconds between client reports.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(2f, 60f), Array.Empty<object>())); _harmony = new Harmony("captain.LagWatch"); _harmony.PatchAll(); ((Component)this).gameObject.AddComponent<PeerSampler>(); ((Component)this).gameObject.AddComponent<ServerHealth>(); ((Component)this).gameObject.AddComponent<ServerMaintenance>(); ((Component)this).gameObject.AddComponent<ClientReporter>(); ((Component)this).gameObject.AddComponent<LagPanel>(); } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal class ClientReporter : MonoBehaviour { private const float SliceSec = 0.5f; private const float WarmupSec = 10f; private const int ModsEveryNReports = 12; private float _inGameSince = -1f; private float _winTime; private float _winFrames; private float _sliceTime; private float _sliceFrames; private float _minSliceFps = float.MaxValue; private const int MaxUnacked = 3; private int _reportCount; private int _unacked; private int _session = -1; private string _modsCache; private int _modCount; private void Update() { if (!Plugin.ReportEnabled.Value || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsDedicated()) { return; } if ((Object)(object)Player.m_localPlayer == (Object)null) { _inGameSince = -1f; ResetWindow(); return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (_inGameSince < 0f) { _inGameSince = realtimeSinceStartup; } if (!(realtimeSinceStartup - _inGameSince < 10f)) { float unscaledDeltaTime = Time.unscaledDeltaTime; _winTime += unscaledDeltaTime; _winFrames += 1f; _sliceTime += unscaledDeltaTime; _sliceFrames += 1f; if (_sliceTime >= 0.5f) { _minSliceFps = Mathf.Min(_minSliceFps, _sliceFrames / _sliceTime); _sliceTime = 0f; _sliceFrames = 0f; } if (_winTime >= Plugin.ReportIntervalSec.Value) { Send(); } } } private void Send() { if (_session != LagWatchRpc.Session) { _session = LagWatchRpc.Session; _unacked = 0; _reportCount = 0; } if (!LagWatchRpc.ServerAcked) { if (_unacked >= 3) { ResetWindow(); return; } _unacked++; } float num = _winFrames / _winTime; float minFps = ((_minSliceFps == float.MaxValue) ? num : _minSliceFps); string mods = ""; if (_reportCount % 12 == 0) { mods = GetMods(); } _reportCount++; LagWatchRpc.SendReport(num, minFps, _modCount, mods); ResetWindow(); } private void ResetWindow() { _winTime = (_winFrames = (_sliceTime = (_sliceFrames = 0f))); _minSliceFps = float.MaxValue; } private string GetMods() { if (_modsCache != null) { return _modsCache; } List<string> list = new List<string>(); foreach (PluginInfo value in Chainloader.PluginInfos.Values) { list.Add(value.Metadata.Name + " " + value.Metadata.Version); } list.Sort(); _modCount = list.Count; _modsCache = string.Join("|", list); return _modsCache; } } internal class NoticeData { public byte PingLv; public byte ZdoLv; public byte FpsLv; public byte QueueLv; public byte TrafficLv; public int Ping; public int Zdo; public int QueueKB; public int TrafficKBs; public float Fps; } internal static class NoticeText { internal static List<string> Build(NoticeData n) { List<string> list = new List<string>(); if (n.PingLv > 0) { list.Add(Strings.Fmt("hintPing", n.Ping)); } if (n.ZdoLv > 0) { list.Add(Strings.Fmt("hintZdo", n.Zdo)); } if (n.FpsLv > 0) { list.Add(Strings.Fmt("hintFps", n.Fps.ToString("F0"))); } if (n.QueueLv > 0) { list.Add(Strings.Fmt("hintQueue", n.QueueKB)); } if (n.TrafficLv > 0) { list.Add(Strings.Fmt("hintTraffic", n.TrafficKBs)); } list.Insert(0, Strings.Get((list.Count > 0) ? "noticeHead" : "noticeGeneric")); return list; } internal static void Show(NoticeData n) { List<string> list = Build(n); foreach (string item in list) { Plugin.Log.LogInfo((object)("[LagWatch] notice: " + item)); } MessageHud instance = MessageHud.instance; if (!((Object)(object)instance == (Object)null)) { instance.ShowMessage((MessageType)2, list[0], 0, (Sprite)null, false, true); for (int i = 1; i < list.Count; i++) { instance.ShowMessage((MessageType)1, list[i], 0, (Sprite)null, false, true); } } } } internal static class Strings { private static readonly Dictionary<string, string[]> T = new Dictionary<string, string[]> { ["title"] = new string[2] { "서버 랙 모니터", "Server Lag Monitor" }, ["colPlayer"] = new string[2] { "접속자", "Player" }, ["colPing"] = new string[2] { "핑", "Ping" }, ["colFps"] = new string[2] { "FPS", "FPS" }, ["colObjects"] = new string[2] { "소유 오브젝트", "Owned obj." }, ["colQueue"] = new string[2] { "송신 대기", "Send queue" }, ["colTraffic"] = new string[2] { "트래픽", "Traffic" }, ["colMods"] = new string[2] { "모드", "Mods" }, ["colStatus"] = new string[2] { "상태", "Status" }, ["stOk"] = new string[2] { "정상", "OK" }, ["stWarn"] = new string[2] { "주의", "Caution" }, ["stBad"] = new string[2] { "위험", "Critical" }, ["summary"] = new string[2] { "정상 {0} 주의 {1} 위험 {2}", "OK {0} Caution {1} Critical {2}" }, ["btnSend"] = new string[2] { "안내 보내기", "Send notice" }, ["btnSending"] = new string[2] { "전송 중", "Sending" }, ["btnSent"] = new string[2] { "전송됨", "Sent" }, ["btnCooldown"] = new string[2] { "잠시 후", "Wait" }, ["btnNoMod"] = new string[2] { "모드 없음", "No mod" }, ["autoOn"] = new string[2] { "자동 경고: 켜짐", "Auto warn: ON" }, ["autoOff"] = new string[2] { "자동 경고: 꺼짐", "Auto warn: OFF" }, ["close"] = new string[2] { "닫기", "Close" }, ["hint"] = new string[2] { "ESC 또는 단축키로 닫기. 행을 클릭하면 상세 정보가 표시됩니다.", "Press ESC or the hotkey to close. Click a row for details." }, ["empty"] = new string[2] { "접속자 정보를 기다리는 중입니다. 서버에 LagWatch 모드가 설치되어 있어야 합니다.", "Waiting for data. The server must have LagWatch installed." }, ["adminOnly"] = new string[2] { "LagWatch 패널은 관리자 전용입니다.", "The LagWatch panel is admin only." }, ["detailEmpty"] = new string[2] { "접속자를 선택하세요.", "Select a player." }, ["detail"] = new string[2] { "{0} - {1}\n핑 평균 {2}ms / 최대 {3}ms 소유 오브젝트 {4}개 송신 대기 {5}KB 트래픽 {6}KB/s\nFPS 평균 {7} / 최저 {8} 설치 모드 {9}개\n상위 오브젝트: {10}\n모드: {11}", "{0} - {1}\nPing avg {2}ms / max {3}ms Owned objects {4} Send queue {5}KB Traffic {6}KB/s\nFPS avg {7} / min {8} Mods {9}\nTop objects: {10}\nMods: {11}" }, ["notReported"] = new string[2] { "미보고(클라이언트에 모드 없음)", "not reported (client lacks the mod)" }, ["modsMore"] = new string[2] { " 외 {0}개", " +{0} more" }, ["srvGood"] = new string[2] { "좋음", "Good" }, ["srvFair"] = new string[2] { "보통", "Fair" }, ["srvBad"] = new string[2] { "힘듦", "Struggling" }, ["srvState"] = new string[2] { "서버 상태", "Server" }, ["srvPing"] = new string[2] { "내 핑", "My ping" }, ["srvFrame"] = new string[2] { "프레임 평균/최대", "Frame avg/max" }, ["srvHitch"] = new string[2] { "프레임 평균/최대 (끊김 {0})", "Frame avg/max (hitches {0})" }, ["srvCpu"] = new string[2] { "CPU (코어1=100)", "CPU (1 core=100)" }, ["srvMem"] = new string[2] { "메모리", "Memory" }, ["srvPlayers"] = new string[2] { "접속자", "Players" }, ["srvObjects"] = new string[2] { "전체 오브젝트", "All objects" }, ["srvTraffic"] = new string[2] { "트래픽 송/수", "Traffic out/in" }, ["srvUptime"] = new string[2] { "가동 시간", "Uptime" }, ["srvHost"] = new string[2] { "호스트", "host" }, ["btnClear"] = new string[2] { "서버 클리어", "Server clear" }, ["btnMemory"] = new string[2] { "메모리 정리", "Free memory" }, ["dlgOk"] = new string[2] { "확인", "OK" }, ["dlgRun"] = new string[2] { "실행", "Run" }, ["dlgCancel"] = new string[2] { "취소", "Cancel" }, ["dlgWorking"] = new string[2] { "서버에 요청하는 중입니다...", "Asking the server..." }, ["clearNotice"] = new string[2] { "안정적인 서버 운영을 위해 필드의 아이템 {0}개, 몬스터 {1}마리를 지웁니다.", "For stable server operation, {0} items and {1} monsters in the field will be cleared." }, ["clearPreview"] = new string[2] { "안정적인 서버 운영을 위해 필드의 아이템 {0}개, 몬스터 {1}마리를 지웁니다.\n(임시 이펙트/잔해 {2}개 포함. 플레이어가 지은 건축물, 상자, 무덤, 길들인 동물은 지우지 않습니다. 되돌릴 수 없습니다.)\n\n실행할까요?", "For stable server operation, {0} items and {1} monsters in the field will be cleared.\n(Includes {2} temporary effects/debris. Player-built pieces, containers, tombstones and tamed animals are never removed. This cannot be undone.)\n\nRun it?" }, ["clearNone"] = new string[2] { "지울 대상이 없습니다.", "Nothing to clear." }, ["clearDone"] = new string[2] { "서버 클리어 완료: 아이템 {0}개, 몬스터 {1}마리, 잔해 {2}개를 지웠습니다.", "Server clear finished: removed {0} items, {1} monsters and {2} debris objects." }, ["clearExpired"] = new string[2] { "확인 시간이 지났습니다. 다시 시도해 주세요.", "The confirmation expired. Please try again." }, ["clearBusy"] = new string[2] { "방금 실행했거나 다른 정리가 진행 중입니다. 잠시 후 다시 시도해 주세요.", "A cleanup just ran or is still running. Please try again shortly." }, ["clearDisabled"] = new string[2] { "서버 컨피그(Maintenance.AllowServerClear)에서 서버 클리어가 꺼져 있습니다.", "Server clear is turned off in the server config (Maintenance.AllowServerClear)." }, ["memConfirm"] = new string[2] { "서버 메모리를 정리합니다.\n- LagWatch 에러 기록 비우기\n- 사용하지 않는 에셋 해제\n- GC(가비지 컬렉션) 실행\n\n서버가 1~2초 멈출 수 있습니다. 실행할까요?", "This frees server memory.\n- Clear LagWatch's error records\n- Unload unused assets\n- Run garbage collection\n\nThe server may stall for a second or two. Run it?" }, ["memDone"] = new string[2] { "메모리 정리 완료 ({4}초)\n프로세스 메모리: {0}MB -> {1}MB\n관리 힙(GC): {2}MB -> {3}MB\n\n※ 프로세스 메모리는 OS로 바로 반환되지 않아 줄지 않을 수 있습니다.", "Memory cleanup finished ({4}s)\nProcess memory: {0}MB -> {1}MB\nManaged heap (GC): {2}MB -> {3}MB\n\nNote: process memory is not always returned to the OS right away, so it may not shrink." }, ["noticeHead"] = new string[2] { "[서버 관리자] 현재 접속 상태가 서버 랙을 유발하고 있습니다. 게임을 종료하고 아래 내용을 조치한 뒤 다시 접속해 주세요.", "[Server admin] Your connection is causing lag on the server. Please quit the game, fix the items below, and rejoin." }, ["noticeGeneric"] = new string[2] { "[서버 관리자] 서버 안정성을 위해 안내드립니다. 게임을 종료한 뒤 PC/네트워크 상태를 확인하고 다시 접속해 주세요.", "[Server admin] For server stability, please quit the game, check your PC/network, and rejoin." }, ["hintPing"] = new string[2] { "- 핑 {0}ms: 네트워크가 불안정합니다. 다운로드/스트리밍/VPN을 끄고 가능하면 유선으로 연결하세요.", "- Ping {0}ms: your network is unstable. Stop downloads/streaming/VPN and use a wired connection if possible." }, ["hintZdo"] = new string[2] { "- 소유 오브젝트 {0}개: 주변의 몬스터/드랍/건축물이 너무 많습니다. 정리한 뒤 접속하세요.", "- {0} owned objects: too many monsters/drops/pieces near you. Clean them up before rejoining." }, ["hintFps"] = new string[2] { "- FPS {0}: PC 성능이 낮아 서버와 동기화가 지연됩니다. 그래픽 옵션과 설치한 모드 수를 줄이세요.", "- FPS {0}: low performance delays sync with the server. Lower graphics settings and reduce installed mods." }, ["hintQueue"] = new string[2] { "- 송신 대기 {0}KB: 서버가 보낸 데이터를 제때 받지 못하고 있습니다. 회선 속도와 방화벽/보안 프로그램을 확인하세요.", "- Send queue {0}KB: you are not receiving server data in time. Check your line speed and firewall/security software." }, ["hintTraffic"] = new string[2] { "- 트래픽 {0}KB/s: 통신량이 많습니다. 대역폭을 쓰는 다른 프로그램을 종료하세요.", "- Traffic {0}KB/s: heavy network usage. Close other programs using bandwidth." } }; internal static bool Korean { get { try { return Localization.instance != null && Localization.instance.GetSelectedLanguage() == "Korean"; } catch { return false; } } } internal static string Get(string key) { string[] value; return T.TryGetValue(key, out value) ? value[(!Korean) ? 1u : 0u] : key; } internal static string Fmt(string key, params object[] args) { return string.Format(Get(key), args); } } internal class ErrorCollector : ILogListener, IDisposable { internal class Entry { public string Source; public string Level; public string Message; public int IntervalCount; public long TotalCount; } private const string OverflowKey = "(other: distinct limit reached)"; private const int MaxLineLen = 160; private readonly object _lock = new object(); private readonly Dictionary<string, Entry> _entries = new Dictionary<string, Entry>(); private readonly bool _includeWarnings; private readonly int _maxDistinct; private readonly int _maxPerSec; private long _bucketSec; private int _bucketCount; private long _dropped; private long _intervalTotal; internal ErrorCollector(bool includeWarnings, int maxDistinct, int maxPerSec) { _includeWarnings = includeWarnings; _maxDistinct = maxDistinct; _maxPerSec = maxPerSec; } public void LogEvent(object sender, LogEventArgs e) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Invalid comparison between Unknown and I4 //IL_001a: 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) //IL_001e: Invalid comparison between Unknown and I4 LogLevel level = e.Level; bool flag = (level & 3) > 0; if (!flag && (!_includeWarnings || (level & 4) == 0)) { return; } long num = Stopwatch.GetTimestamp() / Stopwatch.Frequency; lock (_lock) { if (num != _bucketSec) { _bucketSec = num; _bucketCount = 0; } if (++_bucketCount > _maxPerSec) { _dropped++; _intervalTotal++; return; } } ILogSource source = e.Source; string text = ((source != null) ? source.SourceName : null) ?? "?"; string text2 = text + "|" + Normalize(e.Data?.ToString() ?? ""); lock (_lock) { _intervalTotal++; if (!_entries.TryGetValue(text2, out var value)) { if (_entries.Count >= _maxDistinct) { text2 = "(other: distinct limit reached)"; if (!_entries.TryGetValue(text2, out value)) { value = new Entry { Source = "-", Level = "-", Message = "(other: distinct limit reached)" }; _entries[text2] = value; } } else { value = new Entry { Source = text, Level = (flag ? "Error" : "Warning"), Message = text2.Substring(text.Length + 1) }; _entries[text2] = value; } } value.IntervalCount++; value.TotalCount++; } } internal void Clear() { lock (_lock) { _entries.Clear(); _intervalTotal = 0L; _dropped = 0L; } } internal List<Entry> TakeIntervalTop(int n, out long intervalTotal, out long dropped) { List<Entry> list = new List<Entry>(); lock (_lock) { foreach (Entry value in _entries.Values) { if (value.IntervalCount > 0) { list.Add(new Entry { Source = value.Source, Level = value.Level, Message = value.Message, IntervalCount = value.IntervalCount, TotalCount = value.TotalCount }); } } foreach (Entry value2 in _entries.Values) { value2.IntervalCount = 0; } intervalTotal = _intervalTotal; dropped = _dropped; _intervalTotal = 0L; _dropped = 0L; } list.Sort((Entry a, Entry b) => b.IntervalCount.CompareTo(a.IntervalCount)); if (list.Count > n) { list.RemoveRange(n, list.Count - n); } return list; } private static string Normalize(string raw) { StringBuilder stringBuilder = new StringBuilder(324); int num = 0; int num2 = 0; while (num2 < raw.Length && num < 2) { int num3 = raw.IndexOf('\n', num2); if (num3 < 0) { num3 = raw.Length; } int num4 = num3 - num2; string text = raw.Substring(num2, (num4 > 160) ? 160 : num4).Trim(); num2 = num3 + 1; if (text.Length != 0 && !text.StartsWith("Stack trace")) { if (num > 0) { stringBuilder.Append(" | "); } AppendMasked(stringBuilder, text); num++; } } return stringBuilder.ToString(); } private static void AppendMasked(StringBuilder sb, string line) { bool flag = false; foreach (char c in line) { if (c >= '0' && c <= '9') { if (!flag) { sb.Append('#'); } flag = true; } else { flag = false; sb.Append(c); } } } public void Dispose() { } } internal class ServerHealth : MonoBehaviour { private const int TopErrors = 10; private const int TopPrefabs = 10; private static ServerHealth _instance; private ErrorCollector _errors; private float _nextReport; private float _startedAt; private int _frames; private int _hitches; private float _sumDt; private float _maxDt; private bool _sizeWarned; private int _lastCleanupDay = -1; internal static bool Active { get; private set; } private static string Dir => Path.Combine(Paths.BepInExRootPath, "LagWatch"); internal static void ClearErrorTable() { _instance?._errors?.Clear(); } private void Awake() { _instance = this; } private void Update() { bool flag = Plugin.DiagEnabled.Value && (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer(); if (flag != Active) { if (flag) { StartMonitoring(); } else { StopMonitoring(); } } if (Active) { float unscaledDeltaTime = Time.unscaledDeltaTime; _frames++; _sumDt += unscaledDeltaTime; if (unscaledDeltaTime > _maxDt) { _maxDt = unscaledDeltaTime; } if (unscaledDeltaTime * 1000f >= Plugin.DiagHitchMs.Value) { _hitches++; } if (Time.realtimeSinceStartup >= _nextReport) { Report(); } } } private void OnDestroy() { StopMonitoring(); } private void StartMonitoring() { _errors = new ErrorCollector(Plugin.DiagIncludeWarnings.Value, Plugin.DiagMaxDistinctErrors.Value, Plugin.DiagMaxLogsPerSec.Value); Logger.Listeners.Add((ILogListener)(object)_errors); Active = true; _startedAt = Time.realtimeSinceStartup; _nextReport = _startedAt + Plugin.DiagReportIntervalMin.Value * 60f; ResetFrameStats(); Plugin.Log.LogInfo((object)$"[LagWatch] diagnostics ON (every {Plugin.DiagReportIntervalMin.Value} min, dir={Dir})"); } private void StopMonitoring() { if (Active) { Active = false; if (_errors != null) { Logger.Listeners.Remove((ILogListener)(object)_errors); } _errors = null; Plugin.Log.LogInfo((object)"[LagWatch] diagnostics OFF"); } } private void ResetFrameStats() { _frames = (_hitches = 0); _sumDt = (_maxDt = 0f); } private void Report() { _nextReport = Time.realtimeSinceStartup + Plugin.DiagReportIntervalMin.Value * 60f; try { long intervalTotal; long dropped; List<ErrorCollector.Entry> list = _errors.TakeIntervalTop(10, out intervalTotal, out dropped); string json = BuildJson(list, intervalTotal, dropped); WriteLine(json); float num = ((_frames > 0) ? (_sumDt / (float)_frames * 1000f) : 0f); Plugin.Log.LogInfo((object)$"[LagWatch] diag: frame avg {num:F1}ms max {_maxDt * 1000f:F0}ms hitches {_hitches}, peers {PeerSampler.Stats.Count}, errors {intervalTotal} (distinct top {list.Count}), zdo {ZdoOwnership.TotalZdos}"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] diag report failed (ignored): " + ex.Message)); } ResetFrameStats(); } private string BuildJson(List<ErrorCollector.Entry> top, long errTotal, long dropped) { StringBuilder stringBuilder = new StringBuilder(2048); float num = ((_frames > 0) ? (_sumDt / (float)_frames * 1000f) : 0f); stringBuilder.Append("{\"t\":\"").Append(DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss")).Append('"'); stringBuilder.Append(",\"uptimeMin\":").Append(((Time.realtimeSinceStartup - _startedAt) / 60f).ToString("F0")); stringBuilder.Append(",\"peers\":").Append(PeerSampler.Stats.Count); stringBuilder.Append(",\"frame\":{\"count\":").Append(_frames).Append(",\"avgMs\":") .Append(num.ToString("F1")) .Append(",\"maxMs\":") .Append((_maxDt * 1000f).ToString("F0")) .Append(",\"hitches\":") .Append(_hitches) .Append('}'); stringBuilder.Append(",\"gcMB\":").Append(((float)GC.GetTotalMemory(forceFullCollection: false) / 1048576f).ToString("F0")); stringBuilder.Append(",\"scene\":").Append(((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.m_instances.Count : 0); stringBuilder.Append(",\"zdo\":{\"total\":").Append(ZdoOwnership.TotalZdos).Append(",\"scanMs\":") .Append(ZdoOwnership.LastScanMs) .Append(",\"top\":["); AppendTopPrefabs(stringBuilder); stringBuilder.Append("]}"); stringBuilder.Append(",\"errors\":{\"total\":").Append(errTotal).Append(",\"dropped\":") .Append(dropped) .Append(",\"top\":["); for (int i = 0; i < top.Count; i++) { ErrorCollector.Entry entry = top[i]; if (i > 0) { stringBuilder.Append(','); } stringBuilder.Append("{\"n\":").Append(entry.IntervalCount).Append(",\"total\":") .Append(entry.TotalCount) .Append(",\"lvl\":") .Append(Q(entry.Level)) .Append(",\"src\":") .Append(Q(entry.Source)) .Append(",\"msg\":") .Append(Q(entry.Message)) .Append('}'); } stringBuilder.Append("]}"); stringBuilder.Append(",\"players\":["); bool flag = true; foreach (PeerStat value in PeerSampler.Stats.Values) { if (!flag) { stringBuilder.Append(','); } flag = false; stringBuilder.Append("{\"name\":").Append(Q(value.Name)).Append(",\"lv\":") .Append(value.Level) .Append(",\"ping\":") .Append(value.PingAvg) .Append(",\"zdo\":") .Append(value.OwnedZdos) .Append(",\"queueKB\":") .Append(value.SendQueue / 1024) .Append('}'); } stringBuilder.Append("]}"); return stringBuilder.ToString(); } private static void AppendTopPrefabs(StringBuilder sb) { List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>(ZdoOwnership.GlobalPrefabs); list.Sort((KeyValuePair<int, int> a, KeyValuePair<int, int> b) => b.Value.CompareTo(a.Value)); for (int num = 0; num < list.Count && num < 10; num++) { GameObject val = (((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.GetPrefab(list[num].Key) : null); string s = (((Object)(object)val != (Object)null) ? ((Object)val).name : list[num].Key.ToString()); if (num > 0) { sb.Append(','); } sb.Append('[').Append(Q(s)).Append(',') .Append(list[num].Value) .Append(']'); } } private void WriteLine(string json) { Directory.CreateDirectory(Dir); CleanupOldFiles(); string text = Path.Combine(Dir, "diag-" + DateTime.Now.ToString("yyyy-MM-dd") + ".jsonl"); long num = (long)(Plugin.DiagMaxFileMB.Value * 1048576f); if (File.Exists(text) && new FileInfo(text).Length > num) { if (!_sizeWarned) { _sizeWarned = true; Plugin.Log.LogWarning((object)$"[LagWatch] diag file reached {Plugin.DiagMaxFileMB.Value}MB; skipping writes for today."); } } else { _sizeWarned = false; File.AppendAllText(text, json + "\n", new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } } private void CleanupOldFiles() { int dayOfYear = DateTime.Now.DayOfYear; if (dayOfYear == _lastCleanupDay) { return; } _lastCleanupDay = dayOfYear; DateTime dateTime = DateTime.Now.AddDays(-Plugin.DiagRetainDays.Value); string[] files = Directory.GetFiles(Dir, "diag-*.jsonl"); foreach (string path in files) { if (File.GetLastWriteTime(path) < dateTime) { File.Delete(path); } } } private static string Q(string s) { if (s == null) { return "\"\""; } StringBuilder stringBuilder = new StringBuilder(s.Length + 2); stringBuilder.Append('"'); foreach (char c in s) { switch (c) { case '"': stringBuilder.Append("\\\""); continue; case '\\': stringBuilder.Append("\\\\"); continue; case '\n': stringBuilder.Append("\\n"); continue; case '\t': stringBuilder.Append(' '); continue; case '\r': continue; } if (c >= ' ') { stringBuilder.Append(c); } } return stringBuilder.Append('"').ToString(); } } internal static class AdminActionRpc { private const string RpcClearPreview = "LagWatch.ClearPreview"; private const string RpcClearPreviewReply = "LagWatch.ClearPreviewReply"; private const string RpcClearRun = "LagWatch.ClearRun"; private const string RpcClearResult = "LagWatch.ClearResult"; private const string RpcClearNotice = "LagWatch.ClearNotice"; private const string RpcMemRun = "LagWatch.MemRun"; private const string RpcMemResult = "LagWatch.MemResult"; private static bool IsServer => (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer(); internal static void Register() { ZRoutedRpc instance = ZRoutedRpc.instance; instance.Register<ZPackage>("LagWatch.ClearPreview", (Action<long, ZPackage>)OnClearPreview); instance.Register<ZPackage>("LagWatch.ClearPreviewReply", (Action<long, ZPackage>)OnClearPreviewReply); instance.Register<ZPackage>("LagWatch.ClearRun", (Action<long, ZPackage>)OnClearRun); instance.Register<ZPackage>("LagWatch.ClearResult", (Action<long, ZPackage>)OnClearResult); instance.Register<ZPackage>("LagWatch.ClearNotice", (Action<long, ZPackage>)OnClearNotice); instance.Register<ZPackage>("LagWatch.MemRun", (Action<long, ZPackage>)OnMemRun); instance.Register<ZPackage>("LagWatch.MemResult", (Action<long, ZPackage>)OnMemResult); } internal static void RequestClearPreview() { LagWatchRpc.ToServer("LagWatch.ClearPreview", LagWatchRpc.NewPackage()); } internal static void RequestClearRun(string token) { ZPackage val = LagWatchRpc.NewPackage(); val.Write(token ?? ""); LagWatchRpc.ToServer("LagWatch.ClearRun", val); } internal static void RequestMemoryCleanup() { LagWatchRpc.ToServer("LagWatch.MemRun", LagWatchRpc.NewPackage()); } private static void OnClearPreview(long sender, ZPackage p) { if (!IsServer) { return; } try { if (LagWatchRpc.VersionOk(p) && AdminGate.IsAdmin(sender)) { string token; ClearCounts clearCounts = ServerMaintenance.Instance.Preview(sender, out token); ZPackage val = LagWatchRpc.NewPackage(); val.Write(Plugin.AllowServerClear.Value); val.Write(token); val.Write(clearCounts.Items); val.Write(clearCounts.Monsters); val.Write(clearCounts.Debris); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.ClearPreviewReply", new object[1] { val }); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] clear preview failed (ignored): " + ex.Message)); } } private static void OnClearRun(long sender, ZPackage p) { if (!IsServer) { return; } try { if (!LagWatchRpc.VersionOk(p)) { return; } string token = p.ReadString(); if (AdminGate.IsAdmin(sender)) { ClearCounts counts; int num = ServerMaintenance.Instance.TryRun(sender, token, out counts); if (num != 0) { SendClearResult(sender, num, 0, 0, 0); return; } ZPackage val = LagWatchRpc.NewPackage(); val.Write(counts.Items); val.Write(counts.Monsters); ZRoutedRpc.instance.InvokeRoutedRPC(0L, "LagWatch.ClearNotice", new object[1] { val }); Plugin.Log.LogInfo((object)$"[LagWatch] server clear started by admin peer {sender}: items={counts.Items} monsters={counts.Monsters} debris={counts.Debris}"); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] clear run failed (ignored): " + ex.Message)); } } private static void OnMemRun(long sender, ZPackage p) { if (!IsServer) { return; } try { if (LagWatchRpc.VersionOk(p) && AdminGate.IsAdmin(sender)) { int num = ServerMaintenance.Instance.TryCleanMemory(sender); if (num != 0) { SendMemResult(sender, num, 0f, 0f, 0f, 0f, 0f); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] memory cleanup failed (ignored): " + ex.Message)); } } internal static void SendClearResult(long admin, int code, int items, int monsters, int debris) { ZPackage val = LagWatchRpc.NewPackage(); val.Write(code); val.Write(items); val.Write(monsters); val.Write(debris); ZRoutedRpc.instance.InvokeRoutedRPC(admin, "LagWatch.ClearResult", new object[1] { val }); } internal static void SendMemResult(long admin, int code, float wsBefore, float wsAfter, float heapBefore, float heapAfter, float seconds) { ZPackage val = LagWatchRpc.NewPackage(); val.Write(code); val.Write(wsBefore); val.Write(wsAfter); val.Write(heapBefore); val.Write(heapAfter); val.Write(seconds); ZRoutedRpc.instance.InvokeRoutedRPC(admin, "LagWatch.MemResult", new object[1] { val }); } private static void OnClearPreviewReply(long sender, ZPackage p) { try { if (LagWatchRpc.VersionOk(p)) { bool allowed = p.ReadBool(); string token = p.ReadString(); int items = p.ReadInt(); int monsters = p.ReadInt(); int debris = p.ReadInt(); LagPanel.Instance?.OnClearPreview(allowed, token, items, monsters, debris); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] preview reply parse failed (ignored): " + ex.Message)); } } private static void OnClearResult(long sender, ZPackage p) { try { if (LagWatchRpc.VersionOk(p)) { int code = p.ReadInt(); int items = p.ReadInt(); int monsters = p.ReadInt(); int debris = p.ReadInt(); LagPanel.Instance?.OnClearResult(code, items, monsters, debris); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] clear result parse failed (ignored): " + ex.Message)); } } private static void OnMemResult(long sender, ZPackage p) { try { if (LagWatchRpc.VersionOk(p)) { int code = p.ReadInt(); float wsBefore = p.ReadSingle(); float wsAfter = p.ReadSingle(); float heapBefore = p.ReadSingle(); float heapAfter = p.ReadSingle(); float seconds = p.ReadSingle(); LagPanel.Instance?.OnMemResult(code, wsBefore, wsAfter, heapBefore, heapAfter, seconds); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] memory result parse failed (ignored): " + ex.Message)); } } private static void OnClearNotice(long sender, ZPackage p) { try { if (LagWatchRpc.VersionOk(p)) { int num = p.ReadInt(); int num2 = p.ReadInt(); string text = Strings.Fmt("clearNotice", num, num2); Plugin.Log.LogInfo((object)("[LagWatch] notice: " + text)); MessageHud instance = MessageHud.instance; if (instance != null) { instance.ShowMessage((MessageType)2, text, 0, (Sprite)null, false, true); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] clear notice parse failed (ignored): " + ex.Message)); } } } internal static class AdminGate { private const string RequestRpc = "LagWatch.AdminReq"; private const string SyncRpc = "LagWatch.AdminSync"; private static bool _serverSaysAdmin; internal static bool IsLocalAdmin => _serverSaysAdmin || ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.LocalPlayerIsAdminOrHost()); internal static void Register() { _serverSaysAdmin = false; ZRoutedRpc.instance.Register<string>("LagWatch.AdminReq", (Action<long, string>)OnRequest); ZRoutedRpc.instance.Register<bool>("LagWatch.AdminSync", (Action<long, bool>)OnSync); } internal static void Request() { if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), "LagWatch.AdminReq", new object[1] { "" }); } } internal static bool IsAdmin(long sender) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return false; } if (sender == ZRoutedRpc.instance.GetServerPeerID()) { return true; } ZNetPeer peer = ZNet.instance.GetPeer(sender); string text = ((peer != null) ? peer.m_socket.GetHostName() : null); return text != null && ZNet.instance.IsAdmin(text); } private static void OnRequest(long sender, string _) { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } try { ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.AdminSync", new object[1] { IsAdmin(sender) }); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] admin check failed (ignored): " + ex.Message)); } } private static void OnSync(long sender, bool isAdmin) { _serverSaysAdmin = isAdmin; } } [HarmonyPatch(typeof(Player), "OnSpawned")] internal static class Player_OnSpawned_AdminGatePatch { [HarmonyPostfix] private static void Postfix(Player __instance) { if ((Object)(object)__instance == (Object)(object)Player.m_localPlayer) { AdminGate.Request(); } } } [HarmonyPatch] internal static class LagWatchRpc { private const string RpcReport = "LagWatch.Report"; private const string RpcSubscribe = "LagWatch.Subscribe"; private const string RpcSnapshot = "LagWatch.Snapshot"; private const string RpcWarn = "LagWatch.Warn"; private const string RpcWarnAck = "LagWatch.WarnAck"; private const string RpcNotice = "LagWatch.Notice"; private const string RpcSetAuto = "LagWatch.SetAuto"; private const string RpcDetail = "LagWatch.Detail"; private const string RpcDetailReply = "LagWatch.DetailReply"; private const string RpcReportAck = "LagWatch.ReportAck"; internal const int WarnSent = 0; internal const int WarnCooldown = 1; internal const int WarnNoMod = 2; private static readonly HashSet<long> Subscribers = new HashSet<long>(); private static readonly Dictionary<long, float> LastWarn = new Dictionary<long, float>(); private static bool _versionWarned; internal static bool ServerAcked; internal static int Session; private static bool IsServer => (Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer(); internal static bool HasSubscribers => Subscribers.Count > 0; private static void ResetSession() { Subscribers.Clear(); LastWarn.Clear(); ClientReportStore.Reports.Clear(); PeerSampler.Stats.Clear(); ServerAcked = false; Session++; } internal static void ForgetWarn(long uid) { LastWarn.Remove(uid); } [HarmonyPatch(typeof(Game), "Start")] [HarmonyPostfix] private static void RegisterHandlers() { ResetSession(); ZRoutedRpc instance = ZRoutedRpc.instance; instance.Register<ZPackage>("LagWatch.ReportAck", (Action<long, ZPackage>)OnReportAck); instance.Register<ZPackage>("LagWatch.Report", (Action<long, ZPackage>)OnReport); instance.Register<ZPackage>("LagWatch.Subscribe", (Action<long, ZPackage>)OnSubscribe); instance.Register<ZPackage>("LagWatch.Snapshot", (Action<long, ZPackage>)OnSnapshot); instance.Register<ZPackage>("LagWatch.Warn", (Action<long, ZPackage>)OnWarn); instance.Register<ZPackage>("LagWatch.WarnAck", (Action<long, ZPackage>)OnWarnAck); instance.Register<ZPackage>("LagWatch.Notice", (Action<long, ZPackage>)OnNotice); instance.Register<ZPackage>("LagWatch.SetAuto", (Action<long, ZPackage>)OnSetAuto); instance.Register<ZPackage>("LagWatch.Detail", (Action<long, ZPackage>)OnDetail); instance.Register<ZPackage>("LagWatch.DetailReply", (Action<long, ZPackage>)OnDetailReply); AdminGate.Register(); AdminActionRpc.Register(); } internal static ZPackage NewPackage() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write("2"); return val; } internal static void ToServer(string rpc, ZPackage p) { if (ZRoutedRpc.instance != null) { ZRoutedRpc.instance.InvokeRoutedRPC(ZRoutedRpc.instance.GetServerPeerID(), rpc, new object[1] { p }); } } internal static void SendReport(float avgFps, float minFps, int modCount, string mods) { ZPackage val = NewPackage(); val.Write(avgFps); val.Write(minFps); val.Write(modCount); val.Write(mods ?? ""); ToServer("LagWatch.Report", val); } internal static void SendSubscribe(bool on) { ZPackage val = NewPackage(); val.Write(on); ToServer("LagWatch.Subscribe", val); } internal static void SendWarn(long targetUid) { ZPackage val = NewPackage(); val.Write(targetUid); ToServer("LagWatch.Warn", val); } internal static void SendSetAuto(bool on) { ZPackage val = NewPackage(); val.Write(on); ToServer("LagWatch.SetAuto", val); } internal static void SendDetailRequest(long targetUid) { ZPackage val = NewPackage(); val.Write(targetUid); ToServer("LagWatch.Detail", val); } internal static bool VersionOk(ZPackage p) { string text = p.ReadString(); if (text == "2") { return true; } if (!_versionWarned) { _versionWarned = true; Plugin.Log.LogWarning((object)("[LagWatch] protocol mismatch (peer=" + text + ", local=2); ignoring messages. Update LagWatch on both server and client.")); } return false; } private static void OnReport(long sender, ZPackage p) { if (!IsServer) { return; } try { if (VersionOk(p)) { float avgFps = Mathf.Clamp(p.ReadSingle(), 0f, 1000f); float minFps = Mathf.Clamp(p.ReadSingle(), 0f, 1000f); int modCount = Mathf.Clamp(p.ReadInt(), 0, 2000); string text = p.ReadString(); if (!ClientReportStore.Reports.TryGetValue(sender, out var value)) { value = new ClientReport(); ClientReportStore.Reports[sender] = value; ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.ReportAck", new object[1] { NewPackage() }); } value.AvgFps = avgFps; value.MinFps = minFps; value.ModCount = modCount; if (text.Length > 0) { value.Mods = ((text.Length > 16384) ? text.Substring(0, 16384) : text); } value.ReceivedAt = Time.realtimeSinceStartup; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] report parse failed (ignored): " + ex.Message)); } } private static void OnSubscribe(long sender, ZPackage p) { if (!IsServer) { return; } try { if (!VersionOk(p)) { return; } if (!p.ReadBool()) { Subscribers.Remove(sender); } else if (AdminGate.IsAdmin(sender)) { bool flag = Subscribers.Count == 0; Subscribers.Add(sender); if (flag) { ServerMonitor.Sample(Time.realtimeSinceStartup); } SendSnapshot(sender); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] subscribe failed (ignored): " + ex.Message)); } } private static void OnSetAuto(long sender, ZPackage p) { if (!IsServer) { return; } try { if (VersionOk(p)) { bool flag = p.ReadBool(); if (AdminGate.IsAdmin(sender)) { Plugin.AutoWarnEnabled.Value = flag; Plugin.Log.LogInfo((object)$"[LagWatch] AutoWarnEnabled set to {flag} by admin peer {sender}"); PushSnapshots(); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] set-auto failed (ignored): " + ex.Message)); } } private static void OnDetail(long sender, ZPackage p) { if (!IsServer) { return; } try { if (VersionOk(p)) { long num = p.ReadLong(); if (AdminGate.IsAdmin(sender)) { ZPackage val = NewPackage(); val.Write(num); val.Write(ClientReportStore.Reports.TryGetValue(num, out var value) ? value.Mods : ""); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.DetailReply", new object[1] { val }); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] detail failed (ignored): " + ex.Message)); } } private static void OnWarn(long sender, ZPackage p) { if (!IsServer) { return; } try { if (VersionOk(p)) { long num = p.ReadLong(); if (AdminGate.IsAdmin(sender)) { int num2 = TryWarn(num, auto: false); ZPackage val = NewPackage(); val.Write(num); val.Write(num2); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "LagWatch.WarnAck", new object[1] { val }); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] warn failed (ignored): " + ex.Message)); } } internal static int TryWarn(long target, bool auto) { float realtimeSinceStartup = Time.realtimeSinceStartup; ClientReport fresh = ClientReportStore.GetFresh(target, realtimeSinceStartup); if (fresh == null || !PeerSampler.Stats.TryGetValue(target, out var value)) { return 2; } if (LastWarn.TryGetValue(target, out var value2) && realtimeSinceStartup - value2 < Plugin.WarnCooldownSec.Value) { return 1; } LastWarn[target] = realtimeSinceStartup; ZPackage val = NewPackage(); val.Write(value.PingLv); val.Write(value.ZdoLv); val.Write(value.FpsLv); val.Write(value.QueueLv); val.Write(value.TrafficLv); val.Write(value.PingAvg); val.Write(value.OwnedZdos); val.Write(fresh.AvgFps); val.Write(value.SendQueue / 1024); val.Write((int)((value.OutBytesSec + value.InBytesSec) / 1024f)); ZRoutedRpc.instance.InvokeRoutedRPC(target, "LagWatch.Notice", new object[1] { val }); Plugin.Log.LogInfo((object)$"[LagWatch] notice sent to {value.Name}({target}) auto={auto} lv={value.Level}"); return 0; } private static void SendSnapshot(long to) { float realtimeSinceStartup = Time.realtimeSinceStartup; ZPackage val = NewPackage(); val.Write(Plugin.AutoWarnEnabled.Value); ServerMonitor.Current.Write(val); val.Write(PeerSampler.Stats.Count); foreach (PeerStat value in PeerSampler.Stats.Values) { SnapRow.From(value, ClientReportStore.GetFresh(value.Uid, realtimeSinceStartup)).Write(val); } ZRoutedRpc.instance.InvokeRoutedRPC(to, "LagWatch.Snapshot", new object[1] { val }); } internal static void PushSnapshots() { if (Subscribers.Count == 0) { return; } List<long> list = new List<long>(); foreach (long subscriber in Subscribers) { if (subscriber != ZRoutedRpc.instance.GetServerPeerID() && ZNet.instance.GetPeer(subscriber) == null) { list.Add(subscriber); } else { SendSnapshot(subscriber); } } foreach (long item in list) { Subscribers.Remove(item); } } private static void OnReportAck(long sender, ZPackage p) { try { if (VersionOk(p)) { ServerAcked = true; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] report ack parse failed (ignored): " + ex.Message)); } } private static void OnSnapshot(long sender, ZPackage p) { try { if (VersionOk(p)) { bool autoWarn = p.ReadBool(); ServerSnap server = ServerSnap.Read(p); int num = p.ReadInt(); List<SnapRow> list = new List<SnapRow>(num); for (int i = 0; i < num; i++) { list.Add(SnapRow.Read(p)); } LagPanel.Instance?.OnSnapshot(autoWarn, server, list); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] snapshot parse failed (ignored): " + ex.Message)); } } private static void OnWarnAck(long sender, ZPackage p) { try { if (VersionOk(p)) { long uid = p.ReadLong(); int code = p.ReadInt(); LagPanel.Instance?.OnWarnAck(uid, code); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] ack parse failed (ignored): " + ex.Message)); } } private static void OnDetailReply(long sender, ZPackage p) { try { if (VersionOk(p)) { long uid = p.ReadLong(); string mods = p.ReadString(); LagPanel.Instance?.OnDetail(uid, mods); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] detail parse failed (ignored): " + ex.Message)); } } private static void OnNotice(long sender, ZPackage p) { try { if (VersionOk(p)) { NoticeData n = new NoticeData { PingLv = p.ReadByte(), ZdoLv = p.ReadByte(), FpsLv = p.ReadByte(), QueueLv = p.ReadByte(), TrafficLv = p.ReadByte(), Ping = p.ReadInt(), Zdo = p.ReadInt(), Fps = p.ReadSingle(), QueueKB = p.ReadInt(), TrafficKBs = p.ReadInt() }; NoticeText.Show(n); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] notice parse failed (ignored): " + ex.Message)); } } } internal class SnapRow { public long Uid; public string Name; public int PingAvg; public int PingMax; public float OutKBs; public float InKBs; public int QueueBytes; public int Zdo; public string Top; public bool Reported; public float AvgFps; public float MinFps; public int ModCount; public byte PingLv; public byte ZdoLv; public byte FpsLv; public byte QueueLv; public byte TrafficLv; public byte Level; public float TrafficKBs => OutKBs + InKBs; internal static SnapRow From(PeerStat s, ClientReport r) { return new SnapRow { Uid = s.Uid, Name = (s.Name ?? ""), PingAvg = s.PingAvg, PingMax = s.MaxPing, OutKBs = s.OutBytesSec / 1024f, InKBs = s.InBytesSec / 1024f, QueueBytes = s.SendQueue, Zdo = s.OwnedZdos, Top = (s.TopPrefabs ?? ""), Reported = (r != null), AvgFps = (r?.AvgFps ?? 0f), MinFps = (r?.MinFps ?? 0f), ModCount = (r?.ModCount ?? 0), PingLv = s.PingLv, ZdoLv = s.ZdoLv, FpsLv = s.FpsLv, QueueLv = s.QueueLv, TrafficLv = s.TrafficLv, Level = s.Level }; } internal void Write(ZPackage p) { p.Write(Uid); p.Write(Name); p.Write(PingAvg); p.Write(PingMax); p.Write(OutKBs); p.Write(InKBs); p.Write(QueueBytes); p.Write(Zdo); p.Write(Top); p.Write(Reported); p.Write(AvgFps); p.Write(MinFps); p.Write(ModCount); p.Write(PingLv); p.Write(ZdoLv); p.Write(FpsLv); p.Write(QueueLv); p.Write(TrafficLv); p.Write(Level); } internal static SnapRow Read(ZPackage p) { return new SnapRow { Uid = p.ReadLong(), Name = p.ReadString(), PingAvg = p.ReadInt(), PingMax = p.ReadInt(), OutKBs = p.ReadSingle(), InKBs = p.ReadSingle(), QueueBytes = p.ReadInt(), Zdo = p.ReadInt(), Top = p.ReadString(), Reported = p.ReadBool(), AvgFps = p.ReadSingle(), MinFps = p.ReadSingle(), ModCount = p.ReadInt(), PingLv = p.ReadByte(), ZdoLv = p.ReadByte(), FpsLv = p.ReadByte(), QueueLv = p.ReadByte(), TrafficLv = p.ReadByte(), Level = p.ReadByte() }; } } internal class ClientReport { public float AvgFps; public float MinFps; public int ModCount; public string Mods = ""; public float ReceivedAt; } internal static class ClientReportStore { private const float StaleSec = 30f; internal static readonly Dictionary<long, ClientReport> Reports = new Dictionary<long, ClientReport>(); internal static ClientReport GetFresh(long uid, float now) { if (Reports.TryGetValue(uid, out var value) && now - value.ReceivedAt <= 30f) { return value; } return null; } } internal static class LagScore { internal static void Apply(PeerStat s, ClientReport report) { s.PingLv = High(s.PingAvg, Plugin.PingWarnMs.Value, Plugin.PingBadMs.Value); s.ZdoLv = High(s.OwnedZdos, Plugin.ZdoWarn.Value, Plugin.ZdoBad.Value); s.QueueLv = High((float)s.SendQueue / 1024f, Plugin.QueueWarnKB.Value, Plugin.QueueBadKB.Value); s.TrafficLv = High((s.OutBytesSec + s.InBytesSec) / 1024f, Plugin.TrafficWarnKBs.Value, Plugin.TrafficBadKBs.Value); s.FpsLv = (byte)((report != null && report.AvgFps > 0f) ? Low(report.AvgFps, Plugin.FpsWarn.Value, Plugin.FpsBad.Value) : 0); s.Level = (byte)Mathf.Max(new int[5] { s.PingLv, s.ZdoLv, s.QueueLv, s.TrafficLv, s.FpsLv }); } private static byte High(float v, float warn, float bad) { return (byte)((v >= bad) ? 2u : ((v >= warn) ? 1u : 0u)); } private static byte Low(float v, float warn, float bad) { return (byte)((v <= bad) ? 2u : ((v <= warn) ? 1u : 0u)); } } internal class PeerStat { public long Uid; public string Name; public int Ping; public int PingAvg; public int MaxPing; public float OutBytesSec; public float InBytesSec; public int SendQueue; public int OwnedZdos; public string TopPrefabs = ""; public float SampledAt; public byte PingLv; public byte ZdoLv; public byte FpsLv; public byte QueueLv; public byte TrafficLv; public byte Level; } internal class PeerSampler : MonoBehaviour { internal static readonly Dictionary<long, PeerStat> Stats = new Dictionary<long, PeerStat>(); private float _nextSample; private float _nextZdoScan; private void Update() { if ((Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer()) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (LagWatchRpc.HasSubscribers) { ServerMonitor.OnFrame(Time.unscaledDeltaTime); } if (ZdoOwnership.Scanning) { ZdoOwnership.Step(20000); } else if (ZdoScanNeeded() && (!ZdoOwnership.HasData || realtimeSinceStartup >= _nextZdoScan)) { _nextZdoScan = realtimeSinceStartup + Plugin.ZdoScanIntervalSec.Value; ZdoOwnership.BeginScan(); } if (realtimeSinceStartup >= _nextSample) { _nextSample = realtimeSinceStartup + Plugin.SampleIntervalSec.Value; Sample(realtimeSinceStartup); if (LagWatchRpc.HasSubscribers) { ServerMonitor.Sample(realtimeSinceStartup); } LagWatchRpc.PushSnapshots(); } } private static bool ZdoScanNeeded() { return LagWatchRpc.HasSubscribers || ServerHealth.Active || Plugin.AutoWarnEnabled.Value || Plugin.LogSamples.Value; } private static void Sample(float now) { HashSet<long> hashSet = new HashSet<long>(); float num = default(float); float num2 = default(float); int ping = default(int); float outBps = default(float); float inBps = default(float); foreach (ZNetPeer peer in ZNet.instance.GetPeers()) { if (peer.IsReady()) { peer.m_socket.GetConnectionQuality(ref num, ref num2, ref ping, ref outBps, ref inBps); Record(peer.m_uid, peer.m_playerName, ping, outBps, inBps, peer.m_socket.GetSendQueueSize(), now, hashSet, isHost: false); } } if (!ZNet.instance.IsDedicated() && (Object)(object)Player.m_localPlayer != (Object)null && ZRoutedRpc.instance != null) { Record(ZRoutedRpc.instance.GetServerPeerID(), Player.m_localPlayer.GetPlayerName(), 0, 0f, 0f, 0, now, hashSet, isHost: true); } List<long> list = new List<long>(); foreach (long key in Stats.Keys) { if (!hashSet.Contains(key)) { list.Add(key); } } foreach (long item in list) { Stats.Remove(item); ClientReportStore.Reports.Remove(item); LagWatchRpc.ForgetWarn(item); } } private static void Record(long uid, string name, int ping, float outBps, float inBps, int queue, float now, HashSet<long> seen, bool isHost) { PeerStat value; bool flag = !Stats.TryGetValue(uid, out value); if (flag) { value = new PeerStat { Uid = uid }; Stats[uid] = value; } seen.Add(uid); int num = ((!flag) ? value.Level : 0); value.Name = name; value.Ping = ping; value.PingAvg = (flag ? ping : Mathf.RoundToInt((float)value.PingAvg * 0.6f + (float)ping * 0.4f)); value.MaxPing = Mathf.Max(value.MaxPing, ping); value.OutBytesSec = outBps; value.InBytesSec = inBps; value.SendQueue = queue; value.OwnedZdos = (ZdoOwnership.Owners.TryGetValue(uid, out var value2) ? value2.Count : 0); value.TopPrefabs = ZdoOwnership.TopPrefabs(uid, 3); value.SampledAt = now; ClientReport fresh = ClientReportStore.GetFresh(uid, now); LagScore.Apply(value, fresh); if (Plugin.LogSamples.Value) { Plugin.Log.LogInfo((object)string.Format("[LagWatch] {0}({1}) lv={2} ping={3}ms out={4:F0}B/s in={5:F0}B/s queue={6} zdo={7} fps={8} top=[{9}]", value.Name, value.Uid, value.Level, value.PingAvg, value.OutBytesSec, value.InBytesSec, value.SendQueue, value.OwnedZdos, (fresh != null) ? fresh.AvgFps.ToString("F0") : "n/a", value.TopPrefabs)); } if (value.Level != num) { Plugin.Log.LogInfo((object)$"[LagWatch] {value.Name}({value.Uid}) status {num} -> {value.Level} (ping={value.PingAvg}ms zdo={value.OwnedZdos} queue={value.SendQueue / 1024}KB)"); } if (value.Level == 2 && Plugin.AutoWarnEnabled.Value && !isHost) { LagWatchRpc.TryWarn(uid, auto: true); } } } internal enum PrefabKind : byte { None, Built, Item, Monster, Debris } internal static class PrefabClassifier { private static readonly Dictionary<int, PrefabKind> Cache = new Dictionary<int, PrefabKind>(); internal static PrefabKind Classify(int prefabHash) { if (Cache.TryGetValue(prefabHash, out var value)) { return value; } if ((Object)(object)ZNetScene.instance == (Object)null) { return PrefabKind.None; } GameObject prefab = ZNetScene.instance.GetPrefab(prefabHash); PrefabKind prefabKind = (((Object)(object)prefab != (Object)null) ? ClassifyPrefab(prefab) : PrefabKind.None); Cache[prefabHash] = prefabKind; return prefabKind; } private static PrefabKind ClassifyPrefab(GameObject p) { if ((Object)(object)p.GetComponent<Piece>() != (Object)null || (Object)(object)p.GetComponent<Container>() != (Object)null || (Object)(object)p.GetComponent<TombStone>() != (Object)null) { return PrefabKind.Built; } if ((Object)(object)p.GetComponent<Player>() != (Object)null || (Object)(object)p.GetComponent<Trader>() != (Object)null || (Object)(object)p.GetComponent<Fish>() != (Object)null) { return PrefabKind.None; } Character component = p.GetComponent<Character>(); if ((Object)(object)component != (Object)null) { return IsHostile(component) ? PrefabKind.Monster : PrefabKind.None; } if ((Object)(object)p.GetComponent<ItemDrop>() != (Object)null) { return PrefabKind.Item; } if ((Object)(object)p.GetComponent<Ragdoll>() != (Object)null) { return PrefabKind.Debris; } ZNetView component2 = p.GetComponent<ZNetView>(); if ((Object)(object)component2 != (Object)null && !component2.m_persistent && (Object)(object)p.GetComponent<TimedDestruction>() != (Object)null && (Object)(object)p.GetComponent<Projectile>() == (Object)null) { return PrefabKind.Debris; } return PrefabKind.None; } private static bool IsHostile(Character c) { //IL_0010: 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_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_0018: 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_0050: Expected I4, but got Unknown if (c.m_boss) { return false; } Faction faction = c.m_faction; Faction val = faction; switch (val - 2) { case 0: case 1: case 2: case 3: case 4: case 5: case 7: case 8: case 11: return true; default: return false; } } } internal class ClearCounts { public int Items; public int Monsters; public int Debris; } internal class ServerMaintenance : MonoBehaviour { internal static ServerMaintenance Instance; internal const int ClearOk = 0; internal const int ClearExpired = 1; internal const int ClearBusy = 2; internal const int ClearDisabled = 3; private const float TokenLifeSec = 60f; private const float ClearCooldownSec = 30f; private const float MemCooldownSec = 60f; private string _token; private long _tokenOwner; private float _tokenAt; private float _lastClear = -999f; private float _lastMem = -999f; private bool _busy; private void Awake() { Instance = this; } internal ClearCounts Preview(long admin, out string token) { token = ""; ClearCounts clearCounts = new ClearCounts(); if (!Plugin.AllowServerClear.Value || ZDOMan.instance == null) { return clearCounts; } Collect(out var items, out var monsters, out var debris); clearCounts.Items = items.Count; clearCounts.Monsters = monsters.Count; clearCounts.Debris = debris.Count; token = Guid.NewGuid().ToString("N"); _token = token; _tokenOwner = admin; _tokenAt = Time.realtimeSinceStartup; return clearCounts; } internal int TryRun(long admin, string token, out ClearCounts counts) { counts = new ClearCounts(); if (!Plugin.AllowServerClear.Value) { return 3; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (string.IsNullOrEmpty(_token) || token != _token || admin != _tokenOwner || realtimeSinceStartup - _tokenAt > 60f) { return 1; } if (_busy || realtimeSinceStartup - _lastClear < 30f) { return 2; } _token = null; _lastClear = realtimeSinceStartup; Collect(out var items, out var monsters, out var debris); counts.Items = items.Count; counts.Monsters = monsters.Count; counts.Debris = debris.Count; _busy = true; ((MonoBehaviour)this).StartCoroutine(RunClear(admin, items, monsters, debris)); return 0; } private IEnumerator RunClear(long admin, List<ZDOID> items, List<ZDOID> monsters, List<ZDOID> debris) { int doneItems = 0; int doneMonsters = 0; int doneDebris = 0; int batch = Mathf.Max(10, Plugin.ClearBatchPerFrame.Value); int inBatch = 0; foreach (ZDOID id in items) { if (DestroyZdo(id, recheckTamed: false)) { doneItems++; } int num = inBatch + 1; inBatch = num; if (num >= batch) { inBatch = 0; yield return null; } } foreach (ZDOID id2 in monsters) { if (DestroyZdo(id2, recheckTamed: true)) { doneMonsters++; } int num = inBatch + 1; inBatch = num; if (num >= batch) { inBatch = 0; yield return null; } } foreach (ZDOID id3 in debris) { if (DestroyZdo(id3, recheckTamed: false)) { doneDebris++; } int num = inBatch + 1; inBatch = num; if (num >= batch) { inBatch = 0; yield return null; } } _busy = false; Plugin.Log.LogInfo((object)$"[LagWatch] server clear by admin peer {admin}: items={doneItems} monsters={doneMonsters} debris={doneDebris}"); AdminActionRpc.SendClearResult(admin, 0, doneItems, doneMonsters, doneDebris); } private static bool DestroyZdo(ZDOID id, bool recheckTamed) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) ZDOMan instance = ZDOMan.instance; ZDO val = ((instance != null) ? instance.GetZDO(id) : null); if (val == null) { return false; } if (recheckTamed && val.GetBool(ZDOVars.s_tamed, false)) { return false; } val.SetOwner(ZDOMan.GetSessionID()); ZDOMan.instance.DestroyZDO(val); return true; } private static void Collect(out List<ZDOID> items, out List<ZDOID> monsters, out List<ZDOID> debris) { //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) items = new List<ZDOID>(); monsters = new List<ZDOID>(); debris = new List<ZDOID>(); if (ZDOMan.instance == null || (Object)(object)ZNetScene.instance == (Object)null) { return; } foreach (ZDO value in ZDOMan.instance.m_objectsByID.Values) { switch (PrefabClassifier.Classify(value.GetPrefab())) { case PrefabKind.Item: items.Add(value.m_uid); break; case PrefabKind.Debris: debris.Add(value.m_uid); break; case PrefabKind.Monster: if (!value.GetBool(ZDOVars.s_tamed, false)) { monsters.Add(value.m_uid); } break; } } } internal int TryCleanMemory(long admin) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (_busy || realtimeSinceStartup - _lastMem < 60f) { return 2; } _lastMem = realtimeSinceStartup; _busy = true; ((MonoBehaviour)this).StartCoroutine(RunCleanMemory(admin)); return 0; } private IEnumerator RunCleanMemory(long admin) { Stopwatch sw = Stopwatch.StartNew(); ServerMonitor.ReadMemory(out var wsBefore, out var heapBefore); ServerHealth.ClearErrorTable(); AsyncOperation unload = Resources.UnloadUnusedAssets(); while (!unload.isDone) { yield return null; } GC.Collect(); yield return null; yield return (object)new WaitForSecondsRealtime(1f); ServerMonitor.ReadMemory(out var wsAfter, out var heapAfter); sw.Stop(); _busy = false; Plugin.Log.LogInfo((object)$"[LagWatch] memory cleanup by admin peer {admin}: process {wsBefore:F0}MB -> {wsAfter:F0}MB, managed heap {heapBefore:F0}MB -> {heapAfter:F0}MB, {sw.Elapsed.TotalSeconds:F1}s"); AdminActionRpc.SendMemResult(admin, 0, wsBefore, wsAfter, heapBefore, heapAfter, (float)sw.Elapsed.TotalSeconds); } } internal class ServerSnap { public bool Valid; public float CpuPct; public float MemMB; public float GcMB; public float FrameAvgMs; public float FrameMaxMs; public float OutKBs; public float InKBs; public int Hitches; public int Peers; public int Zdo; public int ScanMs; public int UptimeMin; public byte CpuLv; public byte FrameLv; public byte Level; internal void Write(ZPackage p) { p.Write(Valid); p.Write(CpuPct); p.Write(MemMB); p.Write(GcMB); p.Write(FrameAvgMs); p.Write(FrameMaxMs); p.Write(OutKBs); p.Write(InKBs); p.Write(Hitches); p.Write(Peers); p.Write(Zdo); p.Write(ScanMs); p.Write(UptimeMin); p.Write(CpuLv); p.Write(FrameLv); p.Write(Level); } internal static ServerSnap Read(ZPackage p) { return new ServerSnap { Valid = p.ReadBool(), CpuPct = p.ReadSingle(), MemMB = p.ReadSingle(), GcMB = p.ReadSingle(), FrameAvgMs = p.ReadSingle(), FrameMaxMs = p.ReadSingle(), OutKBs = p.ReadSingle(), InKBs = p.ReadSingle(), Hitches = p.ReadInt(), Peers = p.ReadInt(), Zdo = p.ReadInt(), ScanMs = p.ReadInt(), UptimeMin = p.ReadInt(), CpuLv = p.ReadByte(), FrameLv = p.ReadByte(), Level = p.ReadByte() }; } } internal static class ServerMonitor { internal static ServerSnap Current = new ServerSnap(); private static Process _proc; private static TimeSpan _lastCpu; private static float _lastWall = -1f; private static int _frames; private static int _hitches; private static float _sumDt; private static float _maxDt; internal static void OnFrame(float dt) { _frames++; _sumDt += dt; if (dt > _maxDt) { _maxDt = dt; } if (dt * 1000f >= Plugin.DiagHitchMs.Value) { _hitches++; } } internal static void Sample(float now) { ServerSnap serverSnap = new ServerSnap { Valid = true }; serverSnap.FrameAvgMs = ((_frames > 0) ? (_sumDt / (float)_frames * 1000f) : 0f); serverSnap.FrameMaxMs = _maxDt * 1000f; serverSnap.Hitches = _hitches; _frames = (_hitches = 0); _sumDt = (_maxDt = 0f); try { if (_proc == null) { _proc = Process.GetCurrentProcess(); } _proc.Refresh(); TimeSpan totalProcessorTime = _proc.TotalProcessorTime; float num = now - _lastWall; bool flag = _lastWall < 0f || num > Plugin.SampleIntervalSec.Value * 3f; serverSnap.CpuPct = (flag ? Current.CpuPct : ((float)((totalProcessorTime - _lastCpu).TotalSeconds / (double)Mathf.Max(num, 0.1f) * 100.0))); _lastCpu = totalProcessorTime; _lastWall = now; serverSnap.MemMB = (float)_proc.WorkingSet64 / 1048576f; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] process stats unavailable: " + ex.Message)); } serverSnap.GcMB = (float)GC.GetTotalMemory(forceFullCollection: false) / 1048576f; foreach (PeerStat value in PeerSampler.Stats.Values) { serverSnap.OutKBs += value.OutBytesSec / 1024f; serverSnap.InKBs += value.InBytesSec / 1024f; } serverSnap.Peers = PeerSampler.Stats.Count; serverSnap.Zdo = ZdoOwnership.TotalZdos; serverSnap.ScanMs = (int)ZdoOwnership.LastScanMs; serverSnap.UptimeMin = (int)(Time.realtimeSinceStartup / 60f); serverSnap.CpuLv = High(serverSnap.CpuPct, Plugin.ServerCpuWarn.Value, Plugin.ServerCpuBad.Value); serverSnap.FrameLv = High(serverSnap.FrameAvgMs, Plugin.ServerFrameWarnMs.Value, Plugin.ServerFrameBadMs.Value); serverSnap.Level = (byte)Mathf.Max((int)serverSnap.CpuLv, (int)serverSnap.FrameLv); Current = serverSnap; } internal static void ReadMemory(out float workingSetMB, out float managedHeapMB) { workingSetMB = 0f; try { if (_proc == null) { _proc = Process.GetCurrentProcess(); } _proc.Refresh(); workingSetMB = (float)_proc.WorkingSet64 / 1048576f; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[LagWatch] process memory unavailable: " + ex.Message)); } managedHeapMB = (float)GC.GetTotalMemory(forceFullCollection: false) / 1048576f; } private static byte High(float v, float warn, float bad) { return (byte)((v >= bad) ? 2u : ((v >= warn) ? 1u : 0u)); } } internal static class ZdoOwnership { internal class OwnerInfo { public int Count; public readonly Dictionary<int, int> PrefabCounts = new Dictionary<int, int>(); } internal const int StepBudget = 20000; internal static Dictionary<long, OwnerInfo> Owners = new Dictionary<long, OwnerInfo>(); internal static Dictionary<int, int> GlobalPrefabs = new Dictionary<int, int>(); internal static int TotalZdos; internal static long LastScanMs; internal static bool HasData; private static ZDO[] _snapshot = (ZDO[])(object)new ZDO[0]; private static int _count; private static int _pos; private static bool _scanning; private static bool _countGlobal; private static Dictionary<long, OwnerInfo> _tmpOwners = new Dictionary<long, OwnerInfo>(); private static Dictionary<int, int> _tmpGlobal = new Dictionary<int, int>(); private static long _workTicks; internal static bool Scanning => _scanning; internal static void BeginScan() { if (!_scanning && ZDOMan.instance != null) { Stopwatch stopwatch = Stopwatch.StartNew(); Dictionary<ZDOID, ZDO> objectsByID = ZDOMan.instance.m_objectsByID; _count = objectsByID.Count; if (_snapshot.Length < _count) { _snapshot = (ZDO[])(object)new ZDO[_count + _count / 4 + 16]; } objectsByID.Values.CopyTo(_snapshot, 0); stopwatch.Stop(); _pos = 0; _countGlobal = ServerHealth.Active; _tmpOwners = new Dictionary<long, OwnerInfo>(); _tmpGlobal = new Dictionary<int, int>(); _workTicks = stopwatch.ElapsedTicks; _scanning = true; } } internal static void Step(int budget) { if (!_scanning) { return; } Stopwatch stopwatch = Stopwatch.StartNew(); int num = Math.Min(_pos + budget, _count); while (_pos < num) { ZDO val = _snapshot[_pos]; if (val != null) { int prefab = val.GetPrefab(); if (_countGlobal) { _tmpGlobal.TryGetValue(prefab, out var value); _tmpGlobal[prefab] = value + 1; } long owner = val.GetOwner(); if (owner != 0 && PrefabClassifier.Classify(prefab) != PrefabKind.Built) { if (!_tmpOwners.TryGetValue(owner, out var value2)) { value2 = new OwnerInfo(); _tmpOwners[owner] = value2; } value2.Count++; value2.PrefabCounts.TryGetValue(prefab, out var value3); value2.PrefabCounts[prefab] = value3 + 1; } } _pos++; } stopwatch.Stop(); _workTicks += stopwatch.ElapsedTicks; if (_pos >= _count) { Owners = _tmpOwners; GlobalPrefabs = _tmpGlobal; TotalZdos = _count; LastScanMs = _workTicks * 1000 / Stopwatch.Frequency; HasData = true; _scanning = false; Array.Clear(_snapshot, 0, _count); } } internal static string TopPrefabs(long owner, int take) { if (!Owners.TryGetValue(owner, out var value)) { return ""; } List<KeyValuePair<int, int>> list = new List<KeyValuePair<int, int>>(value.PrefabCounts); list.Sort((KeyValuePair<int, int> a, KeyValuePair<int, int> b) => b.Value.CompareTo(a.Value)); List<string> list2 = new List<string>(); for (int num = 0; num < list.Count && num < take; num++) { GameObject val = (((Object)(object)ZNetScene.instance != (Object)null) ? ZNetScene.instance.GetPrefab(list[num].Key) : null); string arg = (((Object)(object)val != (Object)null) ? ((Object)val).name : list[num].Key.ToString()); list2.Add($"{arg}x{list[num].Value}"); } return string.Join(", ", list2); } } internal class ButtonFx : MonoBehaviour, IPointerEnterHandler, IEventSystemHandler, IPointerExitHandler, IPointerDownHandler, IPointerUpHandler { private const float HoverScale = 1.04f; private const float PressScale = 0.97f; private const float Speed = 18f; private Button _button; private bool _hover; private bool _down; private void Awake() { _button = ((Component)this).GetComponent<Button>(); } private void OnDisable() { //IL_0017: Unknown result type (might be due to invalid IL or missing references) _hover = (_down = false); ((Component)this).transform.localScale = Vector3.one; } public void OnPointerEnter(PointerEventData e) { _hover = true; } public void OnPointerExit(PointerEventData e) { _hover = false; _down = false; } public void OnPointerDown(PointerEventData e) { _down = true; } public void OnPointerUp(PointerEventData e) { _down = false; } private void Update() { //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) float num = ((!((Object)(object)_button == (Object)null) && !((Selectable)_button).interactable) ? 1f : (_down ? 0.97f : (_hover ? 1.04f : 1f))); float x = ((Component)this).transform.localScale.x; if (!Mathf.Approximately(x, num)) { float num2 = Mathf.Lerp(x, num, 1f - Mathf.Exp(-18f * Time.unscaledDeltaTime)); if (Mathf.Abs(num2 - num) < 0.0005f) { num2 = num; } ((Component)this).transform.localScale = new Vector3(num2, num2, 1f); } } } internal class LagPanel : MonoBehaviour { private class Row { public GameObject Go; public Image Bg; public Image Light; public Text Name; public Text Ping; public Text Fps; public Text Objects; public Text Queue; public Text Traffic; public Text Mods; public Text Status; public Text SendLabel; public Button SendBtn; public float LabelResetAt; public float PendingSince; } internal static LagPanel Instance; private const int ModPreviewCount = 12; private const float AckLabelSec = 2.5f; private const float AckTimeoutSec = 6f; private static readonly float[] Cols = new float[10] { 32f, 230f, 100f, 90f, 130f, 110f, 120f, 80f, 100f, 150f }; private static readonly string[] ColKeys = new string[10] { "", "colPlayer", "colPing", "colFps", "colObjects", "colQueue", "colTraffic", "colMods", "colStatus", "" }; private GameObject _canvasGo; private Transform _content; private Text _summary; private Text _autoLabel; private Text _detail; private Text _empty; private Button _autoBtn; private readonly Dictionary<Text, string> _labels = new Dictionary<Text, string>(); private readonly Dictionary<long, Row> _rows = new Dictionary<long, Row>(); private readonly Dictionary<long, string> _mods = new Dictionary<long, string>(); private List<SnapRow> _latest = new List<SnapRow>(); private ServerSnap _server; private Image _srvLight; private Text[] _srvCells; private GameObject _dialog; private Text _dialogText; private Text _dialogOkLabel; private Text _dialogCancelLabel; private Button _dialogOk; private Button _dialogCancel; private Action _onConfirm; private bool _auto; private long _selected; private bool _open; private bool _openedInventory; private static readonly float[] SrvCols = new float[9] { 110f, 110f, 190f, 120f, 100f, 90f, 130f, 150f, 100f }; private void Awake() { Instance = this; } private void Update() { //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_003d: 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) if ((Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsDedicated()) { return; } KeyboardShortcut value; if (_open) { if (!Input.GetKeyDown((KeyCode)27)) { value = Plugin.PanelHotkey.Value; if (!((KeyboardShortcut)(ref value)).IsDown()) { if (_openedInventory && !InventoryGui.IsVisible()) { Close(); } else { TickLabels(); } return; } } Close(); } else { value = Plugin.PanelHotkey.Value; if (((KeyboardShortcut)(ref value)).IsDown() && !TypingInGame()) { Open(); } } } private static bool TypingInGame() { return ((Object)(object)Chat.instance != (Object)null && Chat.instance.HasFocus()) || Console.IsVisible() || TextInput.IsVisible(); } private void Open() { if ((Object)(object)Player.m_localPlayer == (Object)null) { return; } if (!AdminGate.IsLocalAdmin) { AdminGate.Request(); MessageHud instance = MessageHud.instance; if (instance != null) { instance.ShowMessage((MessageType)2, Strings.Get("adminOnly"), 0, (Sprite)null, false, true); } return; } EnsureUi(); foreach (KeyValuePair<Text, string> label in _labels) { label.Key.text = Strings.Get(label.Value); } _canvasGo.SetActive(true); _open = true; if ((Object)(object)InventoryGui.instance != (Object)null && !InventoryGui.IsVisible()) { InventoryGui.instance.Show((Container)null, 1); _openedInventory = true; } Refresh(); LagWatchRpc.SendSubscribe(on: true); } private void Close() { if (_open) { _open = false; LagWatchRpc.SendSubscribe(on: false); if ((Object)(object)_dialog != (Object)null) { _dialog.SetActive(false); } _onConfirm = null; if ((Object)(object)_canvasGo != (Object)null) { _canvasGo.SetActive(false); } if (_openedInventory && (Object)(object)InventoryGui.instance != (Object)null && InventoryGui.IsVisible()) { InventoryGui.instance.Hide(); } _openedInventory = false; } } internal void OnSnapshot(bool autoWarn, ServerSnap server, List<SnapRow> rows) { _auto = autoWarn; _server = server; _latest = rows; if (_open) { Refresh(); } } internal void OnWarnAck(long uid, int code) { if (_rows.TryGetValue(uid, out var value)) { value.SendLabel.text = Strings.Get(code switch { 1 => "btnCooldown", 0 => "btnSent", _ => "btnNoMod", }); UiKit.SetButtonStyle(value.SendBtn, (code == 0) ? UiKit.BtnStyle.Success : UiKit.BtnStyle.Neutral); value.PendingSince = 0f; value.LabelResetAt = Time.unscaledTime + 2.5f; } } internal void OnDetail(long uid, string mods) { _mods[uid] = mods; if (_open && uid == _selected) { RefreshDetail(); } } private void TickLabels() { float unscaledTime = Time.unscaledTime; foreach (Row value in _rows.Values) { bool flag = value.PendingSince > 0f && unscaledTime - value.PendingSince > 6f; bool flag2 = value.LabelResetAt > 0f && unscaledTime >= value.LabelResetAt; if (flag || flag2) { value.SendLabel.text = Strings.Get("btnSend"); UiKit.SetButtonStyle(value.SendBtn, UiKit.BtnStyle.Primary); value.PendingSince = 0f; value.LabelResetAt = 0f; } } } private void Refresh() { _autoLabel.text = Strings.Get(_auto ? "autoOn" : "autoOff"); UiKit.SetButtonStyle(_autoBtn, _auto ? UiKit.BtnStyle.Success : UiKit.BtnStyle.Neutral); RefreshServer(); int num = 0; int num2 = 0; int num3 = 0; foreach (SnapRow item in _latest) { if (item.Level >= 2) { num3++; } else if (item.Level == 1) { num2++; } else { num++; } } _summary.text = Strings.Fmt("summary", num, num2, num3); ((Component)_empty).gameObject.SetActive(_latest.Count == 0); List<SnapRow> list = new List<SnapRow>(_latest); list.Sort((SnapRow a, SnapRow b) => (a.Level != b.Level) ? b.Level.CompareTo(a.Level) : b.PingAvg.CompareTo(a.PingAvg)); HashSet<long> hashSet = new HashSet<long>(); for (int num4 = 0; num4 < list.Count; num4++) { SnapRow snapRow = list[num4]; hashSet.Add(snapRow.Uid); if (!_rows.TryGetValue(snapRow.Uid, out var value)) { value = CreateRow(snapRow.Uid); _rows[snapRow.Uid] = value; } value.Go.transform.SetSiblingIndex(num4); UpdateRow(value, snapRow); } List<long> list2 = new List<long>(); foreach (long key in _rows.Keys) { if (!hashSet.Contains(key)) { list2.Add(key); } } foreach (long item2 in list2) { Object.Destroy((Object)(object)_rows[item2].Go); _rows.Remove(item2); _mods.Remove(item2); } RefreshDetail(); } private void RefreshServer() { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_015a: Unknown result type (might be due to invalid IL or missing references) //IL_01b5: Unknown result type (might be due to invalid IL or missing references) //IL_019a: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_01ec: Unknown result type (might be due to invalid IL or missing references) //IL_0223: Unknown result type (might be due to invalid IL or missing references) //IL_025f: Unknown result type (might be due to invalid IL or missing references) //IL_02ab: Unknown result type (might be due to invalid IL or missing references) //IL_02e2: Unknown result type (might be due to invalid IL or missing references) if (_srvCells != null) { ServerSnap serverSnap = ((_server != null && _server.Valid) ? _server : null); int num = serverSnap?.Level ?? 0; string value = ((serverSnap == null) ? "--" : Strings.Get((num >= 2) ? "srvBad" : ((num == 1) ? "srvFair" : "srvGood"))); ((Graphic)_srvLight).color = ((serverSnap == null) ? UiKit.Gray : UiKit.ServerColor(num)); _srvCells[0].text = Cell(Strings.Get("srvState"), value, (serverSnap == null) ? UiKit.Gray : UiKit.ServerColor(num)); _srvCells[1].text = Cell(Strings.Get("srvPing"), MyPing(), UiKit.TextMain); _srvCells[2].text = ((serverSnap == null) ? Cell(Strings.Get("srvFrame"), "--", UiKit.Gray) : Cell((serverSnap.Hitches > 0) ? Strings.Fmt("srvHitch", serverSnap.Hitches) : Strings.Get("srvFrame"), $"{serverSnap.FrameAvgMs:F0}/{serverSnap.FrameMaxMs:F0}ms", UiKit.ServerColor(serverSnap.FrameLv))); _srvCells[3].text = ((serverSnap == null) ? Cell(Strings.Get("srvCpu"), "--", UiKit.Gray) : Cell(Strings.Get("srvCpu"), $"{serverSnap.CpuPct:F0}%", UiKit.ServerColor(serverSnap.CpuLv))); _srvCells[4].text = Cell(Strings.Get("srvMem"), (serverSnap == null) ? "--" : FormatMem(serverSnap.MemMB), UiKit.TextMain); _srvCells[5].text = Cell(Strings.Get("srvPlayers"), (serverSnap == null) ? "--" : serverSnap.Peers.ToString(), UiKit.TextMain); _srvCells[6].text = Cell(Strings.Get("srvObjects"), (serverSnap == null) ? "--" : serverSnap.Zdo.ToString("N0"), UiKit.TextMain); _srvCells[7].text = Cell(Strings.Get("srvTraffic"), (serverSnap == null) ? "--" : $"{serverSnap.OutKBs:F0}/{serverSnap.InKBs:F0}KB/s", UiKit.TextMain); _srvCells[8].text = Cell(Strings.Get("srvUptime"), (serverSnap == null) ? "--" : FormatUptime(serverSnap.UptimeMin), UiKit.TextMain); } } private static string Cell(string label, string value, Color color) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) return "<size=15><color=#9a9aa5>" + label + "</color></size>\n<color=#" + ColorUtility.ToHtmlStringRGB(color) + ">" + value + "</color>"; } private static string MyPing() { if ((Object)(object)ZNet.instance == (Object)null) { return "--"; } if (ZNet.instance.IsServer()) { return "0ms (" + Strings.Get("srvHost") + ")"; } float num = default(float); float num2 = default(float); int num3 = default(int); float num4 = default(float); float num5 = default(float); ZNet.instance.GetNetStats(ref num, ref num2, ref num3, ref num4, ref num5); return num3 + "ms"; } private static string FormatMem(float mb) { return (mb >= 1024f) ? $"{mb / 1024f:F1}GB" : $"{mb:F0}MB"; } private static string FormatUptime(int min) { return (min >= 60) ? $"{min / 60}h {min % 60}m" : $"{min}m"; } private void UpdateRow(Row row, SnapRow s) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_01b5: Unknown result type (might be due to invalid IL or missing references) ((Graphic)row.Light).color = UiKit.LevelColor(s.Level); ((Graphic)row.Bg).color = ((s.Uid == _selected) ? new Color(0.24f, 0.28f, 0.38f, 0.95f) : new Color(0.14f, 0.14f, 0.17f, 0.92f)); row.Name.text = s.Name; SetCell(row.Ping, $"{s.PingAvg}ms", s.PingLv); SetCell(row.Fps, s.Reported ? s.AvgFps.ToString("F0") : "--", s.Reported ? s.FpsLv : (-1)); SetCell(row.Objects, s.Zdo.ToString(), s.ZdoLv); SetCell(row.Queue, $"{s.QueueBytes / 1024}KB", s.QueueLv); SetCell(row.Traffic, $"{s.TrafficKBs:F0}KB/s", s.TrafficLv); SetCell(row.Mods, s.Reported ? s.ModCount.ToString() : "--", -1); row.Status.text = Strings.Get((s.Level >= 2) ? "stBad" : ((s.Level == 1) ? "stWarn" : "stOk")); ((Graphic)row.Status).color = UiKit.LevelColor(s.Level); } private static void SetCell(Text t, string value, int level) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) t.text = value; ((Graphic)t).color = ((level < 0) ? UiKit.Gray : ((level == 0) ? UiKit.TextMain : UiKit.LevelColor(level))); } private void RefreshDetail() { SnapRow snapRow = _latest.Find((SnapRow r) => r.Uid == _selected); if (snapRow == null) { _detail.text = Strings.Get("detailEmpty"); return; } string text = Strings.Get((snapRow.Level >= 2) ? "stBad" : ((snapRow.Level == 1) ? "stWarn" : "stOk")); string text2 = (snapRow.Reported ? snapRow.AvgFps.ToString("F0") : "--"); string text3 = (snapRow.Reported ? snapRow.MinFps.ToString("F0") : "--"); string text4 = (snapRow.Reported ? snapRow.ModCount.ToString() : "--"); _detail.text = Strings.Fmt("detail", snapRow.Name, text, snapRow.PingAvg, snapRow.PingMax, snapRow.Zdo, snapRow.QueueBytes / 1024, snapRow.TrafficKBs.ToString("F0"), text2, text3, text4, string.IsNullOrEmpty(snapRow.Top) ? "-" : snapRow.Top, ModPreview(snapRow)); } private string ModPreview(SnapRow s) { if (!s.Reported) { return Strings.Get("notReported"); } if (!_mods.TryGetValue(s.Uid, out var value) || value.Length == 0) { return "..."; } string[] array = value.Split(new char[1] { '|' }); int num = Mathf.Min(array.Length, 12); string text = string.Join(", ", array, 0, num); return (array.Length > num) ? (text + Strings.Fmt("modsMore", array.Length - num)) : text; } private void Select(long uid) { _selected = uid; if (!_mods.ContainsKey(uid)) { LagWatchRpc.SendDetailRequest(uid); } Refresh(); } private void EnsureUi() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: 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_0140: Unknown result type (might be due to invalid IL or missing references) //IL_014f: Unknown result type (might be due to invalid IL or missing references) //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_017a: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) //IL_01f3: 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_0252: Unknown result type (might be due to invalid IL or missing references) //IL_0261: Unknown result type (might be due to invalid IL or missing references) //IL_0270: Unknown result type (might be due to invalid IL or missing references) //IL_027f: Unknown result type (might be due to invalid IL or missing references) //IL_0289: Expected O, but got Unknown //IL_0295: Unknown result type (might be due to invalid IL or missing references) //IL_02a4: Unknown result type (might be due to invalid IL or missing references) //IL_02c1: Unknown result type (might be due to invalid IL or missing references) //IL_02cb: Expected O, but got Unknown //IL_02f4: Unknown result type (might be due to invalid IL or missing references) //IL_0303: Unknown result type (might be due to invalid IL or missing references) //IL_0312: Unknown result type (might be due to invalid IL or missing references) //IL_0321: Unknown result type (might be due to invalid IL or missing references) //