Telemetry

This commit is contained in:
erikn 2025-04-26 16:55:48 +02:00
parent e010b9d126
commit 78a2479d6c
8 changed files with 446 additions and 6 deletions

View file

@ -0,0 +1,73 @@
using System;
using System.Globalization;
using System.Text.RegularExpressions;
using System.Numerics;
using Decal.Adapter;
namespace MosswartMassacre
{
public class Coordinates
{
public double NS { get; set; } // +N / S
public double EW { get; set; } // +E / W
public double Z { get; set; }
/* -- quick helper: players current AC coords -- */
public static Coordinates Me
{
get
{
Vector3 pos = Utils.GetPlayerPosition(); // world X,Y,Z
uint land = (uint)CoreManager.Current.Actions.Landcell;
double ew = Geometry.LandblockToEW(land, pos.X);
double ns = Geometry.LandblockToNS(land, pos.Y);
return new Coordinates(ew, ns, pos.Z);
}
}
public uint LandBlock => Geometry.GetLandblockFromCoordinates(EW, NS);
/* -------- parsing “33.2S 72.9E” if you ever need it -------- */
private static readonly Regex Rx = new Regex(
@"(?<NSval>\d{1,3}(?:\.\d{1,3})?)(?<NSchr>[ns])[, ]+(?<EWval>\d{1,3}(?:\.\d{1,3})?)(?<EWchr>[ew])",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public Coordinates() { }
public Coordinates(double ew, double ns, double z = 0) { EW = ew; NS = ns; Z = z; }
public static Coordinates FromString(string s)
{
var m = Rx.Match(s);
if (!m.Success) return null;
double ns = double.Parse(m.Groups["NSval"].Value, CultureInfo.InvariantCulture);
if (m.Groups["NSchr"].Value.ToLower() == "s") ns *= -1;
double ew = double.Parse(m.Groups["EWval"].Value, CultureInfo.InvariantCulture);
if (m.Groups["EWchr"].Value.ToLower() == "w") ew *= -1;
return new Coordinates(ew, ns);
}
/* ---------------- distance helpers ---------------- */
public double DistanceTo(Coordinates o)
{
double ns = (((NS * 10) + 1019.5) * 24) - (((o.NS * 10) + 1019.5) * 24);
double ew = (((EW * 10) + 1019.5) * 24) - (((o.EW * 10) + 1019.5) * 24);
return Math.Sqrt(ns * ns + ew * ew + Math.Pow(Z - o.Z, 2));
}
public double DistanceToFlat(Coordinates o)
{
double ns = (((NS * 10) + 1019.5) * 24) - (((o.NS * 10) + 1019.5) * 24);
double ew = (((EW * 10) + 1019.5) * 24) - (((o.EW * 10) + 1019.5) * 24);
return Math.Sqrt(ns * ns + ew * ew);
}
public override string ToString() =>
$"{Math.Abs(NS):F2}{(NS >= 0 ? 'N' : 'S')} {Math.Abs(EW):F2}{(EW >= 0 ? 'E' : 'W')}";
}
}

View file

@ -0,0 +1,121 @@
using System;
using System.Drawing;
using System.Numerics; // Vector3 & Quaternion
namespace MosswartMassacre
{
public static class Geometry
{
/* ---------- heading / quaternion helpers ---------- */
public static Quaternion HeadingToQuaternion(float deg) =>
ToQuaternion((float)Math.PI * -deg / 180.0f, 0, 0);
public static Quaternion RadiansToQuaternion(float rad) =>
ToQuaternion(rad, 0, 0);
public static double QuaternionToHeading(Quaternion q)
{
double sinr = 2 * (q.W * q.X + q.Y * q.Z);
double cosr = 1 - 2 * (q.X * q.X + q.Y * q.Y);
return Math.Atan2(sinr, cosr);
}
public static double CalculateHeading(Vector3 start, Vector3 target)
{
var dy = target.Y - start.Y;
var dx = target.X - start.X;
return (360 - (Math.Atan2(dy, dx) * 180 / Math.PI) + 90) % 360;
}
// yaw (Z) - pitch (Y) - roll (X)
public static Quaternion ToQuaternion(float yaw, float pitch, float roll)
{
float cy = (float)Math.Cos(yaw * 0.5f); float sy = (float)Math.Sin(yaw * 0.5f);
float cp = (float)Math.Cos(pitch * 0.5f); float sp = (float)Math.Sin(pitch * 0.5f);
float cr = (float)Math.Cos(roll * 0.5f); float sr = (float)Math.Sin(roll * 0.5f);
return new Quaternion(
cy * cp * sr - sy * sp * cr, // X
sy * cp * sr + cy * sp * cr, // Y
sy * cp * cr - cy * sp * sr, // Z
cy * cp * cr + sy * sp * sr // W
);
}
/* ---------- landblock / coordinate helpers ---------- */
public static uint GetLandblockFromCoordinates(double ew, double ns)
{
ns = (ns - 0.5) * 10.0;
ew = (ew - 0.5) * 10.0;
uint basex = (uint)(ew + 0x400);
uint basey = (uint)(ns + 0x400);
byte bx = (byte)(basex >> 3);
byte by = (byte)(basey >> 3);
byte cx = (byte)(basex & 7);
byte cy = (byte)(basey & 7);
int block = (bx << 8) | by;
int cell = (cx << 3) | cy;
return (uint)((block << 16) | (cell + 1));
}
public static float LandblockToEW(uint landcell, float xOff)
{
uint l = (landcell & 0xFF000000) / 0x200000;
return (float)(((xOff / 24) + l - 1019.5) / 10);
}
public static float LandblockToNS(uint landcell, float yOff)
{
uint l = (landcell & 0x00FF0000) / 0x2000;
return (float)(((yOff / 24) + l - 1019.5) / 10);
}
public static float EWToLandblock(uint landcell, float ew)
{
uint l = (landcell & 0xFF000000) / 0x200000;
return (float)(((ew * 10) - l + 1019.5) * 24);
}
public static float NSToLandblock(uint landcell, float ns)
{
uint l = (landcell & 0x00FF0000) / 0x2000;
return (float)(((ns * 10) - l + 1019.5) * 24);
}
public static int LandblockXDifference(uint orig, uint dest) =>
(int)(((dest >> 24) - (orig >> 24)) * 192);
public static int LandblockYDifference(uint orig, uint dest) =>
(int)((((dest << 8) >> 24) - ((orig << 8) >> 24)) * 192);
/* ---------- misc helpers ---------- */
public static float Distance2d(float x1, float y1, float x2, float y2) =>
(float)Math.Sqrt(Math.Pow(x2 - x1, 2) + Math.Pow(y2 - y1, 2));
public static bool LineIntersectsRect(Point p1, Point p2, Rectangle r) =>
LineIntersectsLine(p1, p2, new Point(r.X, r.Y), new Point(r.Right, r.Y)) ||
LineIntersectsLine(p1, p2, new Point(r.Right, r.Y), new Point(r.Right, r.Bottom)) ||
LineIntersectsLine(p1, p2, new Point(r.Right, r.Bottom), new Point(r.X, r.Bottom)) ||
LineIntersectsLine(p1, p2, new Point(r.X, r.Bottom), new Point(r.X, r.Y)) ||
(r.Contains(p1) && r.Contains(p2));
public static bool LineIntersectsLine(Point a1, Point a2, Point b1, Point b2)
{
float q = (a1.Y - b1.Y) * (b2.X - b1.X) - (a1.X - b1.X) * (b2.Y - b1.Y);
float d = (a2.X - a1.X) * (b2.Y - b1.Y) - (a2.Y - a1.Y) * (b2.X - b1.X);
if (d == 0) return false;
float r = q / d;
q = (a1.Y - b1.Y) * (a2.X - a1.X) - (a1.X - b1.X) * (a2.Y - a1.Y);
float s = q / d;
return (r >= 0 && r <= 1 && s >= 0 && s <= 1);
}
}
}

View file

@ -22,6 +22,7 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
<PlatformTarget>x86</PlatformTarget> <PlatformTarget>x86</PlatformTarget>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType> <DebugType>pdbonly</DebugType>
@ -46,9 +47,13 @@
<EmbedInteropTypes>False</EmbedInteropTypes> <EmbedInteropTypes>False</EmbedInteropTypes>
<HintPath>lib\Decal.Interop.Inject.dll</HintPath> <HintPath>lib\Decal.Interop.Inject.dll</HintPath>
</Reference> </Reference>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Drawing" /> <Reference Include="System.Drawing" />
<Reference Include="System.Numerics" />
<Reference Include="System.Windows.Forms" /> <Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml.Linq" /> <Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
@ -64,6 +69,10 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Telemetry.cs" />
<Compile Include="Coordinates.cs" />
<Compile Include="Geometry.cs" />
<Compile Include="Utils.cs" />
<Compile Include="PluginSettings.cs" /> <Compile Include="PluginSettings.cs" />
<Compile Include="HttpCommandServer.cs" /> <Compile Include="HttpCommandServer.cs" />
<Compile Include="DelayedCommandManager.cs" /> <Compile Include="DelayedCommandManager.cs" />

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Timers; using System.Timers;
@ -23,6 +24,7 @@ namespace MosswartMassacre
public static bool RemoteCommandsEnabled { get; set; } = false; public static bool RemoteCommandsEnabled { get; set; } = false;
public static bool HttpServerEnabled { get; set; } = false; public static bool HttpServerEnabled { get; set; } = false;
public static string CharTag { get; set; } = ""; public static string CharTag { get; set; } = "";
public static bool TelemetryEnabled { get; set; } = false;
private static Queue<string> rareMessageQueue = new Queue<string>(); private static Queue<string> rareMessageQueue = new Queue<string>();
private static DateTime _lastSent = DateTime.MinValue; private static DateTime _lastSent = DateTime.MinValue;
private static readonly Queue<string> _chatQueue = new Queue<string>(); private static readonly Queue<string> _chatQueue = new Queue<string>();
@ -47,6 +49,9 @@ namespace MosswartMassacre
// Initialize the view (UI) // Initialize the view (UI)
MainView.ViewInit(); MainView.ViewInit();
// Enable TLS1.2
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -59,6 +64,8 @@ namespace MosswartMassacre
try try
{ {
PluginSettings.Save(); PluginSettings.Save();
if (TelemetryEnabled)
Telemetry.Stop(); // ensure no dangling timer / HttpClient
WriteToChat("Mosswart Massacre is shutting down..."); WriteToChat("Mosswart Massacre is shutting down...");
// Unsubscribe from chat message event // Unsubscribe from chat message event
@ -92,7 +99,10 @@ namespace MosswartMassacre
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled; RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled;
HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled; HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
MainView.SetRareMetaToggleState(RareMetaEnabled); MainView.SetRareMetaToggleState(RareMetaEnabled);
if (TelemetryEnabled)
Telemetry.Start();
WriteToChat("Settings loaded."); WriteToChat("Settings loaded.");
} }
@ -363,13 +373,42 @@ namespace MosswartMassacre
switch (subCommand) switch (subCommand)
{ {
case "telemetry":
if (args.Length > 1)
{
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
{
TelemetryEnabled = true;
Telemetry.Start();
PluginSettings.Instance.TelemetryEnabled = true;
WriteToChat("Telemetry streaming ENABLED.");
}
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
{
TelemetryEnabled = false;
Telemetry.Stop();
PluginSettings.Instance.TelemetryEnabled = false;
WriteToChat("Telemetry streaming DISABLED.");
}
else
{
WriteToChat("Usage: /mm telemetry <enable|disable>");
}
}
else
{
WriteToChat("Usage: /mm telemetry <enable|disable>");
}
break;
case "help": case "help":
WriteToChat("Mosswart Massacre Commands:"); WriteToChat("Mosswart Massacre Commands:");
WriteToChat("/mm report - Show current stats"); WriteToChat("/mm report - Show current stats");
WriteToChat("/mm reset - Reset all counters"); WriteToChat("/mm loc - Show current location");
WriteToChat("/mm meta - Toggle rare meta state"); WriteToChat("/mm telemetry - Telemetry streaming enable|disable"); // NEW
WriteToChat("/mm http - http server enable|disable"); WriteToChat("/mm reset - Reset all counters");
WriteToChat("/mm remotecommand - Listen to remote commands enable|disable"); WriteToChat("/mm meta - Toggle rare meta state");
WriteToChat("/mm http - Local http-command server enable|disable");
WriteToChat("/mm remotecommand - Listen to allegiance !do/!dot enable|disable");
break; break;
case "report": case "report":
@ -378,10 +417,14 @@ namespace MosswartMassacre
WriteToChat(reportMessage); WriteToChat(reportMessage);
break; break;
case "loc":
Coordinates here = Coordinates.Me;
var pos = Utils.GetPlayerPosition();
WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
break;
case "reset": case "reset":
RestartStats(); RestartStats();
break; break;
case "meta": case "meta":
RareMetaEnabled = !RareMetaEnabled; RareMetaEnabled = !RareMetaEnabled;
WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}"); WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}");

View file

@ -14,6 +14,7 @@ namespace MosswartMassacre
private bool _rareMetaEnabled = true; private bool _rareMetaEnabled = true;
private bool _httpServerEnabled = false; private bool _httpServerEnabled = false;
private string _charTag = "default"; private string _charTag = "default";
private bool _telemetryEnabled = false;
public static PluginSettings Instance => _instance; public static PluginSettings Instance => _instance;
public static void Initialize() public static void Initialize()
@ -41,6 +42,7 @@ namespace MosswartMassacre
PluginCore.RareMetaEnabled = _instance.RareMetaEnabled; PluginCore.RareMetaEnabled = _instance.RareMetaEnabled;
PluginCore.RemoteCommandsEnabled = _instance.RemoteCommandsEnabled; PluginCore.RemoteCommandsEnabled = _instance.RemoteCommandsEnabled;
PluginCore.HttpServerEnabled = _instance.HttpServerEnabled; PluginCore.HttpServerEnabled = _instance.HttpServerEnabled;
PluginCore.TelemetryEnabled = _instance.TelemetryEnabled;
PluginCore.CharTag = _instance.CharTag; PluginCore.CharTag = _instance.CharTag;
} }
@ -76,5 +78,10 @@ namespace MosswartMassacre
get => _charTag; get => _charTag;
set { _charTag = value; Save(); } set { _charTag = value; Save(); }
} }
public bool TelemetryEnabled
{
get => _telemetryEnabled;
set { _telemetryEnabled = value; Save(); }
}
} }
} }

