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