diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj index ab9bb54..fcb5186 100644 --- a/MosswartMassacre/MosswartMassacre.csproj +++ b/MosswartMassacre/MosswartMassacre.csproj @@ -10,7 +10,6 @@ MosswartMassacre MosswartMassacre v4.8 - 8.0 512 true @@ -38,9 +37,6 @@ lib\Decal.Adapter.dll False - - lib\utank2-i.dll - False False @@ -73,8 +69,6 @@ - - diff --git a/MosswartMassacre/PluginCore.cs b/MosswartMassacre/PluginCore.cs index 4bc0c14..6d1b035 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,8 +52,6 @@ namespace MosswartMassacre // Enable TLS1.2 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; - //Enable vTank interface - vTank.Enable(); } catch (Exception ex) { @@ -84,9 +82,6 @@ namespace MosswartMassacre // Clean up the view MainView.ViewDestroy(); - //Disable vtank interface - vTank.Disable(); - MyHost = null; } catch (Exception ex) @@ -414,7 +409,6 @@ 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": @@ -422,10 +416,6 @@ 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/PluginSettings.cs b/MosswartMassacre/PluginSettings.cs index da0de30..9a72ea4 100644 --- a/MosswartMassacre/PluginSettings.cs +++ b/MosswartMassacre/PluginSettings.cs @@ -10,59 +10,35 @@ 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 bool _telemetryEnabled = false; private string _charTag = "default"; - - public static PluginSettings Instance => _instance - ?? throw new InvalidOperationException("PluginSettings not initialized"); + private bool _telemetryEnabled = false; + public static PluginSettings Instance => _instance; public static void Initialize() { - // determine settings file path string characterName = CoreManager.Current.CharacterFilter.Name; - string pluginFolder = Path.GetDirectoryName( - typeof(PluginSettings).Assembly.Location); + string pluginFolder = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().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)) { - try - { - string yaml = File.ReadAllText(_filePath); - loaded = deserializer.Deserialize(yaml); - } - catch (Exception ex) - { - PluginCore.DispatchChatToBoxWithPluginIntercept( - $"[MosswartMassacre] Error reading settings, using defaults: {ex.Message}"); - } - } + var deserializer = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .Build(); - if (loaded == null) - { - // either file didn't exist, was empty, or deserialized as null - _instance = new PluginSettings(); - Save(); // write out default skeleton + string yaml = File.ReadAllText(_filePath); + _instance = deserializer.Deserialize(yaml); } else { - _instance = loaded; + _instance = new PluginSettings(); + Save(); } - // apply into runtime + // Apply settings to runtime state PluginCore.RareMetaEnabled = _instance.RareMetaEnabled; PluginCore.RemoteCommandsEnabled = _instance.RemoteCommandsEnabled; PluginCore.HttpServerEnabled = _instance.HttpServerEnabled; @@ -72,29 +48,14 @@ namespace MosswartMassacre public static void Save() { - lock (_sync) - { - try - { - var serializer = new SerializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .Build(); - var yaml = serializer.Serialize(_instance); + var serializer = new SerializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .Build(); - // 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}"); - } - } + string yaml = serializer.Serialize(_instance); + File.WriteAllText(_filePath, yaml); } - // public properties public bool RemoteCommandsEnabled { get => _remoteCommandsEnabled; @@ -112,17 +73,15 @@ namespace MosswartMassacre get => _httpServerEnabled; set { _httpServerEnabled = value; Save(); } } - - public bool TelemetryEnabled - { - get => _telemetryEnabled; - set { _telemetryEnabled = value; Save(); } - } - public string CharTag { get => _charTag; set { _charTag = value; Save(); } } + public bool TelemetryEnabled + { + get => _telemetryEnabled; + set { _telemetryEnabled = value; Save(); } + } } } diff --git a/MosswartMassacre/Telemetry.cs b/MosswartMassacre/Telemetry.cs index ce9bb79..dc57ec3 100644 --- a/MosswartMassacre/Telemetry.cs +++ b/MosswartMassacre/Telemetry.cs @@ -1,109 +1,110 @@ -// Telemetry.cs ─────────────────────────────────────────────────────────────── -using System; +using System; using System.Net.Http; using System.Text; -using System.Threading; -using System.Threading.Tasks; +using System.Timers; 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 { - /* ───────────── 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 + /* ============ 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 ========== */ - /* ───────────── runtime state ───────────── */ private static readonly HttpClient _http = new HttpClient(); - private static string _sessionId; - private static CancellationTokenSource _cts; + private static Timer _timer; private static bool _enabled; + private static string _sessionId; + + /* ============ public API ========= */ - /* ───────────── public API ───────────── */ public static void Start() { - if (_enabled) return; + 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; - _sessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}"; - _cts = new CancellationTokenSource(); - PluginCore.WriteToChat("[Telemetry] HTTP streaming ENABLED"); - - _ = Task.Run(() => LoopAsync(_cts.Token)); // fire-and-forget + PluginCore.WriteToChat("[Tel] timer every " + IntervalSec + " s"); } public static void Stop() { if (!_enabled) return; - _cts.Cancel(); + _enabled = false; + _timer?.Stop(); + _timer?.Dispose(); + _timer = null; + PluginCore.WriteToChat("[Telemetry] HTTP streaming DISABLED"); } - /* ───────────── async loop ───────────── */ - private static async Task LoopAsync(CancellationToken token) - { - while (!token.IsCancellationRequested) - { - try - { - await SendSnapshotAsync(token); - } - catch (Exception ex) - { - PluginCore.WriteToChat($"[Telemetry] send failed: {ex.Message}"); - } + /* ============ snapshot builder === */ - try + private static async void SendSnapshot() + { + try + { + var coords = Coordinates.Me; + + var payload = new { - await Task.Delay(TimeSpan.FromSeconds(IntervalSec), token); + 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}"); } - catch (TaskCanceledException) { } // expected on Stop() + 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}"); } } - /* ───────────── 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, - 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 = VtankControl.VtGetMetaState(), - }; - - 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}"); - } - } } } diff --git a/MosswartMassacre/VtankControl.cs b/MosswartMassacre/VtankControl.cs deleted file mode 100644 index 686dcbb..0000000 --- a/MosswartMassacre/VtankControl.cs +++ /dev/null @@ -1,108 +0,0 @@ -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 deleted file mode 100644 index ac95253..0000000 --- a/MosswartMassacre/vTank.cs +++ /dev/null @@ -1,116 +0,0 @@ -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 - } -} diff --git a/README.md b/README.md deleted file mode 100644 index 16858c3..0000000 --- a/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# 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