View file

@ -0,0 +1,110 @@
using System;
using System.Net.Http;
using System.Text;
using System.Timers;
using Decal.Adapter;
using Newtonsoft.Json;
namespace MosswartMassacre
{
/// <summary>
/// Periodically sends gameplay telemetry to your FastAPI collector.
/// Toggle with: Telemetry.Start() / Telemetry.Stop()
/// </summary>
public static class Telemetry
{
/* ============ CONFIG ============ */
private const string Endpoint = "https://mosswart.snakedesert.se/position";
private const string SharedSecret = "your_shared_secret";
private const int IntervalSec = 5; // send every 5 s
/* ============ internals ========== */
private static readonly HttpClient _http = new HttpClient();
private static Timer _timer;
private static bool _enabled;
private static string _sessionId;
/* ============ public API ========= */
public static void Start()
{
if (_enabled) return; // already on
_sessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
_timer = new Timer(IntervalSec * 1000);
_timer.Elapsed += (_, __) => SendSnapshot();
_timer.Start();
_enabled = true;
PluginCore.WriteToChat("[Telemetry] HTTP streaming ENABLED");
PluginCore.WriteToChat("[Tel] timer every " + IntervalSec + " s");
}
public static void Stop()
{
if (!_enabled) return;
_enabled = false;
_timer?.Stop();
_timer?.Dispose();
_timer = null;
PluginCore.WriteToChat("[Telemetry] HTTP streaming DISABLED");
}
/* ============ snapshot builder === */
private static async void SendSnapshot()
{
try
{
var coords = Coordinates.Me;
var payload = new
{
character_name = CoreManager.Current.CharacterFilter.Name,
char_tag = PluginCore.CharTag,
session_id = _sessionId,
timestamp = DateTime.UtcNow.ToString("o"),
ew = coords.EW,
ns = coords.NS,
z = coords.Z,
kills = PluginCore.totalKills,
deaths = 0,
rares_found = PluginCore.rareCount,
prismatic_taper_count = 0,
vt_state = "Unknown"
};
string json = JsonConvert.SerializeObject(payload);
var req = new HttpRequestMessage(HttpMethod.Post, Endpoint)
{
Content = new StringContent(json, Encoding.UTF8, "application/json")
};
req.Headers.Add("X-Plugin-Secret", SharedSecret);
/* ---------- NEW: wait for response & print result ---------- */
var resp = await _http.SendAsync(req);
if (resp.IsSuccessStatusCode)
{
PluginCore.WriteToChat($"[Tel] ✓ {resp.StatusCode}");
}
else
{
PluginCore.WriteToChat($"[Tel] ✗ {resp.StatusCode} ({await resp.Content.ReadAsStringAsync()})");
}
}
catch (Exception ex)
{
var inner = ex.InnerException?.Message ?? "no inner msg";
PluginCore.WriteToChat($"[Tel] FAILED — {ex.GetType().Name}: {ex.Message} ⇢ {inner}");
}
}
}
}

