diff --git a/MosswartMassacre/MosswartMassacre.csproj b/MosswartMassacre/MosswartMassacre.csproj
index fcb5186..ab9bb54 100644
--- a/MosswartMassacre/MosswartMassacre.csproj
+++ b/MosswartMassacre/MosswartMassacre.csproj
@@ -10,6 +10,7 @@
MosswartMassacre
MosswartMassacre
v4.8
+ 8.0
512
true
@@ -37,6 +38,9 @@
lib\Decal.Adapter.dll
False
+
+ lib\utank2-i.dll
+
False
False
@@ -69,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/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(); }
+ }
}
}
diff --git a/MosswartMassacre/Telemetry.cs b/MosswartMassacre/Telemetry.cs
index dc57ec3..ce9bb79 100644
--- a/MosswartMassacre/Telemetry.cs
+++ b/MosswartMassacre/Telemetry.cs
@@ -1,110 +1,109 @@
-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,
+ 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
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
+ }
+}
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