From b027a792018b9f29bebbdb733a316099428b7355 Mon Sep 17 00:00:00 2001 From: erik Date: Sun, 27 Apr 2025 20:02:47 +0200 Subject: [PATCH 1/4] Fixed debug --- MosswartMassacre/MosswartMassacre.csproj | 1 + MosswartMassacre/Telemetry.cs | 141 +++++++++++------------ 2 files changed, 70 insertions(+), 72 deletions(-) diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj index fcb5186..6bdba56 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -10,6 +10,7 @@ MosswartMassacre MosswartMassacre v4.8 + 8.0 512 true diff --git a/MosswartMassacre/Telemetry.cs b/MosswartMassacre/Telemetry.cs index dc57ec3..7facf99 100644 --- a/MosswartMassacre/Telemetry.cs +++ b/MosswartMassacre/Telemetry.cs @@ -1,110 +1,107 @@ -using System; +// Telemetry.cs ─────────────────────────────────────────────────────────────── +using System; using System.Net.Http; using System.Text; -using System.Timers; +using System.Threading; +using System.Threading.Tasks; using Decal.Adapter; using Newtonsoft.Json; namespace MosswartMassacre { - /// - /// Periodically sends gameplay telemetry to your FastAPI collector. - /// Toggle with: Telemetry.Start() / Telemetry.Stop() - /// 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 ========== */ + /* ───────────── configuration ───────────── */ + private const string Endpoint = "https://mosswart.snakedesert.se/position/"; // <- trailing slash! + private const string SharedSecret = "your_shared_secret"; // <- keep in sync + private const int IntervalSec = 5; // seconds between posts + /* ───────────── runtime state ───────────── */ private static readonly HttpClient _http = new HttpClient(); - private static Timer _timer; - private static bool _enabled; private static string _sessionId; + private static CancellationTokenSource _cts; + private static bool _enabled; - /* ============ public API ========= */ - + /* ───────────── 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(); + if (_enabled) return; _enabled = true; + _sessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}"; + _cts = new CancellationTokenSource(); + PluginCore.WriteToChat("[Telemetry] HTTP streaming ENABLED"); - PluginCore.WriteToChat("[Tel] timer every " + IntervalSec + " s"); + + _ = Task.Run(() => LoopAsync(_cts.Token)); // fire-and-forget } public static void Stop() { if (!_enabled) return; - + _cts.Cancel(); _enabled = false; - _timer?.Stop(); - _timer?.Dispose(); - _timer = null; - PluginCore.WriteToChat("[Telemetry] HTTP streaming DISABLED"); } - /* ============ snapshot builder === */ - - private static async void SendSnapshot() + /* ───────────── async loop ───────────── */ + private static async Task LoopAsync(CancellationToken token) { - try + while (!token.IsCancellationRequested) { - var coords = Coordinates.Me; - - var payload = new + try { - 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}"); + await SendSnapshotAsync(token); } - else + catch (Exception ex) { - PluginCore.WriteToChat($"[Tel] ✗ {resp.StatusCode} ({await resp.Content.ReadAsStringAsync()})"); + PluginCore.WriteToChat($"[Telemetry] send failed: {ex.Message}"); } - } - catch (Exception ex) - { - var inner = ex.InnerException?.Message ?? "no inner msg"; - PluginCore.WriteToChat($"[Tel] FAILED — {ex.GetType().Name}: {ex.Message} ⇢ {inner}"); + + try + { + await Task.Delay(TimeSpan.FromSeconds(IntervalSec), token); + } + catch (TaskCanceledException) { } // expected on Stop() } } + /* ───────────── single POST ───────────── */ + private static async Task SendSnapshotAsync(CancellationToken token) + { + 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); + + using var resp = await _http.SendAsync(req, token); + + if (!resp.IsSuccessStatusCode) // stay quiet on success + { + PluginCore.WriteToChat($"[Telemetry] server replied {resp.StatusCode}"); + } + } } } From de1b72aae50894ee9325932f60a148187838f506 Mon Sep 17 00:00:00 2001 From: erik Date: Mon, 28 Apr 2025 09:28:50 +0200 Subject: [PATCH 2/4] added readme --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..16858c3 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Mossy Plugins + +A collection of DECAL plugins for Asheron's Call, providing utility overlays and automation features. + + ## Contents + - `mossy.sln`: Visual Studio solution containing both projects. + - `GearCycler/`: Simple plugin with a UI button to cycle gear (placeholder behavior). + - `MosswartMassacre/`: Advanced plugin tracking monster kills, rare discoveries, and offering HTTP/telemetry features. + - `packages/`: Vendored NuGet packages (Newtonsoft.Json, YamlDotNet). + + ## Prerequisites + - Windows with .NET Framework 4.8 + - Visual Studio 2017+ (MSBuild Tools 15.0) or equivalent MSBuild environment + - DECAL Adapter installed for Asheron's Call + - VirindiViewService (included in each project's `lib/` folder) + + ## Setup & Build + 1. Clone this repository. + 2. Ensure the DECAL and Virindi DLLs are present under `MosswartMassacre/lib/` and referenced by each project. + 3. Restore NuGet packages if needed (`nuget restore mossy.sln`). + 4. Open `mossy.sln` in Visual Studio and build the solution. + 5. The output DLLs will be in each project’s `bin/Debug/` or `bin/Release/` folder. + 6. Deploy the plugin DLLs (and any required XML or YAML files) to your DECAL plugin directory. + + ## GearCycler + A minimal plugin demonstrating a VirindiViewService-based UI. + - UI layout: `GearCycler/ViewXML/mainView.xml`. + - Core logic in `GearCycler/GearCore.cs`. + - On button click, it logs a chat message; extend the `btnCycle.Hit` handler to add gear-cycling logic. + + ## MosswartMassacre + Tracks monster kills and rare drops, with multiple utility features. + + ### Features + - **Kill Tracking**: Counts total kills and computes rates (kills/5 min, kills/hour). + - **Rare Discoveries**: Increments rare count and can automatically set rare meta state. + - **UI Overlay**: Displays stats and provides buttons to reset stats or toggle rare meta. + - **Command Interface** (`/mm` commands): + - `/mm help` : Show available commands. + - `/mm report` : Display current stats in chat. + - `/mm loc` : Show current map coordinates. + - `/mm reset` : Reset kill counters and timers. + - `/mm meta` : Toggle automatic rare meta state. + - `/mm http ` : Start/stop local HTTP command server (port 8085). + - `/mm remotecommands ` : Listen for remote commands from your allegiance chat. + - `/mm telemetry ` : Enable/disable periodic telemetry streaming. + + ### HTTP Command Server + - Listens on `http://localhost:8085/`. + - Accepts POST data: `target=&command=`, then sends a /tell and executes the command. + + ### Configuration + - Per-character YAML config stored at `/.yaml`. + - Settings include: + - `remote_commands_enabled` + - `rare_meta_enabled` + - `http_server_enabled` + - `telemetry_enabled` + - `char_tag` + - Config is auto-generated on first run; modify it or use UI/commands to update. + + ### Telemetry + - Periodically posts JSON snapshots of position and stats to a configurable endpoint. + - Configure `Endpoint`, `SharedSecret`, and `IntervalSec` in `Telemetry.cs`. + + ## Dependencies + - Decal.Adapter (v2.9.8.3) + - Decal.Interop.Core & Decal.Interop.Inject + - VirindiViewService + - Newtonsoft.Json (v13.0.3) + - YamlDotNet (v16.3.0) + + ## Contributing + 1. Fork the repository. + 2. Create a feature branch. + 3. Commit your changes and ensure the solution builds. + 4. Submit a pull request with a description of your changes. + + -- + _This README provides a high-level overview to get up and running quickly._ \ No newline at end of file From 347cfe6423010f8a324cba0592554902ea03d6c0 Mon Sep 17 00:00:00 2001 From: erik Date: Tue, 29 Apr 2025 09:58:25 +0200 Subject: [PATCH 3/4] New vtank control --- MosswartMassacre/MosswartMassacre.csproj | 7 +- MosswartMassacre/PluginCore.cs | 12 ++- MosswartMassacre/Telemetry.cs | 4 +- MosswartMassacre/VtankControl.cs | 108 +++++++++++++++++++++ MosswartMassacre/vTank.cs | 116 +++++++++++++++++++++++ 5 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 MosswartMassacre/VtankControl.cs create mode 100644 MosswartMassacre/vTank.cs diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj index 6bdba56..ab9bb54 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -10,7 +10,7 @@ MosswartMassacre MosswartMassacre v4.8 - 8.0 + 8.0 512 true @@ -38,6 +38,9 @@ lib\Decal.Adapter.dll False + + lib\utank2-i.dll + False False @@ -70,6 +73,8 @@ + + diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs index 6d1b035..4bc0c14 100644 --- a/MosswartMassacre/PluginCore.cs +++ b/MosswartMassacre/PluginCore.cs @@ -34,7 +34,7 @@ namespace MosswartMassacre try { MyHost = Host; - + WriteToChat("Mosswart Massacre has started!"); // Subscribe to chat message event @@ -52,6 +52,8 @@ namespace MosswartMassacre // Enable TLS1.2 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; + //Enable vTank interface + vTank.Enable(); } catch (Exception ex) { @@ -82,6 +84,9 @@ namespace MosswartMassacre // Clean up the view MainView.ViewDestroy(); + //Disable vtank interface + vTank.Disable(); + MyHost = null; } catch (Exception ex) @@ -409,6 +414,7 @@ namespace MosswartMassacre 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"); + WriteToChat("/mm getmetastate - Gets the current metastate"); break; case "report": @@ -416,6 +422,10 @@ namespace MosswartMassacre string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}"; WriteToChat(reportMessage); break; + case "getmetastate": + string metaState = VtankControl.VtGetMetaState(); + WriteToChat(metaState); + break; case "loc": Coordinates here = Coordinates.Me; diff --git a/MosswartMassacre/Telemetry.cs b/MosswartMassacre/Telemetry.cs index 7facf99..ce9bb79 100644 --- a/MosswartMassacre/Telemetry.cs +++ b/MosswartMassacre/Telemetry.cs @@ -83,10 +83,12 @@ namespace MosswartMassacre z = coords.Z, kills = PluginCore.totalKills, + onlinetime = (DateTime.Now - PluginCore.statsStartTime).ToString(@"dd\.hh\:mm\:ss"), + kills_per_hour = PluginCore.killsPerHour.ToString("F0"), deaths = 0, rares_found = PluginCore.rareCount, prismatic_taper_count = 0, - vt_state = "Unknown" + vt_state = VtankControl.VtGetMetaState(), }; string json = JsonConvert.SerializeObject(payload); diff --git a/MosswartMassacre/VtankControl.cs b/MosswartMassacre/VtankControl.cs new file mode 100644 index 0000000..686dcbb --- /dev/null +++ b/MosswartMassacre/VtankControl.cs @@ -0,0 +1,108 @@ +using System; + +namespace MosswartMassacre +{ + /// + /// Provides helper methods to control VTank from within your plugin. + /// + public static class VtankControl + { + /// + /// Sends a chat command to VTank to switch its current meta-state. + /// + /// + /// The name of the VTank meta-state to activate. + /// + /// Always returns 1 on sending the command. + public static double VtSetMetaState(string state) + { + // Dispatch a local chat command that VTank will interpret. + PluginCore.Decal_DispatchOnChatCommand($"/vt setmetastate {state}"); + return 1; + } + + /// + /// Queries VTank for its currently active meta-state. + /// + /// + /// The name of the current meta-state, or empty string if VTank isn’t initialized. + /// + public static string VtGetMetaState() + { + // Instance.CurrentMetaState is typed as object, so cast it: + return (vTank.Instance.CurrentMetaState as string) ?? string.Empty; + } + + /// + /// Attempts to set a VTank configuration value by name. + /// + /// + /// The VTank setting key (e.g. “EnableCombat”, “RingDistance”). + /// + /// + /// The string or numeric value to assign. Numeric strings will be parsed. + /// + /// + /// 1 if the setting was applied or possibly applied; 0 on known failure. + /// + public static double VtSetSetting(string setting, string value) + { + try + { + var settingType = vTank.Instance.GetSettingType(setting); + + if (settingType == typeof(string)) + { + vTank.Instance.SetSetting(setting, value); + } + else if (double.TryParse(value, out double number)) + { + if (settingType == typeof(bool)) + vTank.Instance.SetSetting(setting, number == 1); + else if (settingType == typeof(double)) + vTank.Instance.SetSetting(setting, number); + else if (settingType == typeof(int)) + vTank.Instance.SetSetting(setting, Convert.ToInt32(number)); + else if (settingType == typeof(float)) + vTank.Instance.SetSetting(setting, Convert.ToSingle(number)); + } + else + { + // Value wasn’t parseable—report failure + return 0; + } + } + catch + { + // Swallow any errors and signal failure + return 0; + } + + return 1; + } + + /// + /// Reads back a VTank configuration value as a string. + /// + /// The name of the setting to read. + /// + /// The raw string form of the setting, or empty string if undefined. + /// + public static string VtGetSetting(string setting) + { + var val = vTank.Instance.GetSetting(setting); + return (val as string) ?? string.Empty; + } + + /// + /// Checks whether the VTank macro engine is currently enabled. + /// + /// + /// true if macros are active; otherwise false. + /// + public static bool VtMacroEnabled() + { + return vTank.Instance.MacroEnabled; + } + } +} diff --git a/MosswartMassacre/vTank.cs b/MosswartMassacre/vTank.cs new file mode 100644 index 0000000..ac95253 --- /dev/null +++ b/MosswartMassacre/vTank.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using uTank2; +using static uTank2.PluginCore; + +namespace MosswartMassacre +{ + /// + /// Helper class for working with the VTank plugin + /// + public static unsafe class vTank + { + internal static IList ChatQueue = null; + internal static Type ChatType; + + /// + /// The TrustedRelay interface for VTank control + /// + public static cExternalInterfaceTrustedRelay Instance { get; internal set; } + + /// + /// Current VTank action locks. Key is lock type, Value is when the lock is set to expire. + /// + public static Dictionary locks = new Dictionary(); + + /// + /// Enables VTank helper functionality + /// + public static void Enable() + { + foreach (uTank2.ActionLockType ty in Enum.GetValues(typeof(uTank2.ActionLockType))) + locks[ty] = DateTime.MinValue; + + try + { + ConstructorInfo ctor = typeof(cExternalInterfaceTrustedRelay) + .GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic)[0]; + Instance = (cExternalInterfaceTrustedRelay)ctor.Invoke(new object[] { eExternalsPermissionLevel.None }); + + FieldInfo fieldInfo = Instance.GetType() + .GetField("a", BindingFlags.NonPublic | BindingFlags.Instance); + fieldInfo.SetValue(Instance, 15); + + Type vTankChatHandler = typeof(uTank2.PluginCore).Assembly.GetType("a7"); + FieldInfo vTankChatList = vTankChatHandler + .GetField("a", BindingFlags.NonPublic | BindingFlags.Static); + ChatType = vTankChatHandler.GetNestedType("a"); + ChatQueue = (IList)(vTankChatList.GetValue(null)); + } + catch + { + Disable(); + } + } + + /// + /// Disables VTank helper functionality + /// + public static void Disable() + { + ChatType = null; + ChatQueue = null; + Instance = null; + } + + /// + /// Lock VTank from performing actions. Use Decision_UnLock to cancel. + /// + /// the type of action to put a lock on + /// time to lock vtank for + public static void Decision_Lock(uTank2.ActionLockType actionLockType, TimeSpan timeSpan) + { + Instance?.Decision_Lock(actionLockType, timeSpan); + DateTime newExp = DateTime.UtcNow + timeSpan; + if (locks[actionLockType] < newExp) locks[actionLockType] = newExp; + } + + /// + /// Cancel a VTank lock + /// + /// the type of action to unlock + public static void Decision_UnLock(uTank2.ActionLockType actionLockType) + { + Instance?.Decision_UnLock(actionLockType); + locks[actionLockType] = DateTime.MinValue; + } + + #region Tell(string message, int color = 0, int target = 0) + /// + /// Sends a chat message to VTank so that it will be capturable by metas. + /// + /// message to send + /// color of the chat text + /// chat window target + public static void Tell(string message, int color = 0, int target = 0) + { + if (ChatQueue != null) + { + object newA = Activator.CreateInstance(ChatType); + ChatType.GetField("a").SetValue(newA, message); // message + ChatType.GetField("b").SetValue(newA, color); // color + ChatType.GetField("c").SetValue(newA, target); // target + try + { + ChatQueue.Add(newA); + } + catch { } + } + } + #endregion + } +} From fc2575833b5b434c9c8d74c6144f32484b4c1dea Mon Sep 17 00:00:00 2001 From: erik Date: Tue, 29 Apr 2025 20:22:19 +0200 Subject: [PATCH 4/4] Bugfix in PluginSettings. --- MosswartMassacre/PluginSettings.cs | 83 ++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/MosswartMassacre/PluginSettings.cs b/MosswartMassacre/PluginSettings.cs index 9a72ea4..da0de30 100644 --- a/MosswartMassacre/PluginSettings.cs +++ b/MosswartMassacre/PluginSettings.cs @@ -10,35 +10,59 @@ namespace MosswartMassacre { private static PluginSettings _instance; private static string _filePath; + private static readonly object _sync = new object(); + + // backing fields private bool _remoteCommandsEnabled = false; private bool _rareMetaEnabled = true; private bool _httpServerEnabled = false; - private string _charTag = "default"; private bool _telemetryEnabled = false; - public static PluginSettings Instance => _instance; + private string _charTag = "default"; + + public static PluginSettings Instance => _instance + ?? throw new InvalidOperationException("PluginSettings not initialized"); public static void Initialize() { + // determine settings file path string characterName = CoreManager.Current.CharacterFilter.Name; - string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + string pluginFolder = Path.GetDirectoryName( + typeof(PluginSettings).Assembly.Location); _filePath = Path.Combine(pluginFolder, $"{characterName}.yaml"); + // build serializer/deserializer once + var builder = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance); + var deserializer = builder.Build(); + + PluginSettings loaded = null; + if (File.Exists(_filePath)) { - var deserializer = new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .Build(); + try + { + string yaml = File.ReadAllText(_filePath); + loaded = deserializer.Deserialize(yaml); + } + catch (Exception ex) + { + PluginCore.DispatchChatToBoxWithPluginIntercept( + $"[MosswartMassacre] Error reading settings, using defaults: {ex.Message}"); + } + } - string yaml = File.ReadAllText(_filePath); - _instance = deserializer.Deserialize(yaml); + if (loaded == null) + { + // either file didn't exist, was empty, or deserialized as null + _instance = new PluginSettings(); + Save(); // write out default skeleton } else { - _instance = new PluginSettings(); - Save(); + _instance = loaded; } - // Apply settings to runtime state + // apply into runtime PluginCore.RareMetaEnabled = _instance.RareMetaEnabled; PluginCore.RemoteCommandsEnabled = _instance.RemoteCommandsEnabled; PluginCore.HttpServerEnabled = _instance.HttpServerEnabled; @@ -48,14 +72,29 @@ namespace MosswartMassacre public static void Save() { - var serializer = new SerializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .Build(); + lock (_sync) + { + try + { + var serializer = new SerializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .Build(); + var yaml = serializer.Serialize(_instance); - string yaml = serializer.Serialize(_instance); - File.WriteAllText(_filePath, yaml); + // atomic write: write to .tmp then replace + var temp = _filePath + ".tmp"; + File.WriteAllText(temp, yaml); + File.Replace(temp, _filePath, null); + } + catch (Exception ex) + { + PluginCore.DispatchChatToBoxWithPluginIntercept( + $"[MosswartMassacre] Error saving settings: {ex.Message}"); + } + } } + // public properties public bool RemoteCommandsEnabled { get => _remoteCommandsEnabled; @@ -73,15 +112,17 @@ namespace MosswartMassacre get => _httpServerEnabled; set { _httpServerEnabled = value; Save(); } } - public string CharTag - { - get => _charTag; - set { _charTag = value; Save(); } - } + public bool TelemetryEnabled { get => _telemetryEnabled; set { _telemetryEnabled = value; Save(); } } + + public string CharTag + { + get => _charTag; + set { _charTag = value; Save(); } + } } }