76
MosswartMassacre/Utils.cs Normal file
View file

@ -0,0 +1,76 @@
using System;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
using System.Numerics;
namespace MosswartMassacre
{
/// <summary>
/// Misc. helpers shared across the plugin.
/// Add new utilities here as your project grows.
/// </summary>
public static class Utils
{
/* ----------------------------------------------------------
* 1) Direct-memory physics helpers
* -------------------------------------------------------- */
/// <summary>
/// Return the players raw world position by reading the
/// physics-object pointer (same offsets UB uses: +0x84/88/8C).
/// </summary>
public static unsafe Vector3 GetPlayerPosition()
{
try
{
int id = CoreManager.Current.CharacterFilter.Id;
if (!CoreManager.Current.Actions.IsValidObject(id))
return new Vector3();
byte* p = (byte*)CoreManager.Current.Actions.Underlying.GetPhysicsObjectPtr(id);
return new Vector3(
*(float*)(p + 0x84), // X
*(float*)(p + 0x88), // Y
*(float*)(p + 0x8C)); // Z
}
catch
{
return new Vector3();
}
}
/// <summary>
/// Convenience: returns the current landcell (upper 16 bits of landblock).
/// </summary>
public static uint GetPlayerLandcell() =>
(uint)CoreManager.Current.Actions.Landcell;
/* ----------------------------------------------------------
* 2) High-level wrappers around Geometry / Coordinates
* -------------------------------------------------------- */
/// <summary>
/// Get AC-style coordinates (EW/NS) for the player in one call.
/// </summary>
public static Coordinates GetPlayerCoordinates()
{
Vector3 pos = GetPlayerPosition();
uint landcell = GetPlayerLandcell();
double ew = Geometry.LandblockToEW(landcell, pos.X);
double ns = Geometry.LandblockToNS(landcell, pos.Y);
return new Coordinates(ew, ns, pos.Z);
}
/* ----------------------------------------------------------
* 3) Generic math helpers you may want later
* -------------------------------------------------------- */
public static double Clamp(double value, double min, double max) =>
value < min ? min : (value > max ? max : value);
public static double DegToRad(double deg) => deg * Math.PI / 180.0;
public static double RadToDeg(double rad) => rad * 180.0 / Math.PI;
}
}

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
<package id="YamlDotNet" version="16.3.0" targetFramework="net48" /> <package id="YamlDotNet" version="16.3.0" targetFramework="net48" />
</packages> </packages>