From a0f40cf2cd90c82a558bd118a632e224b10ce8ca Mon Sep 17 00:00:00 2001 From: erik Date: Sat, 24 May 2025 21:10:45 +0200 Subject: [PATCH] Client telemetry --- MosswartMassacre/ClientTelemetry.cs | 35 ++ MosswartMassacre/ManyHook.cs | 392 +++++++++++++++++++++++ MosswartMassacre/MosswartMassacre.csproj | 2 + MosswartMassacre/PluginCore.cs | 11 +- MosswartMassacre/WebSocket.cs | 34 +- 5 files changed, 466 insertions(+), 8 deletions(-) create mode 100644 MosswartMassacre/ClientTelemetry.cs create mode 100644 MosswartMassacre/ManyHook.cs diff --git a/MosswartMassacre/ClientTelemetry.cs b/MosswartMassacre/ClientTelemetry.cs new file mode 100644 index 0000000..78a9791 --- /dev/null +++ b/MosswartMassacre/ClientTelemetry.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using System.Threading; +using System; + +public class ClientTelemetry +{ + private readonly Process _proc; + + public ClientTelemetry() + { + _proc = Process.GetCurrentProcess(); + } + + /// Working-set memory in bytes. + public long MemoryBytes => _proc.WorkingSet64; + + /// Total open handles. + public int HandleCount => _proc.HandleCount; + + /// CPU utilisation (%) averaged over . + public float GetCpuUsage(int sampleMs = 500) + { + // you can keep your PerformanceCounter variant, but here’s a simpler PID-based way: + var startCpu = _proc.TotalProcessorTime; + var start = DateTime.UtcNow; + Thread.Sleep(sampleMs); + var endCpu = _proc.TotalProcessorTime; + var end = DateTime.UtcNow; + + // CPU‐time used across all cores: + var cpuMs = (endCpu - startCpu).TotalMilliseconds; + var elapsedMs = (end - start).TotalMilliseconds * Environment.ProcessorCount; + return (float)(cpuMs / elapsedMs * 100.0); + } +} diff --git a/MosswartMassacre/ManyHook.cs b/MosswartMassacre/ManyHook.cs new file mode 100644 index 0000000..424cd46 --- /dev/null +++ b/MosswartMassacre/ManyHook.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using Decal.Adapter; + +namespace MosswartMassacre +{ + public class MultiHook + { + internal IntPtr Entrypoint; + internal Delegate Del; + + internal List CallLocations = new List(); + internal List Hooks = new List(); + + public MultiHook(int entrypoint, params int[] callLocations) + { + Entrypoint = (IntPtr)entrypoint; + CallLocations.AddRange(callLocations); + Hooks.AddRange(callLocations.Select(c => new Hook((int)Entrypoint, c))); + } + + public bool Setup(Delegate del) + { + Del = del; + return !Hooks.Any(h => !h.Setup(del)); + } + + public bool Remove() + { + return !Hooks.Any(h => !h.Remove()); + } + } + + /// + /// New improved Hooker + /// + public class Hook + { + internal IntPtr Entrypoint; + internal Delegate Del; + internal int call; + + public Hook(int entrypoint, int call_location) + { + Entrypoint = (IntPtr)entrypoint; + call = call_location; + } + public bool Setup(Delegate del) + { + if (!hookers.Contains(this)) + { + Del = del; + if (ReadCall(call) != (int)Entrypoint) + { + PluginCore.WriteToChat( + $"[Hook] Failed to detour 0x{call:X8}. " + + $"Expected 0x{((int)Entrypoint):X8}, " + + $"got 0x{ReadCall(call):X8}" + ); + return false; + } + if (!PatchCall(call, Marshal.GetFunctionPointerForDelegate(Del))) + return false; + + hookers.Add(this); + PluginCore.WriteToChat($"[Hook] Hooked 0x{(int)Entrypoint:X8}"); + return true; + } + return false; + } + public bool Remove() + { + if (hookers.Contains(this)) + { + hookers.Remove(this); + if (PatchCall(call, Entrypoint)) + { + PluginCore.WriteToChat($"[Hook] Un-hooked 0x{(int)Entrypoint:X8}"); + return true; + } + } + return false; + } + + // static half + internal static List hookers = new List(); + + [DllImport("kernel32.dll")] + internal static extern bool VirtualProtectEx( + IntPtr hProcess, IntPtr lpAddress, + UIntPtr dwSize, int flNewProtect, + out int lpflOldProtect + ); + + internal static void Write(IntPtr address, int newValue) + { + unsafe + { + VirtualProtectEx( + Process.GetCurrentProcess().Handle, + address, (UIntPtr)4, 0x40, out int old + ); + *(int*)address = newValue; + VirtualProtectEx( + Process.GetCurrentProcess().Handle, + address, (UIntPtr)4, old, out old + ); + } + } + + internal static bool PatchCall(int callLocation, IntPtr newPointer) + { + unsafe + { + byte* p = (byte*)callLocation; + if ((p[0] & 0xFE) != 0xE8) return false; + int newOffset = (int)newPointer - (callLocation + 5); + Write((IntPtr)(callLocation + 1), newOffset); + return true; + } + } + + internal static int ReadCall(int callLocation) + { + unsafe + { + byte* p = (byte*)callLocation; + if ((p[0] & 0xFE) != 0xE8) return 0; + int prevOffset = *(int*)(callLocation + 1); + return prevOffset + (callLocation + 5); + } + } + + internal static void Cleanup() + { + for (int i = hookers.Count - 1; i >= 0; i--) + hookers[i].Remove(); + } + } + + /// + /// New improved Hooker (Virtual‐table edition) + /// + public class VHook + { + internal int Entrypoint; + internal Delegate Del; + internal int call; + + public VHook(int entrypoint, int vtbl_address) + { + Entrypoint = entrypoint; + call = vtbl_address; + } + public bool Setup(Delegate del) + { + if (!hookers.Contains(this)) + { + Del = del; + if (ReadVCall(call) != Entrypoint) return false; + if (PatchVCall(call, (int)Marshal.GetFunctionPointerForDelegate(Del))) + { + hookers.Add(this); + PluginCore.WriteToChat($"[VHook] Hooked vtbl slot 0x{call:X8}"); + return true; + } + } + return false; + } + public bool Remove() + { + if (hookers.Contains(this)) + { + hookers.Remove(this); + if (PatchVCall(call, Entrypoint)) + { + PluginCore.WriteToChat($"[VHook] Un-hooked vtbl slot 0x{call:X8}"); + return true; + } + } + return false; + } + + // static half + internal static List hookers = new List(); + + internal static bool PatchVCall(int callLocation, int newPointer) + { + unsafe + { + Hook.Write((IntPtr)callLocation, newPointer); + return *(int*)callLocation == newPointer; + } + } + internal unsafe static int ReadVCall(int callLocation) => *(int*)callLocation; + + internal static void Cleanup() + { + for (int i = hookers.Count - 1; i >= 0; i--) + hookers[i].Remove(); + } + } + + internal static class ChatHooks + { + // For loc_463D60 – UIElement, probably SetText(this, string) + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void SetTextDelegate(IntPtr @this, IntPtr strPtr); + + // For loc_469440 – Same signature; adjust if you find it’s different in IDA! + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate void SetTextDelegate2(IntPtr @this, IntPtr strPtr); + + private static MultiHook _hook1, _hook2; + private static SetTextDelegate _orig1; + private static SetTextDelegate2 _orig2; + + // All relevant direct code addresses (calls/jmps) to loc_463D60: + private static readonly int[] Sites1 = { + //0x00464A22, + //0x0046B8FF, + //0x0047037F, + //0x00474AA1, + 0x004D22A3, + 0x004E5F20, + 0x004F4663, + // (skip 0x004BD370, it's a jmp) +}; + + // All relevant direct code addresses (calls/jmps) to loc_469440: + private static readonly int[] Sites2 = { + //0x00468EFA, + //0x00469F23, + //0x0046A4C5, + //0x004B80EC, + 0x004CDD49, + 0x004F4679, + //0x004F5392, + //0x004F623B, + //0x004F6253, + // (skip 0x004F46E6, it's a jmp) +}; + + + private static bool IsSanePtr(IntPtr ptr) + { + long v = ptr.ToInt64(); + // Only allow user-mode addresses (protects from 0, 0x10, or other garbage) + return v > 0x10000 && v < 0x00007FFFFFFFFFFF; + } + + private static void MyHook1(IntPtr @this, IntPtr strPtr) + { + string msg; + long val = strPtr.ToInt64(); + + // Always log, even if pointer looks bad! + if (val == 0 || val == 0x10 || val < 0x10000) + { + msg = $"[SetText1] strPtr=0x{val:X} [skipped-invalid]"; + } + else + { + try + { + // Try direct pointer to string (UNICODE) + msg = Marshal.PtrToStringUni(strPtr); + if (string.IsNullOrEmpty(msg)) + { + msg = $"[SetText1] strPtr=0x{val:X} [direct:empty]"; + } + else + { + msg = $"[SetText1] strPtr=0x{val:X} \"{msg}\""; + } + } + catch (Exception ex) + { + msg = $"[SetText1] strPtr=0x{val:X} [EX:{ex.GetType().Name}]"; + } + } + + // Actually write to log + LogToFile("SetText1", msg); + + // Always call the original + _orig1(@this, strPtr); + } + + + // Repeat for MyHook2, just change the tag/log if you want to differentiate + private static void MyHook2(IntPtr @this, IntPtr strPtr) + { + try + { + string msg; + long val = strPtr.ToInt64(); + if (val == 0 || val == 0x10 || val < 0x10000) + { + msg = $"[SetText2] strPtr=0x{val:X} [skipped-invalid]"; + } + else + { + try + { + msg = Marshal.PtrToStringUni(strPtr); + if (string.IsNullOrEmpty(msg)) + msg = $"[SetText2] strPtr=0x{val:X} [direct:empty]"; + else + msg = $"[SetText2] \"{msg}\""; + } + catch (Exception ex) + { + msg = $"[SetText2] strPtr=0x{val:X} [EX:{ex.GetType().Name}]"; + } + } + + LogToFile("SetText2", msg); + } + catch { } + finally + { + _orig2(@this, strPtr); + } + } + + + + + public static void Init() + { + unsafe + { + foreach (int addr in Sites2) + { + byte* p = (byte*)addr; + int callTarget = Hook.ReadCall(addr); + // Print the bytes and resolved call target + PluginCore.WriteToChat( + $"[DEBUG] Addr: 0x{addr:X8} Bytes: {p[0]:X2} {p[1]:X2} {p[2]:X2} {p[3]:X2} {p[4]:X2} -> 0x{callTarget:X8}" + ); + } + } + // Patch for loc_463D60 (ENTRY1) + _hook1 = new MultiHook(0x00463D60, Sites1); + _orig1 = Marshal.GetDelegateForFunctionPointer(_hook1.Entrypoint); + if (_hook1.Setup((SetTextDelegate)MyHook1)) + PluginCore.WriteToChat("[ChatHooks] SetText1 hook installed."); + else + PluginCore.WriteToChat("[ChatHooks] SetText1 hook FAILED."); + + // Patch for loc_469440 (ENTRY2) + _hook2 = new MultiHook(0x00469440, Sites2); + _orig2 = Marshal.GetDelegateForFunctionPointer(_hook2.Entrypoint); + if (_hook2.Setup((SetTextDelegate2)MyHook2)) + PluginCore.WriteToChat("[ChatHooks] SetText2 hook installed."); + else + PluginCore.WriteToChat("[ChatHooks] SetText2 hook FAILED."); + } + + public static void Dispose() + { + _hook1?.Remove(); + _hook2?.Remove(); + _hook1 = null; + _hook2 = null; + Hook.Cleanup(); + VHook.Cleanup(); + PluginCore.WriteToChat("[ChatHooks] All hooks removed."); + } + private static void LogToFile(string prefix, string msg) + { + try + { + string characterName = CoreManager.Current.CharacterFilter.Name; + string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + string characterFolder = Path.Combine(pluginFolder, characterName); + + Directory.CreateDirectory(characterFolder); + + string logFile = Path.Combine(characterFolder, "hooklog.txt"); + File.AppendAllText(logFile, $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} [{prefix}] {msg}\r\n"); + } + catch { } + } + } + +} diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj index 9dca846..792b73e 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -148,6 +148,8 @@ Shared\VCS_Connector.cs + + diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs index 5c1b254..809a6ef 100644 --- a/MosswartMassacre/PluginCore.cs +++ b/MosswartMassacre/PluginCore.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Drawing; using System.Globalization; using System.Linq; @@ -73,6 +74,7 @@ namespace MosswartMassacre _inventoryLogger = new MossyInventory(); + } catch (Exception ex) { @@ -140,7 +142,7 @@ namespace MosswartMassacre Telemetry.Start(); if (WebSocketEnabled) WebSocket.Start(); - + } @@ -235,8 +237,10 @@ namespace MosswartMassacre { Decal_DispatchOnChatCommand("/vt setmetastate loot_rare"); } - + DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000); + // Fire and forget: we don't await, since sending is not critical and we don't want to block. + _ = WebSocket.SendRareAsync(rareText); } if (e.Color == 18 && e.Text.EndsWith("!report\"")) @@ -521,7 +525,8 @@ namespace MosswartMassacre WriteToChat("/mm http - Local http-command server enable|disable"); WriteToChat("/mm remotecommand - Listen to allegiance !do/!dot enable|disable"); WriteToChat("/mm getmetastate - Gets the current metastate"); - + WriteToChat("/TESTAR"); + break; case "report": diff --git a/MosswartMassacre/WebSocket.cs b/MosswartMassacre/WebSocket.cs index f13a069..9dbaf74 100644 --- a/MosswartMassacre/WebSocket.cs +++ b/MosswartMassacre/WebSocket.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Decal.Adapter; using Newtonsoft.Json; +using uTank2; namespace MosswartMassacre { @@ -22,7 +23,7 @@ namespace MosswartMassacre public static class WebSocket { // ─── configuration ────────────────────────── - private static readonly Uri WsEndpoint = new Uri("wss://mosswart.snakedesert.se/websocket/"); + private static readonly Uri WsEndpoint = new Uri("wss://overlord.snakedesert.se/websocket/"); private const string SharedSecret = "your_shared_secret"; private const int IntervalSec = 5; private static string SessionId = ""; @@ -185,6 +186,7 @@ namespace MosswartMassacre var envelope = new { type = "chat", + timestamp = DateTime.UtcNow.ToString("o"), character_name = CoreManager.Current.CharacterFilter.Name, text = chatText, color = colorIndex @@ -198,6 +200,8 @@ namespace MosswartMassacre var envelope = new { type = "spawn", + timestamp = DateTime.UtcNow.ToString("o"), + character_name = CoreManager.Current.CharacterFilter.Name, mob = monster, ns = nsCoord, ew = ewCoord @@ -206,6 +210,23 @@ namespace MosswartMassacre var json = JsonConvert.SerializeObject(envelope); await SendEncodedAsync(json, CancellationToken.None); } + public static async Task SendRareAsync(string rare) + { + var coords = Coordinates.Me; + var envelope = new + { + type = "rare", + timestamp = DateTime.UtcNow.ToString("o"), + character_name = CoreManager.Current.CharacterFilter.Name, + name = rare, + ew = coords.EW, + ns = coords.NS, + z = coords.Z + + }; + var json = JsonConvert.SerializeObject(envelope); + await SendEncodedAsync(json, CancellationToken.None); + } // ─── shared send helper with locking ─────────────── @@ -242,6 +263,7 @@ namespace MosswartMassacre private static string BuildPayloadJson() { + var tele = new ClientTelemetry(); var coords = Coordinates.Me; var payload = new { @@ -254,13 +276,15 @@ namespace MosswartMassacre ns = coords.NS, z = coords.Z, kills = PluginCore.totalKills, - onlinetime = (DateTime.Now - PluginCore.statsStartTime) - .ToString(@"dd\.hh\:mm\:ss"), kills_per_hour = PluginCore.killsPerHour.ToString("F0"), + onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"), deaths = 0, - rares_found = PluginCore.rareCount, prismatic_taper_count = 0, - vt_state = VtankControl.VtGetMetaState() + vt_state = VtankControl.VtGetMetaState(), + mem_mb = tele.MemoryBytes, + cpu_pct = tele.GetCpuUsage(), + mem_handles = tele.HandleCount + }; return JsonConvert.SerializeObject(payload); }