Compare commits

...

4 commits

Author SHA1 Message Date
erik
fc2575833b Bugfix in PluginSettings. 2025-04-29 20:22:19 +02:00
erik
347cfe6423 New vtank control 2025-04-29 09:58:25 +02:00
erik
de1b72aae5 added readme 2025-04-28 09:28:50 +02:00
erik
b027a79201 Fixed debug 2025-04-27 20:02:47 +02:00
7 changed files with 454 additions and 94 deletions

View file

@ -10,6 +10,7 @@
<RootNamespace>MosswartMassacre</RootNamespace>
<AssemblyName>MosswartMassacre</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<LangVersion>8.0</LangVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
@ -37,6 +38,9 @@
<HintPath>lib\Decal.Adapter.dll</HintPath>
<EmbedInteropTypes>False</EmbedInteropTypes>
</Reference>
<Reference Include="uTank2">
<HintPath>lib\utank2-i.dll</HintPath>
</Reference>
<Reference Include="Decal.Interop.Core, Version=2.9.8.3, Culture=neutral, PublicKeyToken=481f17d392f1fb65, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<EmbedInteropTypes>False</EmbedInteropTypes>
@ -69,6 +73,8 @@
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="vTank.cs" />
<Compile Include="VtankControl.cs" />
<Compile Include="Telemetry.cs" />
<Compile Include="Coordinates.cs" />
<Compile Include="Geometry.cs" />

View file

@ -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;

View file

@ -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<PluginSettings>(yaml);
}
catch (Exception ex)
{
PluginCore.DispatchChatToBoxWithPluginIntercept(
$"[MosswartMassacre] Error reading settings, using defaults: {ex.Message}");
}
}
string yaml = File.ReadAllText(_filePath);
_instance = deserializer.Deserialize<PluginSettings>(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(); }
}
}
}

View file

@ -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
{
/// <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 ========== */
/* ───────────── 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}");
}
}
}
}

View file

@ -0,0 +1,108 @@
using System;
namespace MosswartMassacre
{
/// <summary>
/// Provides helper methods to control VTank from within your plugin.
/// </summary>
public static class VtankControl
{
/// <summary>
/// Sends a chat command to VTank to switch its current meta-state.
/// </summary>
/// <param name="state">
/// The name of the VTank meta-state to activate.
/// </param>
/// <returns>Always returns 1 on sending the command.</returns>
public static double VtSetMetaState(string state)
{
// Dispatch a local chat command that VTank will interpret.
PluginCore.Decal_DispatchOnChatCommand($"/vt setmetastate {state}");
return 1;
}
/// <summary>
/// Queries VTank for its currently active meta-state.
/// </summary>
/// <returns>
/// The name of the current meta-state, or empty string if VTank isnt initialized.
/// </returns>
public static string VtGetMetaState()
{
// Instance.CurrentMetaState is typed as object, so cast it:
return (vTank.Instance.CurrentMetaState as string) ?? string.Empty;
}
/// <summary>
/// Attempts to set a VTank configuration value by name.
/// </summary>
/// <param name="setting">
/// The VTank setting key (e.g. “EnableCombat”, “RingDistance”).
/// </param>
/// <param name="value">
/// The string or numeric value to assign. Numeric strings will be parsed.
/// </param>
/// <returns>
/// 1 if the setting was applied or possibly applied; 0 on known failure.
/// </returns>
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 wasnt parseable—report failure
return 0;
}
}
catch
{
// Swallow any errors and signal failure
return 0;
}
return 1;
}
/// <summary>
/// Reads back a VTank configuration value as a string.
/// </summary>
/// <param name="setting">The name of the setting to read.</param>
/// <returns>
/// The raw string form of the setting, or empty string if undefined.
/// </returns>
public static string VtGetSetting(string setting)
{
var val = vTank.Instance.GetSetting(setting);
return (val as string) ?? string.Empty;
}
/// <summary>
/// Checks whether the VTank macro engine is currently enabled.
/// </summary>
/// <returns>
/// <c>true</c> if macros are active; otherwise <c>false</c>.
/// </returns>
public static bool VtMacroEnabled()
{
return vTank.Instance.MacroEnabled;
}
}
}

116
MosswartMassacre/vTank.cs Normal file
View file

@ -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
{
/// <summary>
/// Helper class for working with the VTank plugin
/// </summary>
public static unsafe class vTank
{
internal static IList ChatQueue = null;
internal static Type ChatType;
/// <summary>
/// The TrustedRelay interface for VTank control
/// </summary>
public static cExternalInterfaceTrustedRelay Instance { get; internal set; }
/// <summary>
/// Current VTank action locks. Key is lock type, Value is when the lock is set to expire.
/// </summary>
public static Dictionary<uTank2.ActionLockType, DateTime> locks = new Dictionary<uTank2.ActionLockType, DateTime>();
/// <summary>
/// Enables VTank helper functionality
/// </summary>
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();
}
}
/// <summary>
/// Disables VTank helper functionality
/// </summary>
public static void Disable()
{
ChatType = null;
ChatQueue = null;
Instance = null;
}
/// <summary>
/// Lock VTank from performing actions. Use Decision_UnLock to cancel.
/// </summary>
/// <param name="actionLockType">the type of action to put a lock on</param>
/// <param name="timeSpan">time to lock vtank for</param>
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;
}
/// <summary>
/// Cancel a VTank lock
/// </summary>
/// <param name="actionLockType">the type of action to unlock</param>
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)
/// <summary>
/// Sends a chat message to VTank so that it will be capturable by metas.
/// </summary>
/// <param name="message">message to send</param>
/// <param name="color">color of the chat text</param>
/// <param name="target">chat window target</param>
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
}
}

80
README.md Normal file
View file

@ -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 projects `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 <enable|disable>` : Start/stop local HTTP command server (port 8085).
- `/mm remotecommands <enable|disable>` : Listen for remote commands from your allegiance chat.
- `/mm telemetry <enable|disable>` : Enable/disable periodic telemetry streaming.
### HTTP Command Server
- Listens on `http://localhost:8085/`.
- Accepts POST data: `target=<player>&command=<text>`, then sends a /tell and executes the command.
### Configuration
- Per-character YAML config stored at `<PluginDir>/<CharacterName>.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._