Compare commits
7 commits
master
...
awesome-fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61c74be07e | ||
|
|
aa744a8edc | ||
|
|
aede9d8bdd | ||
|
|
4e2bb68e86 | ||
|
|
2e12766f8b | ||
|
|
60d06341dd | ||
|
|
d2e9988bdd |
15 changed files with 850 additions and 495 deletions
140
MosswartMassacre/ChatManager.cs
Normal file
140
MosswartMassacre/ChatManager.cs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Decal.Adapter;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles game chat events: kill/rare detection, forwarding chat to WebSocket, and "/mm" command parsing.
|
||||
/// </summary>
|
||||
internal class ChatManager : IDisposable
|
||||
{
|
||||
private readonly StatsManager _statsManager;
|
||||
private readonly IWebSocketService _wsService;
|
||||
private readonly Action<string> _mmCommandHandler;
|
||||
private bool _isStarted;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new ChatManager.
|
||||
/// </summary>
|
||||
/// <param name="statsManager">Stats manager for registering kills/rares.</param>
|
||||
/// <param name="mmCommandHandler">Delegate to handle '/mm' commands.</param>
|
||||
public ChatManager(StatsManager statsManager, IWebSocketService wsService, Action<string> mmCommandHandler)
|
||||
{
|
||||
_statsManager = statsManager ?? throw new ArgumentNullException(nameof(statsManager));
|
||||
_wsService = wsService ?? throw new ArgumentNullException(nameof(wsService));
|
||||
_mmCommandHandler = mmCommandHandler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribes to chat and command events.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
if (_isStarted) return;
|
||||
_isStarted = true;
|
||||
CoreManager.Current.ChatBoxMessage += OnChatIntercept;
|
||||
CoreManager.Current.CommandLineText += OnChatCommand;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribes from chat and command events.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (!_isStarted) return;
|
||||
CoreManager.Current.ChatBoxMessage -= OnChatIntercept;
|
||||
CoreManager.Current.CommandLineText -= OnChatCommand;
|
||||
_isStarted = false;
|
||||
}
|
||||
|
||||
private void OnChatIntercept(object sender, ChatTextInterceptEventArgs e)
|
||||
{
|
||||
// Forward chat text asynchronously to WebSocket
|
||||
try
|
||||
{
|
||||
string cleaned = NormalizeChatLine(e.Text);
|
||||
_ = _wsService.SendChatTextAsync(e.Color, cleaned);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat("Error sending chat over WS: " + ex.Message);
|
||||
}
|
||||
|
||||
// Kill detection
|
||||
try
|
||||
{
|
||||
if (PluginCore.IsKilledByMeMessage(e.Text))
|
||||
{
|
||||
_statsManager.RegisterKill();
|
||||
}
|
||||
// Rare discovery detection
|
||||
if (PluginCore.IsRareDiscoveryMessage(e.Text, out string rareText))
|
||||
{
|
||||
_statsManager.RegisterRare();
|
||||
if (PluginSettings.Instance.RareMetaEnabled)
|
||||
PluginCore.Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
|
||||
DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
|
||||
}
|
||||
// Remote commands from allegiance
|
||||
if (PluginSettings.Instance.RemoteCommandsEnabled && e.Color == 18)
|
||||
{
|
||||
string characterName = Regex.Escape(CoreManager.Current.CharacterFilter.Name);
|
||||
string pattern = $"^\\[Allegiance\\].*Dunking Rares.*say[s]?, \\\"!do {characterName} (?<command>.+)\\\"$";
|
||||
string tag = Regex.Escape(PluginCore.CharTag);
|
||||
string patterntag = $"^\\[Allegiance\\].*Dunking Rares.*say[s]?, \\\"!dot {tag} (?<command>.+)\\\"$";
|
||||
var match = Regex.Match(e.Text, pattern);
|
||||
var matchtag = Regex.Match(e.Text, patterntag);
|
||||
if (match.Success)
|
||||
{
|
||||
string command = match.Groups["command"].Value;
|
||||
PluginCore.Decal_DispatchOnChatCommand(command);
|
||||
DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
|
||||
}
|
||||
else if (matchtag.Success)
|
||||
{
|
||||
string command = matchtag.Groups["command"].Value;
|
||||
PluginCore.Decal_DispatchOnChatCommand(command);
|
||||
DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat("Error processing chat message: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChatCommand(object sender, ChatParserInterceptEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Text.StartsWith("/mm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
e.Eat = true;
|
||||
_mmCommandHandler?.Invoke(e.Text);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat("[Error] Failed to process /mm command: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeChatLine(string raw)
|
||||
{
|
||||
if (string.IsNullOrEmpty(raw)) return raw;
|
||||
// Remove tags
|
||||
string noTags = Regex.Replace(raw, "<[^>]+>", "");
|
||||
// Trim newlines
|
||||
string trimmed = noTags.TrimEnd('\r', '\n');
|
||||
// Collapse spaces
|
||||
return Regex.Replace(trimmed, "[ ]{2,}", " ");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
137
MosswartMassacre/CommandHandler.cs
Normal file
137
MosswartMassacre/CommandHandler.cs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
using Decal.Adapter;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles "/mm" commands from chat.
|
||||
/// </summary>
|
||||
public class CommandHandler : ICommandHandler
|
||||
{
|
||||
private readonly StatsManager _statsManager;
|
||||
private readonly IWebSocketService _wsService;
|
||||
|
||||
public CommandHandler(StatsManager statsManager, IWebSocketService wsService)
|
||||
{
|
||||
_statsManager = statsManager ?? throw new ArgumentNullException(nameof(statsManager));
|
||||
_wsService = wsService ?? throw new ArgumentNullException(nameof(wsService));
|
||||
}
|
||||
|
||||
public void Handle(string commandText)
|
||||
{
|
||||
// Remove the /mm prefix and trim
|
||||
string[] args = commandText.Length > 3
|
||||
? commandText.Substring(3).Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
: Array.Empty<string>();
|
||||
if (args.Length == 0)
|
||||
{
|
||||
PluginCore.WriteToChat("Usage: /mm <command>. Try /mm help");
|
||||
return;
|
||||
}
|
||||
|
||||
string sub = args[0].ToLowerInvariant();
|
||||
switch (sub)
|
||||
{
|
||||
case "ws":
|
||||
if (args.Length > 1)
|
||||
{
|
||||
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_wsService.Start();
|
||||
PluginSettings.Instance.WebSocketEnabled = true;
|
||||
PluginCore.WriteToChat("WS streaming ENABLED.");
|
||||
}
|
||||
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_wsService.Stop();
|
||||
PluginSettings.Instance.WebSocketEnabled = false;
|
||||
PluginCore.WriteToChat("WS streaming DISABLED.");
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat("Usage: /mm ws <enable|disable>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat("Usage: /mm ws <enable|disable>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "help":
|
||||
PluginCore.WriteToChat("Mosswart Massacre Commands:");
|
||||
PluginCore.WriteToChat("/mm report - Show current stats");
|
||||
PluginCore.WriteToChat("/mm loc - Show current location");
|
||||
PluginCore.WriteToChat("/mm ws - Websocket streaming enable|disable");
|
||||
PluginCore.WriteToChat("/mm reset - Reset all counters");
|
||||
PluginCore.WriteToChat("/mm meta - Toggle rare meta state");
|
||||
PluginCore.WriteToChat("/mm http - Local http-command server enable|disable");
|
||||
PluginCore.WriteToChat("/mm remotecommand - Listen to allegiance !do/!dot enable|disable");
|
||||
PluginCore.WriteToChat("/mm getmetastate - Gets the current metastate");
|
||||
break;
|
||||
|
||||
case "report":
|
||||
var elapsed = DateTime.Now - _statsManager.StatsStartTime;
|
||||
var report = $"Total Kills: {_statsManager.TotalKills}, Kills per Hour: {_statsManager.KillsPerHour:F2}, Elapsed Time: {elapsed:dd.hh:mm:ss}, Rares Found: {_statsManager.RareCount}";
|
||||
PluginCore.WriteToChat(report);
|
||||
break;
|
||||
|
||||
case "getmetastate":
|
||||
var state = VtankControl.VtGetMetaState();
|
||||
PluginCore.WriteToChat(state);
|
||||
break;
|
||||
|
||||
case "loc":
|
||||
var here = Coordinates.Me;
|
||||
var pos = Utils.GetPlayerPosition();
|
||||
PluginCore.WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
|
||||
break;
|
||||
|
||||
case "reset":
|
||||
_statsManager.Restart();
|
||||
PluginCore.WriteToChat("Stats have been reset.");
|
||||
break;
|
||||
|
||||
case "meta":
|
||||
PluginSettings.Instance.RareMetaEnabled = !PluginSettings.Instance.RareMetaEnabled;
|
||||
PluginCore.WriteToChat($"Rare meta state is now {(PluginSettings.Instance.RareMetaEnabled ? "ON" : "OFF")}");
|
||||
MainView.SetRareMetaToggleState(PluginSettings.Instance.RareMetaEnabled);
|
||||
break;
|
||||
|
||||
case "http":
|
||||
if (args.Length > 1)
|
||||
{
|
||||
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PluginSettings.Instance.HttpServerEnabled = true;
|
||||
HttpCommandServer.Start();
|
||||
}
|
||||
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PluginSettings.Instance.HttpServerEnabled = false;
|
||||
HttpCommandServer.Stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat("Usage: /mm http <enable|disable>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
PluginCore.WriteToChat("Usage: /mm http <enable|disable>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "remotecommands":
|
||||
PluginSettings.Instance.RemoteCommandsEnabled = !PluginSettings.Instance.RemoteCommandsEnabled;
|
||||
PluginCore.WriteToChat($"Remote command listening is now {(PluginSettings.Instance.RemoteCommandsEnabled ? "ENABLED" : "DISABLED")}.");
|
||||
break;
|
||||
|
||||
default:
|
||||
PluginCore.WriteToChat($"Unknown /mm command: {sub}. Try /mm help");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,18 @@ namespace MosswartMassacre
|
|||
PluginCore.WriteToChat("Error in delayed command system: " + ex.Message);
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Shutdown the delayed command system: unsubscribe from RenderFrame and clear pending commands.
|
||||
/// </summary>
|
||||
public static void Shutdown()
|
||||
{
|
||||
if (isDelayListening)
|
||||
{
|
||||
CoreManager.Current.RenderFrame -= Core_RenderFrame_Delay;
|
||||
isDelayListening = false;
|
||||
}
|
||||
delayedCommands.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public class DelayedCommand
|
||||
|
|
|
|||
14
MosswartMassacre/ICommandHandler.cs
Normal file
14
MosswartMassacre/ICommandHandler.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles plugin commands (e.g., /mm commands).
|
||||
/// </summary>
|
||||
public interface ICommandHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes a command text, e.g. "/mm report".
|
||||
/// </summary>
|
||||
/// <param name="commandText">The full command text.</param>
|
||||
void Handle(string commandText);
|
||||
}
|
||||
}
|
||||
18
MosswartMassacre/IStreamService.cs
Normal file
18
MosswartMassacre/IStreamService.cs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a start/stop service with life-cycle management.
|
||||
/// </summary>
|
||||
public interface IStreamService : System.IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Starts the service.
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the service.
|
||||
/// </summary>
|
||||
void Stop();
|
||||
}
|
||||
}
|
||||
23
MosswartMassacre/IWebSocketService.cs
Normal file
23
MosswartMassacre/IWebSocketService.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Service for WebSocket streaming and command handling.
|
||||
/// </summary>
|
||||
public interface IWebSocketService : IStreamService
|
||||
{
|
||||
/// <summary>
|
||||
/// Fires when a valid command envelope arrives from the server for this character.
|
||||
/// </summary>
|
||||
event Action<CommandEnvelope> OnServerCommand;
|
||||
|
||||
/// <summary>
|
||||
/// Sends chat text asynchronously over the WebSocket.
|
||||
/// </summary>
|
||||
/// <param name="colorIndex">Chat color index.</param>
|
||||
/// <param name="chatText">Text to send.</param>
|
||||
Task SendChatTextAsync(int colorIndex, string chatText);
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,9 @@
|
|||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
<HintPath>lib\Decal.Interop.Inject.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="System.Configuration">
|
||||
<HintPath>$(FrameworkPathOverride)\System.Configuration.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
|
|
@ -76,7 +79,6 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="vTank.cs" />
|
||||
<Compile Include="VtankControl.cs" />
|
||||
<Compile Include="Telemetry.cs" />
|
||||
<Compile Include="Coordinates.cs" />
|
||||
<Compile Include="Geometry.cs" />
|
||||
<Compile Include="Utils.cs" />
|
||||
|
|
@ -92,6 +94,14 @@
|
|||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="ViewSystemSelector.cs" />
|
||||
<!-- WebSocket.cs replaced by WebSocketService -->
|
||||
<Compile Include="StatsManager.cs" />
|
||||
<Compile Include="ChatManager.cs" />
|
||||
<Compile Include="ICommandHandler.cs" />
|
||||
<Compile Include="CommandHandler.cs" />
|
||||
<Compile Include="IStreamService.cs" />
|
||||
<Compile Include="IWebSocketService.cs" />
|
||||
<Compile Include="WebSocketService.cs" />
|
||||
<Compile Include="Wrapper.cs" />
|
||||
<Compile Include="Wrapper_Decal.cs" />
|
||||
<Compile Include="Wrapper_MyHuds.cs" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Runtime.InteropServices;
|
||||
|
|
@ -9,25 +9,76 @@ using Decal.Adapter.Wrappers;
|
|||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
[ComVisible(true)]
|
||||
[Guid("be0a51b6-cf1f-4318-ad49-e40d6aebe14b")]
|
||||
[FriendlyName("Mosswart Massacre")]
|
||||
public class PluginCore : PluginBase
|
||||
{
|
||||
internal static PluginHost MyHost;
|
||||
internal static int totalKills = 0;
|
||||
internal static int rareCount = 0;
|
||||
internal static DateTime lastKillTime = DateTime.Now;
|
||||
internal static double killsPer5Min = 0;
|
||||
internal static double killsPerHour = 0;
|
||||
internal static DateTime statsStartTime = DateTime.Now;
|
||||
internal static Timer updateTimer;
|
||||
// Stats manager for kills and rares
|
||||
private static StatsManager StatsMgr;
|
||||
// Chat and command handling
|
||||
private ChatManager chatManager;
|
||||
private IWebSocketService _wsService;
|
||||
private ICommandHandler _commandHandler;
|
||||
public static bool RareMetaEnabled { get; set; } = true;
|
||||
/// <summary>
|
||||
/// Handles server-sent commands via WebSocket.
|
||||
/// </summary>
|
||||
private void HandleServerCommand(CommandEnvelope env)
|
||||
{
|
||||
DispatchChatToBoxWithPluginIntercept(env.Command);
|
||||
CoreManager.Current.Actions.InvokeChatParser($"/a Executed '{env.Command}' from Mosswart Overlord");
|
||||
}
|
||||
public static bool RemoteCommandsEnabled { get; set; } = false;
|
||||
public static bool HttpServerEnabled { get; set; } = false;
|
||||
public static string CharTag { get; set; } = "";
|
||||
public static bool TelemetryEnabled { get; set; } = false;
|
||||
// Telemetry is removed; HTTP posting no longer supported
|
||||
public bool WebSocketEnabled { get; set; } = false;
|
||||
private static Queue<string> rareMessageQueue = new Queue<string>();
|
||||
private static DateTime _lastSent = DateTime.MinValue;
|
||||
private static readonly Queue<string> _chatQueue = new Queue<string>();
|
||||
// Precompiled regex patterns for detecting kill messages
|
||||
private static readonly Regex[] KillRegexes = new Regex[]
|
||||
{
|
||||
new Regex(@"^You flatten (?<targetname>.+)'s body with the force of your assault!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You bring (?<targetname>.+) to a fiery end!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You beat (?<targetname>.+) to a lifeless pulp!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You smite (?<targetname>.+) mightily!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You obliterate (?<targetname>.+)!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You run (?<targetname>.+) through!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You reduce (?<targetname>.+) to a sizzling, oozing mass!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You knock (?<targetname>.+) into next Morningthaw!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You split (?<targetname>.+) apart!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You cleave (?<targetname>.+) in twain!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You slay (?<targetname>.+) viciously enough to impart death several times over!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You reduce (?<targetname>.+) to a drained, twisted corpse!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^Your killing blow nearly turns (?<targetname>.+) inside-out!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^Your attack stops (?<targetname>.+) cold!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^Your lightning coruscates over (?<targetname>.+)'s mortal remains!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^Your assault sends (?<targetname>.+) to an icy death!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^You killed (?<targetname>.+)!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^The thunder of crushing (?<targetname>.+) is followed by the deafening silence of death!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^The deadly force of your attack is so strong that (?<targetname>.+)'s ancestors feel it!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+)'s seared corpse smolders before you!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) is reduced to cinders!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) is shattered by your assault!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) catches your attack, with dire consequences!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) is utterly destroyed by your attack!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) suffers a frozen fate!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+)'s perforated corpse falls before you!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) is fatally punctured!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+)'s last strength dissolves before you!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) is torn to ribbons by your assault!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) is liquified by your attack!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+)'s last strength withers before you!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) is dessicated by your attack!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^(?<targetname>.+) is incinerated by your assault!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
// Additional patterns from original killPatterns
|
||||
new Regex(@"^(?<targetname>.+)'s death is preceded by a sharp, stabbing pain!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^Electricity tears (?<targetname>.+) apart!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
new Regex(@"^Blistered by lightning, (?<targetname>.+) falls!$", RegexOptions.Compiled | RegexOptions.CultureInvariant),
|
||||
};
|
||||
|
||||
protected override void Startup()
|
||||
{
|
||||
|
|
@ -37,15 +88,27 @@ namespace MosswartMassacre
|
|||
|
||||
WriteToChat("Mosswart Massacre has started!");
|
||||
|
||||
// Subscribe to chat message event
|
||||
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
|
||||
CoreManager.Current.CommandLineText += OnChatCommand;
|
||||
// Initialize WebSocket streaming service
|
||||
_wsService = new WebSocketService(StatsMgr);
|
||||
if (WebSocketEnabled)
|
||||
{
|
||||
_wsService.Start();
|
||||
}
|
||||
_wsService.OnServerCommand += HandleServerCommand;
|
||||
// Initialize command handler for "/mm" commands
|
||||
_commandHandler = new CommandHandler(StatsMgr, _wsService);
|
||||
|
||||
// Initialize chat handling
|
||||
chatManager = new ChatManager(StatsMgr, _wsService, _commandHandler.Handle);
|
||||
chatManager.Start();
|
||||
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
|
||||
|
||||
// Initialize the timer
|
||||
updateTimer = new Timer(1000); // Update every second
|
||||
updateTimer.Elapsed += UpdateStats;
|
||||
updateTimer.Start();
|
||||
// Initialize stats manager
|
||||
StatsMgr = new StatsManager();
|
||||
StatsMgr.KillStatsUpdated += (total, per5, perHour) => MainView.UpdateKillStats(total, per5, perHour);
|
||||
StatsMgr.ElapsedTimeUpdated += elapsed => MainView.UpdateElapsedTime(elapsed);
|
||||
StatsMgr.RareCountUpdated += rare => MainView.UpdateRareCount(rare);
|
||||
StatsMgr.Start();
|
||||
|
||||
// Initialize the view (UI)
|
||||
MainView.ViewInit();
|
||||
|
|
@ -66,27 +129,28 @@ namespace MosswartMassacre
|
|||
try
|
||||
{
|
||||
PluginSettings.Save();
|
||||
if (TelemetryEnabled)
|
||||
Telemetry.Stop(); // ensure no dangling timer / HttpClient
|
||||
WriteToChat("Mosswart Massacre is shutting down...");
|
||||
// clean up any pending delayed commands
|
||||
DelayedCommandManager.Shutdown();
|
||||
|
||||
// Unsubscribe from chat message event
|
||||
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
|
||||
CoreManager.Current.CommandLineText -= OnChatCommand;
|
||||
// Stop chat handling
|
||||
chatManager?.Stop();
|
||||
|
||||
// Stop and dispose of the timer
|
||||
if (updateTimer != null)
|
||||
{
|
||||
updateTimer.Stop();
|
||||
updateTimer.Dispose();
|
||||
updateTimer = null;
|
||||
}
|
||||
// Stop and dispose of stats manager
|
||||
StatsMgr?.Stop();
|
||||
StatsMgr?.Dispose();
|
||||
|
||||
// Clean up the view
|
||||
MainView.ViewDestroy();
|
||||
// Disable vtank interface
|
||||
vTank.Disable();
|
||||
|
||||
// Stop WebSocket service
|
||||
if (_wsService != null)
|
||||
{
|
||||
_wsService.OnServerCommand -= HandleServerCommand;
|
||||
_wsService.Stop();
|
||||
_wsService.Dispose();
|
||||
}
|
||||
MyHost = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
@ -102,209 +166,34 @@ namespace MosswartMassacre
|
|||
|
||||
// Apply the values
|
||||
RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
|
||||
WebSocketEnabled = PluginSettings.Instance.WebSocketEnabled;
|
||||
RemoteCommandsEnabled = PluginSettings.Instance.RemoteCommandsEnabled;
|
||||
HttpServerEnabled = PluginSettings.Instance.HttpServerEnabled;
|
||||
TelemetryEnabled = PluginSettings.Instance.TelemetryEnabled;
|
||||
CharTag = PluginSettings.Instance.CharTag;
|
||||
MainView.SetRareMetaToggleState(RareMetaEnabled);
|
||||
if (TelemetryEnabled)
|
||||
Telemetry.Start();
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void OnChatText(object sender, ChatTextInterceptEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// WriteToChat($"[Debug] Chat Color: {e.Color}, Message: {e.Text}");
|
||||
|
||||
if (IsKilledByMeMessage(e.Text))
|
||||
{
|
||||
totalKills++;
|
||||
lastKillTime = DateTime.Now;
|
||||
CalculateKillsPerInterval();
|
||||
MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
|
||||
}
|
||||
|
||||
if (IsRareDiscoveryMessage(e.Text, out string rareText))
|
||||
{
|
||||
rareCount++;
|
||||
MainView.UpdateRareCount(rareCount);
|
||||
|
||||
if (RareMetaEnabled)
|
||||
{
|
||||
Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
|
||||
}
|
||||
|
||||
DelayedCommandManager.AddDelayedCommand($"/a {rareText}", 3000);
|
||||
}
|
||||
// if (e.Text.EndsWith("!testrare\""))
|
||||
// {
|
||||
// string simulatedText = $"{CoreManager.Current.CharacterFilter.Name} has discovered the Ancient Pickle!";
|
||||
//
|
||||
// if (IsRareDiscoveryMessage(simulatedText, out string simulatedRareText))
|
||||
// {
|
||||
// rareCount++;
|
||||
// MainView.UpdateRareCount(rareCount);
|
||||
//
|
||||
// if (RareMetaEnabled)
|
||||
// {
|
||||
// Decal_DispatchOnChatCommand("/vt setmetastate loot_rare");
|
||||
// }
|
||||
//
|
||||
// DelayedCommandManager.AddDelayedCommand($"/a {simulatedRareText}", 3000);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// WriteToChat("[Test] Simulated rare message didn't match the regex.");
|
||||
// }
|
||||
//
|
||||
// return;
|
||||
// }
|
||||
if (e.Color == 18 && e.Text.EndsWith("!report\""))
|
||||
{
|
||||
TimeSpan elapsed = DateTime.Now - statsStartTime;
|
||||
string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}";
|
||||
WriteToChat($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
|
||||
MyHost.Actions.InvokeChatParser($"/a {reportMessage}");
|
||||
}
|
||||
if (RemoteCommandsEnabled && e.Color == 18)
|
||||
{
|
||||
string characterName = Regex.Escape(CoreManager.Current.CharacterFilter.Name);
|
||||
string pattern = $@"^\[Allegiance\].*Dunking Rares.*say[s]?, \""!do {characterName} (?<command>.+)\""$";
|
||||
string tag = Regex.Escape(PluginCore.CharTag);
|
||||
string patterntag = $@"^\[Allegiance\].*Dunking Rares.*say[s]?, \""!dot {tag} (?<command>.+)\""$";
|
||||
|
||||
|
||||
var match = Regex.Match(e.Text, pattern);
|
||||
var matchtag = Regex.Match(e.Text, patterntag);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
string command = match.Groups["command"].Value;
|
||||
DispatchChatToBoxWithPluginIntercept(command);
|
||||
DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
|
||||
}
|
||||
else if (matchtag.Success)
|
||||
{
|
||||
string command = matchtag.Groups["command"].Value;
|
||||
DispatchChatToBoxWithPluginIntercept(command);
|
||||
DelayedCommandManager.AddDelayedCommand($"/a [Remote] Executing: {command}", 2000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// HTTP Telemetry removed
|
||||
if (WebSocketEnabled)
|
||||
_wsService?.Start();
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
/// <summary>
|
||||
/// Determines if the chat text indicates a kill by this character.
|
||||
/// </summary>
|
||||
public static bool IsKilledByMeMessage(string text)
|
||||
{
|
||||
WriteToChat("Error processing chat message: " + ex.Message);
|
||||
}
|
||||
}
|
||||
private void OnChatCommand(object sender, ChatParserInterceptEventArgs e)
|
||||
// Check each precompiled kill message pattern
|
||||
foreach (var regex in KillRegexes)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (e.Text.StartsWith("/mm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
e.Eat = true; // Prevent the message from showing in chat
|
||||
HandleMmCommand(e.Text);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[Error] Failed to process /mm command: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateStats(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Update the elapsed time
|
||||
TimeSpan elapsed = DateTime.Now - statsStartTime;
|
||||
MainView.UpdateElapsedTime(elapsed);
|
||||
|
||||
// Recalculate kill rates
|
||||
CalculateKillsPerInterval();
|
||||
MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteToChat("Error updating stats: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateKillsPerInterval()
|
||||
{
|
||||
double minutesElapsed = (DateTime.Now - statsStartTime).TotalMinutes;
|
||||
|
||||
if (minutesElapsed > 0)
|
||||
{
|
||||
killsPer5Min = (totalKills / minutesElapsed) * 5;
|
||||
killsPerHour = (totalKills / minutesElapsed) * 60;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsKilledByMeMessage(string text)
|
||||
{
|
||||
string[] killPatterns = new string[]
|
||||
{
|
||||
@"^You flatten (?<targetname>.+)'s body with the force of your assault!$",
|
||||
@"^You bring (?<targetname>.+) to a fiery end!$",
|
||||
@"^You beat (?<targetname>.+) to a lifeless pulp!$",
|
||||
@"^You smite (?<targetname>.+) mightily!$",
|
||||
@"^You obliterate (?<targetname>.+)!$",
|
||||
@"^You run (?<targetname>.+) through!$",
|
||||
@"^You reduce (?<targetname>.+) to a sizzling, oozing mass!$",
|
||||
@"^You knock (?<targetname>.+) into next Morningthaw!$",
|
||||
@"^You split (?<targetname>.+) apart!$",
|
||||
@"^You cleave (?<targetname>.+) in twain!$",
|
||||
@"^You slay (?<targetname>.+) viciously enough to impart death several times over!$",
|
||||
@"^You reduce (?<targetname>.+) to a drained, twisted corpse!$",
|
||||
@"^Your killing blow nearly turns (?<targetname>.+) inside-out!$",
|
||||
@"^Your attack stops (?<targetname>.+) cold!$",
|
||||
@"^Your lightning coruscates over (?<targetname>.+)'s mortal remains!$",
|
||||
@"^Your assault sends (?<targetname>.+) to an icy death!$",
|
||||
@"^You killed (?<targetname>.+)!$",
|
||||
@"^The thunder of crushing (?<targetname>.+) is followed by the deafening silence of death!$",
|
||||
@"^The deadly force of your attack is so strong that (?<targetname>.+)'s ancestors feel it!$",
|
||||
@"^(?<targetname>.+)'s seared corpse smolders before you!$",
|
||||
@"^(?<targetname>.+) is reduced to cinders!$",
|
||||
@"^(?<targetname>.+) is shattered by your assault!$",
|
||||
@"^(?<targetname>.+) catches your attack, with dire consequences!$",
|
||||
@"^(?<targetname>.+) is utterly destroyed by your attack!$",
|
||||
@"^(?<targetname>.+) suffers a frozen fate!$",
|
||||
@"^(?<targetname>.+)'s perforated corpse falls before you!$",
|
||||
@"^(?<targetname>.+) is fatally punctured!$",
|
||||
@"^(?<targetname>.+)'s death is preceded by a sharp, stabbing pain!$",
|
||||
@"^(?<targetname>.+) is torn to ribbons by your assault!$",
|
||||
@"^(?<targetname>.+) is liquified by your attack!$",
|
||||
@"^(?<targetname>.+)'s last strength dissolves before you!$",
|
||||
@"^Electricity tears (?<targetname>.+) apart!$",
|
||||
@"^Blistered by lightning, (?<targetname>.+) falls!$",
|
||||
@"^(?<targetname>.+)'s last strength withers before you!$",
|
||||
@"^(?<targetname>.+) is dessicated by your attack!$",
|
||||
@"^(?<targetname>.+) is incinerated by your assault!$"
|
||||
};
|
||||
|
||||
foreach (string pattern in killPatterns)
|
||||
{
|
||||
if (Regex.IsMatch(text, pattern))
|
||||
if (regex.IsMatch(text))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
|
||||
/// <summary>
|
||||
/// Determines if the chat text indicates a rare discovery by this character.
|
||||
/// </summary>
|
||||
public static bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
|
||||
{
|
||||
rareTextOnly = null;
|
||||
|
||||
|
|
@ -320,21 +209,20 @@ namespace MosswartMassacre
|
|||
|
||||
return false;
|
||||
}
|
||||
// Prefix for all chat messages from this plugin
|
||||
private const string ChatPrefix = "[Mosswart Massacre] ";
|
||||
/// <summary>
|
||||
/// Write a message to the chat with plugin prefix.
|
||||
/// </summary>
|
||||
public static void WriteToChat(string message)
|
||||
{
|
||||
MyHost.Actions.AddChatText("[Mosswart Massacre] " + message, 0, 1);
|
||||
MyHost.Actions.AddChatText(ChatPrefix + message, 0, 1);
|
||||
}
|
||||
public static void RestartStats()
|
||||
{
|
||||
totalKills = 0;
|
||||
rareCount = 0;
|
||||
statsStartTime = DateTime.Now;
|
||||
killsPer5Min = 0;
|
||||
killsPerHour = 0;
|
||||
|
||||
// Reset stats via StatsManager
|
||||
StatsMgr?.Restart();
|
||||
WriteToChat("Stats have been reset.");
|
||||
MainView.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
|
||||
MainView.UpdateRareCount(rareCount);
|
||||
}
|
||||
public static void ToggleRareMeta()
|
||||
{
|
||||
|
|
@ -365,141 +253,6 @@ namespace MosswartMassacre
|
|||
if (!Decal_DispatchOnChatCommand(cmd))
|
||||
CoreManager.Current.Actions.InvokeChatParser(cmd);
|
||||
}
|
||||
private void HandleMmCommand(string text)
|
||||
{
|
||||
// Remove the /mm prefix and trim extra whitespace
|
||||
string[] args = text.Substring(3).Trim().Split(' ');
|
||||
|
||||
if (args.Length == 0 || string.IsNullOrEmpty(args[0]))
|
||||
{
|
||||
WriteToChat("Usage: /mm <command>. Try /mm help");
|
||||
return;
|
||||
}
|
||||
|
||||
string subCommand = args[0].ToLower();
|
||||
|
||||
switch (subCommand)
|
||||
{
|
||||
case "telemetry":
|
||||
if (args.Length > 1)
|
||||
{
|
||||
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
TelemetryEnabled = true;
|
||||
Telemetry.Start();
|
||||
PluginSettings.Instance.TelemetryEnabled = true;
|
||||
WriteToChat("Telemetry streaming ENABLED.");
|
||||
}
|
||||
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
TelemetryEnabled = false;
|
||||
Telemetry.Stop();
|
||||
PluginSettings.Instance.TelemetryEnabled = false;
|
||||
WriteToChat("Telemetry streaming DISABLED.");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToChat("Usage: /mm telemetry <enable|disable>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToChat("Usage: /mm telemetry <enable|disable>");
|
||||
}
|
||||
break;
|
||||
case "help":
|
||||
WriteToChat("Mosswart Massacre Commands:");
|
||||
WriteToChat("/mm report - Show current stats");
|
||||
WriteToChat("/mm loc - Show current location");
|
||||
WriteToChat("/mm telemetry - Telemetry streaming enable|disable"); // NEW
|
||||
WriteToChat("/mm reset - Reset all counters");
|
||||
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":
|
||||
TimeSpan elapsed = DateTime.Now - statsStartTime;
|
||||
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;
|
||||
var pos = Utils.GetPlayerPosition();
|
||||
WriteToChat($"Location: {here} (X={pos.X:F1}, Y={pos.Y:F1}, Z={pos.Z:F1})");
|
||||
break;
|
||||
case "reset":
|
||||
RestartStats();
|
||||
break;
|
||||
case "meta":
|
||||
RareMetaEnabled = !RareMetaEnabled;
|
||||
WriteToChat($"Rare meta state is now {(RareMetaEnabled ? "ON" : "OFF")}");
|
||||
MainView.SetRareMetaToggleState(RareMetaEnabled); // <-- sync the UI
|
||||
break;
|
||||
|
||||
case "http":
|
||||
if (args.Length > 1)
|
||||
{
|
||||
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PluginSettings.Instance.HttpServerEnabled = true;
|
||||
HttpServerEnabled = true;
|
||||
HttpCommandServer.Start();
|
||||
}
|
||||
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PluginSettings.Instance.HttpServerEnabled = false;
|
||||
HttpServerEnabled = false;
|
||||
HttpCommandServer.Stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToChat("Usage: /mm http <enable|disable>");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToChat("Usage: /mm http <enable|disable>");
|
||||
}
|
||||
break;
|
||||
|
||||
case "remotecommands":
|
||||
if (args.Length > 1)
|
||||
{
|
||||
if (args[1].Equals("enable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PluginSettings.Instance.RemoteCommandsEnabled = true;
|
||||
RemoteCommandsEnabled = true;
|
||||
WriteToChat("Remote command listening is now ENABLED.");
|
||||
}
|
||||
else if (args[1].Equals("disable", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
PluginSettings.Instance.RemoteCommandsEnabled = false;
|
||||
RemoteCommandsEnabled = false;
|
||||
WriteToChat("Remote command listening is now DISABLED.");
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToChat("Invalid remotecommands argument. Use 'enable' or 'disable'.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteToChat("Usage: /mm remotecommands <enable|disable>");
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
WriteToChat($"Unknown /mm command: {subCommand}. Try /mm help");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,17 +11,32 @@ namespace MosswartMassacre
|
|||
private static PluginSettings _instance;
|
||||
private static string _filePath;
|
||||
private static readonly object _sync = new object();
|
||||
// Timer to debounce saving settings to disk
|
||||
private static readonly System.Timers.Timer _saveTimer;
|
||||
// Reuse YAML serializer/deserializer to avoid rebuilding per operation
|
||||
private static readonly IDeserializer _deserializer = new DeserializerBuilder()
|
||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||
.Build();
|
||||
private static readonly ISerializer _serializer = new SerializerBuilder()
|
||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||
.Build();
|
||||
|
||||
// backing fields
|
||||
private bool _remoteCommandsEnabled = false;
|
||||
private bool _rareMetaEnabled = true;
|
||||
private bool _httpServerEnabled = false;
|
||||
private bool _telemetryEnabled = false;
|
||||
private bool _webSocketEnabled = false;
|
||||
private string _charTag = "default";
|
||||
|
||||
public static PluginSettings Instance => _instance
|
||||
?? throw new InvalidOperationException("PluginSettings not initialized");
|
||||
|
||||
// Static constructor to initialize save debounce timer
|
||||
static PluginSettings()
|
||||
{
|
||||
_saveTimer = new System.Timers.Timer(1000) { AutoReset = false };
|
||||
_saveTimer.Elapsed += (s, e) => Save();
|
||||
}
|
||||
public static void Initialize()
|
||||
{
|
||||
// determine settings file path
|
||||
|
|
@ -29,10 +44,7 @@ namespace MosswartMassacre
|
|||
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();
|
||||
// use shared deserializer
|
||||
|
||||
PluginSettings loaded = null;
|
||||
|
||||
|
|
@ -41,7 +53,7 @@ namespace MosswartMassacre
|
|||
try
|
||||
{
|
||||
string yaml = File.ReadAllText(_filePath);
|
||||
loaded = deserializer.Deserialize<PluginSettings>(yaml);
|
||||
loaded = _deserializer.Deserialize<PluginSettings>(yaml);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -69,11 +81,8 @@ namespace MosswartMassacre
|
|||
{
|
||||
try
|
||||
{
|
||||
// serialize to YAML
|
||||
var serializer = new SerializerBuilder()
|
||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||
.Build();
|
||||
var yaml = serializer.Serialize(_instance);
|
||||
// serialize to YAML using shared serializer
|
||||
var yaml = _serializer.Serialize(_instance);
|
||||
|
||||
// write temp file
|
||||
var temp = _filePath + ".tmp";
|
||||
|
|
@ -98,36 +107,43 @@ namespace MosswartMassacre
|
|||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Schedule settings to be saved after debounce interval.
|
||||
/// </summary>
|
||||
private static void ScheduleSave()
|
||||
{
|
||||
_saveTimer.Stop();
|
||||
_saveTimer.Start();
|
||||
}
|
||||
|
||||
// public properties
|
||||
public bool RemoteCommandsEnabled
|
||||
{
|
||||
get => _remoteCommandsEnabled;
|
||||
set { _remoteCommandsEnabled = value; Save(); }
|
||||
set { _remoteCommandsEnabled = value; ScheduleSave(); }
|
||||
}
|
||||
|
||||
public bool RareMetaEnabled
|
||||
{
|
||||
get => _rareMetaEnabled;
|
||||
set { _rareMetaEnabled = value; Save(); }
|
||||
set { _rareMetaEnabled = value; ScheduleSave(); }
|
||||
}
|
||||
|
||||
public bool HttpServerEnabled
|
||||
{
|
||||
get => _httpServerEnabled;
|
||||
set { _httpServerEnabled = value; Save(); }
|
||||
set { _httpServerEnabled = value; ScheduleSave(); }
|
||||
}
|
||||
|
||||
public bool TelemetryEnabled
|
||||
public bool WebSocketEnabled
|
||||
{
|
||||
get => _telemetryEnabled;
|
||||
set { _telemetryEnabled = value; Save(); }
|
||||
get => _webSocketEnabled;
|
||||
set { _webSocketEnabled = value; ScheduleSave(); }
|
||||
}
|
||||
|
||||
public string CharTag
|
||||
{
|
||||
get => _charTag;
|
||||
set { _charTag = value; Save(); }
|
||||
set { _charTag = value; ScheduleSave(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ using System.Runtime.InteropServices;
|
|||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("9b6a07e1-ae78-47f4-b09c-174f6a27d7a3")]
|
||||
[assembly: Guid("be0a51b6-cf1f-4318-ad49-e40d6aebe14b")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
// Major Version
|
||||
|
|
|
|||
120
MosswartMassacre/StatsManager.cs
Normal file
120
MosswartMassacre/StatsManager.cs
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
using System;
|
||||
using System.Timers;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages kill and rare statistics, with periodic updates.
|
||||
/// </summary>
|
||||
public class StatsManager : IDisposable
|
||||
{
|
||||
private readonly Timer _timer;
|
||||
|
||||
public int TotalKills { get; private set; }
|
||||
public int RareCount { get; private set; }
|
||||
public double KillsPer5Min { get; private set; }
|
||||
public double KillsPerHour { get; private set; }
|
||||
public DateTime StatsStartTime { get; private set; }
|
||||
public DateTime LastKillTime { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Raised when kill statistics change: total kills, kills per 5 min, kills per hour.
|
||||
/// </summary>
|
||||
public event Action<int, double, double> KillStatsUpdated;
|
||||
/// <summary>
|
||||
/// Raised when elapsed time updates (periodic timer).
|
||||
/// </summary>
|
||||
public event Action<TimeSpan> ElapsedTimeUpdated;
|
||||
/// <summary>
|
||||
/// Raised when rare count changes.
|
||||
/// </summary>
|
||||
public event Action<int> RareCountUpdated;
|
||||
|
||||
public StatsManager()
|
||||
{
|
||||
StatsStartTime = DateTime.Now;
|
||||
LastKillTime = DateTime.Now;
|
||||
_timer = new Timer(1000);
|
||||
_timer.Elapsed += OnTimerElapsed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start periodic updates.
|
||||
/// </summary>
|
||||
public void Start()
|
||||
{
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop periodic updates.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
_timer.Stop();
|
||||
}
|
||||
|
||||
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
|
||||
{
|
||||
RecalculateRates();
|
||||
ElapsedTimeUpdated?.Invoke(DateTime.Now - StatsStartTime);
|
||||
KillStatsUpdated?.Invoke(TotalKills, KillsPer5Min, KillsPerHour);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a new kill, updating counts and raising events.
|
||||
/// </summary>
|
||||
public void RegisterKill()
|
||||
{
|
||||
TotalKills++;
|
||||
LastKillTime = DateTime.Now;
|
||||
RecalculateRates();
|
||||
KillStatsUpdated?.Invoke(TotalKills, KillsPer5Min, KillsPerHour);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register a rare discovery and raise event.
|
||||
/// </summary>
|
||||
public void RegisterRare()
|
||||
{
|
||||
RareCount++;
|
||||
RareCountUpdated?.Invoke(RareCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset all statistics to zero and starting point.
|
||||
/// </summary>
|
||||
public void Restart()
|
||||
{
|
||||
TotalKills = 0;
|
||||
RareCount = 0;
|
||||
StatsStartTime = DateTime.Now;
|
||||
KillsPer5Min = 0;
|
||||
KillsPerHour = 0;
|
||||
LastKillTime = DateTime.Now;
|
||||
KillStatsUpdated?.Invoke(TotalKills, KillsPer5Min, KillsPerHour);
|
||||
RareCountUpdated?.Invoke(RareCount);
|
||||
}
|
||||
|
||||
private void RecalculateRates()
|
||||
{
|
||||
double minutesElapsed = (DateTime.Now - StatsStartTime).TotalMinutes;
|
||||
if (minutesElapsed > 0)
|
||||
{
|
||||
KillsPer5Min = (TotalKills / minutesElapsed) * 5;
|
||||
KillsPerHour = (TotalKills / minutesElapsed) * 60;
|
||||
}
|
||||
else
|
||||
{
|
||||
KillsPer5Min = 0;
|
||||
KillsPerHour = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Elapsed -= OnTimerElapsed;
|
||||
_timer.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
// Telemetry.cs ───────────────────────────────────────────────────────────────
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Decal.Adapter;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
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
|
||||
|
||||
/* ───────────── runtime state ───────────── */
|
||||
private static readonly HttpClient _http = new HttpClient();
|
||||
private static string _sessionId;
|
||||
private static CancellationTokenSource _cts;
|
||||
private static bool _enabled;
|
||||
|
||||
/* ───────────── public API ───────────── */
|
||||
public static void Start()
|
||||
{
|
||||
if (_enabled) return;
|
||||
|
||||
_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
|
||||
}
|
||||
|
||||
public static void Stop()
|
||||
{
|
||||
if (!_enabled) return;
|
||||
_cts.Cancel();
|
||||
_enabled = false;
|
||||
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}");
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
215
MosswartMassacre/WebSocketService.cs
Normal file
215
MosswartMassacre/WebSocketService.cs
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
using System;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Configuration;
|
||||
using Decal.Adapter;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MosswartMassacre
|
||||
{
|
||||
/// <summary>
|
||||
/// Envelope for commands received via WebSocket.
|
||||
/// </summary>
|
||||
public class CommandEnvelope
|
||||
{
|
||||
[JsonProperty("player_name")]
|
||||
public string PlayerName { get; set; }
|
||||
|
||||
[JsonProperty("command")]
|
||||
public string Command { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket service for sending chat and receiving server commands.
|
||||
/// </summary>
|
||||
public class WebSocketService : IWebSocketService
|
||||
{
|
||||
private readonly StatsManager _statsManager;
|
||||
private readonly Uri _endpoint;
|
||||
private readonly string _secret;
|
||||
private readonly int _intervalSec;
|
||||
private ClientWebSocket _ws;
|
||||
private CancellationTokenSource _cts;
|
||||
private bool _enabled;
|
||||
private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1);
|
||||
private string _sessionId;
|
||||
|
||||
/// <summary>
|
||||
/// Raised when a command arrives for this character.
|
||||
/// </summary>
|
||||
public event Action<CommandEnvelope> OnServerCommand;
|
||||
|
||||
public WebSocketService(StatsManager statsManager)
|
||||
{
|
||||
_statsManager = statsManager ?? throw new ArgumentNullException(nameof(statsManager));
|
||||
// Load configuration
|
||||
try
|
||||
{
|
||||
var endpoint = ConfigurationManager.AppSettings["WebSocketEndpoint"];
|
||||
_endpoint = !string.IsNullOrEmpty(endpoint)
|
||||
? new Uri(endpoint)
|
||||
: new Uri("wss://mosswart.snakedesert.se/websocket/");
|
||||
}
|
||||
catch
|
||||
{
|
||||
_endpoint = new Uri("wss://mosswart.snakedesert.se/websocket/");
|
||||
}
|
||||
_secret = ConfigurationManager.AppSettings["WebSocketSharedSecret"]
|
||||
?? "your_shared_secret";
|
||||
if (!int.TryParse(ConfigurationManager.AppSettings["WebSocketIntervalSec"], out _intervalSec))
|
||||
{
|
||||
_intervalSec = 5;
|
||||
}
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
if (_enabled) return;
|
||||
_enabled = true;
|
||||
_cts = new CancellationTokenSource();
|
||||
PluginCore.WriteToChat("[WebSocket] connecting…");
|
||||
_ = Task.Run(() => ConnectLoopAsync(_cts.Token));
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!_enabled) return;
|
||||
_enabled = false;
|
||||
_cts.Cancel();
|
||||
_ws?.Abort();
|
||||
_ws?.Dispose();
|
||||
_ws = null;
|
||||
PluginCore.WriteToChat("[WebSocket] DISABLED");
|
||||
}
|
||||
|
||||
public async Task SendChatTextAsync(int colorIndex, string chatText)
|
||||
{
|
||||
await _sendLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_ws == null || _ws.State != WebSocketState.Open)
|
||||
return;
|
||||
var envelope = new
|
||||
{
|
||||
type = "chat",
|
||||
character_name = CoreManager.Current.CharacterFilter.Name,
|
||||
text = chatText,
|
||||
color = colorIndex
|
||||
};
|
||||
var json = JsonConvert.SerializeObject(envelope);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
await _ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cts.Token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat("[WebSocket] send error: " + ex.Message);
|
||||
Stop();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_sendLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ConnectLoopAsync(CancellationToken token)
|
||||
{
|
||||
while (_enabled && !token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ws = new ClientWebSocket();
|
||||
_ws.Options.SetRequestHeader("X-Plugin-Secret", _secret);
|
||||
await _ws.ConnectAsync(_endpoint, token);
|
||||
PluginCore.WriteToChat("[WebSocket] CONNECTED");
|
||||
_sessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
|
||||
|
||||
// Register this client
|
||||
var registerMsg = new { type = "register", player_name = CoreManager.Current.CharacterFilter.Name };
|
||||
await SendChatTextAsync(0, JsonConvert.SerializeObject(registerMsg));
|
||||
PluginCore.WriteToChat("[WebSocket] REGISTERED");
|
||||
|
||||
var buffer = new byte[4096];
|
||||
var receiveTask = ReceiveLoopAsync(buffer, token);
|
||||
|
||||
// Telemetry loop
|
||||
while (_ws.State == WebSocketState.Open && !token.IsCancellationRequested)
|
||||
{
|
||||
var coords = Coordinates.Me;
|
||||
var payload = new
|
||||
{
|
||||
type = "telemetry",
|
||||
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 = _statsManager.TotalKills,
|
||||
onlinetime = (DateTime.Now - _statsManager.StatsStartTime).ToString(@"dd\.hh\:mm\:ss"),
|
||||
kills_per_hour = _statsManager.KillsPerHour,
|
||||
deaths = 0,
|
||||
rares_found = _statsManager.RareCount,
|
||||
prismatic_taper_count = 0,
|
||||
vt_state = VtankControl.VtGetMetaState()
|
||||
};
|
||||
var json = JsonConvert.SerializeObject(payload);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
await _ws.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, token);
|
||||
await Task.Delay(TimeSpan.FromSeconds(_intervalSec), token);
|
||||
}
|
||||
|
||||
await receiveTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[WebSocket] error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ws?.Abort();
|
||||
_ws?.Dispose();
|
||||
_ws = null;
|
||||
}
|
||||
// wait before reconnect
|
||||
await Task.Delay(2000);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(byte[] buffer, CancellationToken token)
|
||||
{
|
||||
while (_ws.State == WebSocketState.Open && !token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _ws.ReceiveAsync(new ArraySegment<byte>(buffer), token);
|
||||
if (result.MessageType == WebSocketMessageType.Close) break;
|
||||
var msg = Encoding.UTF8.GetString(buffer, 0, result.Count).Trim();
|
||||
CommandEnvelope env;
|
||||
try { env = JsonConvert.DeserializeObject<CommandEnvelope>(msg); }
|
||||
catch { continue; }
|
||||
if (string.Equals(env.PlayerName, CoreManager.Current.CharacterFilter.Name, StringComparison.OrdinalIgnoreCase))
|
||||
OnServerCommand?.Invoke(env);
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
PluginCore.WriteToChat($"[WebSocket] receive error: {ex.Message}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
_sendLock.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<appSettings>
|
||||
<!-- WebSocket settings -->
|
||||
<add key="WebSocketEndpoint" value="wss://mosswart.snakedesert.se/websocket/" />
|
||||
<add key="WebSocketSharedSecret" value="your_shared_secret" />
|
||||
<add key="WebSocketIntervalSec" value="5" />
|
||||
</appSettings>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<dependentAssembly>
|
||||
|
|
|
|||
BIN
MosswartMassacre/lib/utank2-i.dll
Normal file
BIN
MosswartMassacre/lib/utank2-i.dll
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue