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);
}