Phase 2: Extract IPluginLogger and KillTracker

- Create IPluginLogger interface, PluginCore implements it
- CharacterStats.cs and WebSocket.cs now use IPluginLogger instead of PluginCore.WriteToChat
- Extract KillTracker.cs: owns kill detection (all 36 regex patterns), death tracking,
  rate calculation, and the 1-sec stats update timer
- Bridge properties on PluginCore maintain backward compat for WebSocket telemetry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-27 07:29:49 +00:00
parent 4845a67c1f
commit 366cca8cb6
6 changed files with 265 additions and 160 deletions

View file

@ -26,6 +26,8 @@ namespace MosswartMassacre
public static class CharacterStats public static class CharacterStats
{ {
private static IPluginLogger _logger;
// Cached allegiance data (populated from network messages) // Cached allegiance data (populated from network messages)
private static string allegianceName; private static string allegianceName;
private static int allegianceSize; private static int allegianceSize;
@ -44,8 +46,9 @@ namespace MosswartMassacre
/// <summary> /// <summary>
/// Reset all cached data. Call on plugin init. /// Reset all cached data. Call on plugin init.
/// </summary> /// </summary>
internal static void Init() internal static void Init(IPluginLogger logger = null)
{ {
_logger = logger;
allegianceName = null; allegianceName = null;
allegianceSize = 0; allegianceSize = 0;
followers = 0; followers = 0;
@ -112,7 +115,7 @@ namespace MosswartMassacre
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[CharStats] Allegiance processing error: {ex.Message}"); _logger?.Log($"[CharStats] Allegiance processing error: {ex.Message}");
} }
} }
@ -142,7 +145,7 @@ namespace MosswartMassacre
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[CharStats] Property processing error: {ex.Message}"); _logger?.Log($"[CharStats] Property processing error: {ex.Message}");
} }
} }
@ -169,7 +172,7 @@ namespace MosswartMassacre
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[CharStats] Int64 property update error: {ex.Message}"); _logger?.Log($"[CharStats] Int64 property update error: {ex.Message}");
} }
} }
@ -186,7 +189,7 @@ namespace MosswartMassacre
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[CharStats] Title processing error: {ex.Message}"); _logger?.Log($"[CharStats] Title processing error: {ex.Message}");
} }
} }
@ -201,7 +204,7 @@ namespace MosswartMassacre
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[CharStats] Set title error: {ex.Message}"); _logger?.Log($"[CharStats] Set title error: {ex.Message}");
} }
} }
@ -329,7 +332,7 @@ namespace MosswartMassacre
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[CharStats] Error collecting stats: {ex.Message}"); _logger?.Log($"[CharStats] Error collecting stats: {ex.Message}");
} }
} }
} }

View file

@ -0,0 +1,11 @@
namespace MosswartMassacre
{
/// <summary>
/// Interface for writing messages to the game chat window.
/// Eliminates direct PluginCore.WriteToChat() dependencies from manager classes.
/// </summary>
public interface IPluginLogger
{
void Log(string message);
}
}

View file

@ -0,0 +1,176 @@
using System;
using System.Text.RegularExpressions;
using System.Timers;
namespace MosswartMassacre
{
/// <summary>
/// Tracks kills, deaths, and kill rate calculations.
/// Owns the 1-second stats update timer.
/// </summary>
internal class KillTracker
{
private readonly IPluginLogger _logger;
private readonly Action<int, double, double> _onStatsUpdated;
private readonly Action<TimeSpan> _onElapsedUpdated;
private int _totalKills;
private int _sessionDeaths;
private int _totalDeaths;
private double _killsPer5Min;
private double _killsPerHour;
private DateTime _lastKillTime = DateTime.Now;
private DateTime _statsStartTime = DateTime.Now;
private Timer _updateTimer;
// Kill message patterns — all 35+ patterns preserved exactly
private static readonly 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!$"
};
internal int TotalKills => _totalKills;
internal double KillsPerHour => _killsPerHour;
internal double KillsPer5Min => _killsPer5Min;
internal int SessionDeaths => _sessionDeaths;
internal int TotalDeaths => _totalDeaths;
internal DateTime StatsStartTime => _statsStartTime;
internal DateTime LastKillTime => _lastKillTime;
internal int RareCount { get; set; }
/// <param name="logger">Logger for chat output</param>
/// <param name="onStatsUpdated">Callback(totalKills, killsPer5Min, killsPerHour) for UI updates</param>
/// <param name="onElapsedUpdated">Callback(elapsed) for UI elapsed time updates</param>
internal KillTracker(IPluginLogger logger, Action<int, double, double> onStatsUpdated, Action<TimeSpan> onElapsedUpdated)
{
_logger = logger;
_onStatsUpdated = onStatsUpdated;
_onElapsedUpdated = onElapsedUpdated;
}
internal void Start()
{
_updateTimer = new Timer(Constants.StatsUpdateIntervalMs);
_updateTimer.Elapsed += UpdateStats;
_updateTimer.Start();
}
internal void Stop()
{
if (_updateTimer != null)
{
_updateTimer.Stop();
_updateTimer.Dispose();
_updateTimer = null;
}
}
internal bool CheckForKill(string text)
{
if (IsKilledByMeMessage(text))
{
_totalKills++;
_lastKillTime = DateTime.Now;
CalculateKillsPerInterval();
_onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
return true;
}
return false;
}
internal void OnDeath()
{
_sessionDeaths++;
}
internal void SetTotalDeaths(int totalDeaths)
{
_totalDeaths = totalDeaths;
}
internal void RestartStats()
{
_totalKills = 0;
RareCount = 0;
_sessionDeaths = 0;
_statsStartTime = DateTime.Now;
_killsPer5Min = 0;
_killsPerHour = 0;
_logger?.Log($"Stats have been reset. Session deaths: {_sessionDeaths}, Total deaths: {_totalDeaths}");
_onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
}
private void UpdateStats(object sender, ElapsedEventArgs e)
{
try
{
TimeSpan elapsed = DateTime.Now - _statsStartTime;
_onElapsedUpdated?.Invoke(elapsed);
CalculateKillsPerInterval();
_onStatsUpdated?.Invoke(_totalKills, _killsPer5Min, _killsPerHour);
}
catch (Exception ex)
{
_logger?.Log("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)
{
foreach (string pattern in KillPatterns)
{
if (Regex.IsMatch(text, pattern))
return true;
}
return false;
}
}
}

View file

@ -306,6 +306,8 @@
</Compile> </Compile>
<Compile Include="CommandRouter.cs" /> <Compile Include="CommandRouter.cs" />
<Compile Include="Constants.cs" /> <Compile Include="Constants.cs" />
<Compile Include="IPluginLogger.cs" />
<Compile Include="KillTracker.cs" />
<Compile Include="ClientTelemetry.cs" /> <Compile Include="ClientTelemetry.cs" />
<Compile Include="DecalHarmonyClean.cs" /> <Compile Include="DecalHarmonyClean.cs" />
<Compile Include="FlagTrackerData.cs" /> <Compile Include="FlagTrackerData.cs" />

View file

@ -18,7 +18,7 @@ using Mag.Shared.Constants;
namespace MosswartMassacre namespace MosswartMassacre
{ {
[FriendlyName("Mosswart Massacre")] [FriendlyName("Mosswart Massacre")]
public class PluginCore : PluginBase public class PluginCore : PluginBase, IPluginLogger
{ {
// Hot Reload Support Properties // Hot Reload Support Properties
private static string _assemblyDirectory = null; private static string _assemblyDirectory = null;
@ -47,21 +47,21 @@ namespace MosswartMassacre
public static bool IsHotReload { get; set; } public static bool IsHotReload { get; set; }
internal static PluginHost MyHost; internal static PluginHost MyHost;
internal static int totalKills = 0;
internal static int rareCount = 0; internal static int rareCount = 0;
internal static int sessionDeaths = 0; // Deaths this session
internal static int totalDeaths = 0; // Total deaths from character
internal static int cachedPrismaticCount = 0; // Cached Prismatic Taper count internal static int cachedPrismaticCount = 0; // Cached Prismatic Taper count
internal static int lastPrismaticCount = 0; // For delta calculation internal static int lastPrismaticCount = 0; // For delta calculation
// Track taper items and their containers for accurate release detection // Track taper items and their containers for accurate release detection
private static readonly Dictionary<int, int> trackedTaperContainers = new Dictionary<int, int>(); private static readonly Dictionary<int, int> trackedTaperContainers = new Dictionary<int, int>();
private static readonly Dictionary<int, int> lastKnownStackSizes = new Dictionary<int, int>(); private static readonly Dictionary<int, int> lastKnownStackSizes = new Dictionary<int, int>();
internal static DateTime lastKillTime = DateTime.Now; // Bridge properties for WebSocket telemetry until IGameStats migration (Phase 5)
internal static double killsPer5Min = 0; private static KillTracker _staticKillTracker;
internal static double killsPerHour = 0; internal static int totalKills => _staticKillTracker?.TotalKills ?? 0;
internal static DateTime statsStartTime = DateTime.Now; internal static double killsPerHour => _staticKillTracker?.KillsPerHour ?? 0;
internal static Timer updateTimer; internal static int sessionDeaths => _staticKillTracker?.SessionDeaths ?? 0;
internal static int totalDeaths => _staticKillTracker?.TotalDeaths ?? 0;
internal static DateTime statsStartTime => _staticKillTracker?.StatsStartTime ?? DateTime.Now;
internal static DateTime lastKillTime => _staticKillTracker?.LastKillTime ?? DateTime.Now;
private static Timer vitalsTimer; private static Timer vitalsTimer;
private static System.Windows.Forms.Timer commandTimer; private static System.Windows.Forms.Timer commandTimer;
private static Timer characterStatsTimer; private static Timer characterStatsTimer;
@ -127,7 +127,8 @@ namespace MosswartMassacre
private static DateTime _lastSent = DateTime.MinValue; private static DateTime _lastSent = DateTime.MinValue;
private static readonly Queue<string> _chatQueue = new Queue<string>(); private static readonly Queue<string> _chatQueue = new Queue<string>();
// Command routing // Managers
private KillTracker _killTracker;
private CommandRouter _commandRouter; private CommandRouter _commandRouter;
protected override void Startup() protected override void Startup()
@ -186,10 +187,13 @@ namespace MosswartMassacre
// Initialize VVS view after character login // Initialize VVS view after character login
ViewManager.ViewInit(); ViewManager.ViewInit();
// Initialize the timer // Initialize kill tracker (owns the 1-sec stats timer)
updateTimer = new Timer(Constants.StatsUpdateIntervalMs); _killTracker = new KillTracker(
updateTimer.Elapsed += UpdateStats; this,
updateTimer.Start(); (kills, per5, perHr) => ViewManager.UpdateKillStats(kills, per5, perHr),
elapsed => ViewManager.UpdateElapsedTime(elapsed));
_staticKillTracker = _killTracker;
_killTracker.Start();
// Initialize vitals streaming timer // Initialize vitals streaming timer
vitalsTimer = new Timer(Constants.VitalsUpdateIntervalMs); vitalsTimer = new Timer(Constants.VitalsUpdateIntervalMs);
@ -207,13 +211,15 @@ namespace MosswartMassacre
// Initialize character stats and hook ServerDispatch early // Initialize character stats and hook ServerDispatch early
// 0x0013 (character properties with luminance) fires DURING login, // 0x0013 (character properties with luminance) fires DURING login,
// BEFORE LoginComplete — must hook here to catch it // BEFORE LoginComplete — must hook here to catch it
CharacterStats.Init(); CharacterStats.Init(this);
CoreManager.Current.EchoFilter.ServerDispatch += EchoFilter_ServerDispatch; CoreManager.Current.EchoFilter.ServerDispatch += EchoFilter_ServerDispatch;
// Enable TLS1.2 // Enable TLS1.2
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
//Enable vTank interface //Enable vTank interface
vTank.Enable(); vTank.Enable();
// Set logger for WebSocket
WebSocket.SetLogger(this);
//lyssna på commands //lyssna på commands
WebSocket.OnServerCommand += HandleServerCommand; WebSocket.OnServerCommand += HandleServerCommand;
//starta inventory. Hanterar subscriptions i den med //starta inventory. Hanterar subscriptions i den med
@ -263,13 +269,8 @@ namespace MosswartMassacre
// Unsubscribe from server dispatch // Unsubscribe from server dispatch
CoreManager.Current.EchoFilter.ServerDispatch -= EchoFilter_ServerDispatch; CoreManager.Current.EchoFilter.ServerDispatch -= EchoFilter_ServerDispatch;
// Stop and dispose of the timers // Stop kill tracker
if (updateTimer != null) _killTracker?.Stop();
{
updateTimer.Stop();
updateTimer.Dispose();
updateTimer = null;
}
if (vitalsTimer != null) if (vitalsTimer != null)
{ {
@ -390,8 +391,7 @@ namespace MosswartMassacre
} }
// Initialize death tracking // Initialize death tracking
totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); _killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
sessionDeaths = 0;
// Initialize cached Prismatic Taper count // Initialize cached Prismatic Taper count
InitializePrismaticTaperCount(); InitializePrismaticTaperCount();
@ -604,8 +604,7 @@ namespace MosswartMassacre
} }
// 6. Reinitialize death tracking // 6. Reinitialize death tracking
totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); _killTracker?.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
// Don't reset sessionDeaths - keep the current session count
// 7. Reinitialize cached Prismatic Taper count // 7. Reinitialize cached Prismatic Taper count
InitializePrismaticTaperCount(); InitializePrismaticTaperCount();
@ -995,8 +994,8 @@ namespace MosswartMassacre
private void OnCharacterDeath(object sender, Decal.Adapter.Wrappers.DeathEventArgs e) private void OnCharacterDeath(object sender, Decal.Adapter.Wrappers.DeathEventArgs e)
{ {
sessionDeaths++; _killTracker.OnDeath();
totalDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); _killTracker.SetTotalDeaths(CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths));
} }
private void HandleServerCommand(CommandEnvelope env) private void HandleServerCommand(CommandEnvelope env)
@ -1038,18 +1037,13 @@ namespace MosswartMassacre
try try
{ {
if (IsKilledByMeMessage(e.Text)) _killTracker.CheckForKill(e.Text);
{
totalKills++;
lastKillTime = DateTime.Now;
CalculateKillsPerInterval();
ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
if (IsRareDiscoveryMessage(e.Text, out string rareText)) if (IsRareDiscoveryMessage(e.Text, out string rareText))
{ {
rareCount++; _killTracker.RareCount++;
ViewManager.UpdateRareCount(rareCount); rareCount = _killTracker.RareCount; // sync static for now
ViewManager.UpdateRareCount(_killTracker.RareCount);
if (RareMetaEnabled) if (RareMetaEnabled)
{ {
@ -1063,8 +1057,8 @@ namespace MosswartMassacre
if (e.Color == 18 && e.Text.EndsWith("!report\"")) if (e.Color == 18 && e.Text.EndsWith("!report\""))
{ {
TimeSpan elapsed = DateTime.Now - statsStartTime; TimeSpan elapsed = DateTime.Now - _killTracker.StatsStartTime;
string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}"; string reportMessage = $"Total Kills: {_killTracker.TotalKills}, Kills per Hour: {_killTracker.KillsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {_killTracker.RareCount}";
WriteToChat($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}"); WriteToChat($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
MyHost.Actions.InvokeChatParser($"/a {reportMessage}"); MyHost.Actions.InvokeChatParser($"/a {reportMessage}");
} }
@ -1098,24 +1092,6 @@ namespace MosswartMassacre
} }
} }
private void UpdateStats(object sender, ElapsedEventArgs e)
{
try
{
// Update the elapsed time
TimeSpan elapsed = DateTime.Now - statsStartTime;
ViewManager.UpdateElapsedTime(elapsed);
// Recalculate kill rates
CalculateKillsPerInterval();
ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
}
catch (Exception ex)
{
WriteToChat("Error updating stats: " + ex.Message);
}
}
private static void SendVitalsUpdate(object sender, ElapsedEventArgs e) private static void SendVitalsUpdate(object sender, ElapsedEventArgs e)
{ {
@ -1211,67 +1187,6 @@ namespace MosswartMassacre
} }
} }
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))
return true;
}
return false;
}
private bool IsRareDiscoveryMessage(string text, out string rareTextOnly) private bool IsRareDiscoveryMessage(string text, out string rareTextOnly)
{ {
rareTextOnly = null; rareTextOnly = null;
@ -1316,18 +1231,13 @@ namespace MosswartMassacre
} }
} }
} }
void IPluginLogger.Log(string message) => WriteToChat(message);
public static void RestartStats() public static void RestartStats()
{ {
totalKills = 0; _staticKillTracker?.RestartStats();
rareCount = 0; ViewManager.UpdateRareCount(_staticKillTracker?.RareCount ?? 0);
sessionDeaths = 0; // Reset session deaths only
statsStartTime = DateTime.Now;
killsPer5Min = 0;
killsPerHour = 0;
WriteToChat($"Stats have been reset. Session deaths: {sessionDeaths}, Total deaths: {totalDeaths}");
ViewManager.UpdateKillStats(totalKills, killsPer5Min, killsPerHour);
ViewManager.UpdateRareCount(rareCount);
} }
public static void ToggleRareMeta() public static void ToggleRareMeta()
{ {
@ -1396,8 +1306,8 @@ namespace MosswartMassacre
_commandRouter.Register("report", args => _commandRouter.Register("report", args =>
{ {
TimeSpan elapsed = DateTime.Now - statsStartTime; TimeSpan elapsed = DateTime.Now - _killTracker.StatsStartTime;
string reportMessage = $"Total Kills: {totalKills}, Kills per Hour: {killsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {rareCount}, Session Deaths: {sessionDeaths}, Total Deaths: {totalDeaths}"; string reportMessage = $"Total Kills: {_killTracker.TotalKills}, Kills per Hour: {_killTracker.KillsPerHour:F2}, Elapsed Time: {elapsed:dd\\.hh\\:mm\\:ss}, Rares Found: {_killTracker.RareCount}, Session Deaths: {_killTracker.SessionDeaths}, Total Deaths: {_killTracker.TotalDeaths}";
WriteToChat(reportMessage); WriteToChat(reportMessage);
}, "Show current stats"); }, "Show current stats");
@ -1656,17 +1566,17 @@ namespace MosswartMassacre
try try
{ {
WriteToChat("=== Death Tracking Statistics ==="); WriteToChat("=== Death Tracking Statistics ===");
WriteToChat($"Session Deaths: {sessionDeaths}"); WriteToChat($"Session Deaths: {_killTracker.SessionDeaths}");
WriteToChat($"Total Deaths: {totalDeaths}"); WriteToChat($"Total Deaths: {_killTracker.TotalDeaths}");
int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
WriteToChat($"Character Property NumDeaths: {currentCharDeaths}"); WriteToChat($"Character Property NumDeaths: {currentCharDeaths}");
if (currentCharDeaths != totalDeaths) if (currentCharDeaths != _killTracker.TotalDeaths)
{ {
WriteToChat($"[WARNING] Death count sync issue detected!"); WriteToChat($"[WARNING] Death count sync issue detected!");
WriteToChat($"Updating totalDeaths from {totalDeaths} to {currentCharDeaths}"); WriteToChat($"Updating totalDeaths from {_killTracker.TotalDeaths} to {currentCharDeaths}");
totalDeaths = currentCharDeaths; _killTracker.SetTotalDeaths(currentCharDeaths);
} }
WriteToChat("Death tracking is active and will increment on character death."); WriteToChat("Death tracking is active and will increment on character death.");
@ -1682,14 +1592,14 @@ namespace MosswartMassacre
try try
{ {
WriteToChat("=== Manual Death Test ==="); WriteToChat("=== Manual Death Test ===");
WriteToChat($"Current sessionDeaths variable: {sessionDeaths}"); WriteToChat($"Current sessionDeaths variable: {_killTracker.SessionDeaths}");
WriteToChat($"Current totalDeaths variable: {totalDeaths}"); WriteToChat($"Current totalDeaths variable: {_killTracker.TotalDeaths}");
int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths); int currentCharDeaths = CoreManager.Current.CharacterFilter.GetCharProperty((int)IntValueKey.NumDeaths);
WriteToChat($"Character Property NumDeaths (43): {currentCharDeaths}"); WriteToChat($"Character Property NumDeaths (43): {currentCharDeaths}");
sessionDeaths++; _killTracker.OnDeath();
WriteToChat($"Manually incremented sessionDeaths to: {sessionDeaths}"); WriteToChat($"Manually incremented sessionDeaths to: {_killTracker.SessionDeaths}");
WriteToChat("Note: This doesn't simulate a real death, just tests the tracking variables."); WriteToChat("Note: This doesn't simulate a real death, just tests the tracking variables.");
WriteToChat($"Death event subscription check:"); WriteToChat($"Death event subscription check:");

View file

@ -35,6 +35,7 @@ namespace MosswartMassacre
private const string SharedSecret = "your_shared_secret"; private const string SharedSecret = "your_shared_secret";
private const int IntervalSec = 5; private const int IntervalSec = 5;
private static string SessionId = ""; private static string SessionId = "";
private static IPluginLogger _logger;
// ─── cached prismatic taper count ─── (now handled by PluginCore event system) // ─── cached prismatic taper count ─── (now handled by PluginCore event system)
@ -51,13 +52,15 @@ namespace MosswartMassacre
// ─── public API ───────────────────────────── // ─── public API ─────────────────────────────
public static void SetLogger(IPluginLogger logger) => _logger = logger;
public static void Start() public static void Start()
{ {
if (_enabled) return; if (_enabled) return;
_enabled = true; _enabled = true;
_cts = new CancellationTokenSource(); _cts = new CancellationTokenSource();
PluginCore.WriteToChat("[WebSocket] connecting…"); _logger?.Log("[WebSocket] connecting…");
_ = Task.Run(ConnectAndLoopAsync); _ = Task.Run(ConnectAndLoopAsync);
} }
@ -72,7 +75,7 @@ namespace MosswartMassacre
_ws?.Dispose(); _ws?.Dispose();
_ws = null; _ws = null;
PluginCore.WriteToChat("[WebSocket] DISABLED"); _logger?.Log("[WebSocket] DISABLED");
} }
// ─── connect / receive / telemetry loop ────────────────────── // ─── connect / receive / telemetry loop ──────────────────────
@ -87,7 +90,7 @@ namespace MosswartMassacre
_ws = new ClientWebSocket(); _ws = new ClientWebSocket();
_ws.Options.SetRequestHeader("X-Plugin-Secret", SharedSecret); _ws.Options.SetRequestHeader("X-Plugin-Secret", SharedSecret);
await _ws.ConnectAsync(WsEndpoint, _cts.Token); await _ws.ConnectAsync(WsEndpoint, _cts.Token);
PluginCore.WriteToChat("[WebSocket] CONNECTED"); _logger?.Log("[WebSocket] CONNECTED");
SessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}"; SessionId = $"{CoreManager.Current.CharacterFilter.Name}-{DateTime.UtcNow:yyyyMMdd-HHmmss}";
// ─── Register this socket under our character name ─── // ─── Register this socket under our character name ───
@ -98,7 +101,7 @@ namespace MosswartMassacre
}; };
var regJson = JsonConvert.SerializeObject(registerEnvelope); var regJson = JsonConvert.SerializeObject(registerEnvelope);
await SendEncodedAsync(regJson, _cts.Token); await SendEncodedAsync(regJson, _cts.Token);
PluginCore.WriteToChat("[WebSocket] REGISTERED"); _logger?.Log("[WebSocket] REGISTERED");
var buffer = new byte[4096]; var buffer = new byte[4096];
@ -118,7 +121,7 @@ namespace MosswartMassacre
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[WebSocket] receive error: {ex.Message}"); _logger?.Log($"[WebSocket] receive error: {ex.Message}");
break; break;
} }
@ -151,7 +154,7 @@ namespace MosswartMassacre
}); });
// 5) Inline telemetry loop // 5) Inline telemetry loop
PluginCore.WriteToChat("[WebSocket] Starting telemetry loop"); _logger?.Log("[WebSocket] Starting telemetry loop");
while (_ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested) while (_ws.State == WebSocketState.Open && !_cts.Token.IsCancellationRequested)
{ {
try try
@ -161,7 +164,7 @@ namespace MosswartMassacre
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[WebSocket] Telemetry failed: {ex.Message}"); _logger?.Log($"[WebSocket] Telemetry failed: {ex.Message}");
break; // Exit telemetry loop on failure break; // Exit telemetry loop on failure
} }
@ -171,30 +174,30 @@ namespace MosswartMassacre
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
PluginCore.WriteToChat("[WebSocket] Telemetry loop cancelled"); _logger?.Log("[WebSocket] Telemetry loop cancelled");
break; break;
} }
} }
// Log why telemetry loop exited // Log why telemetry loop exited
PluginCore.WriteToChat($"[WebSocket] Telemetry loop ended - State: {_ws?.State}, Cancelled: {_cts.Token.IsCancellationRequested}"); _logger?.Log($"[WebSocket] Telemetry loop ended - State: {_ws?.State}, Cancelled: {_cts.Token.IsCancellationRequested}");
// Wait for receive loop to finish // Wait for receive loop to finish
await receiveTask; await receiveTask;
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
PluginCore.WriteToChat("[WebSocket] Connection cancelled"); _logger?.Log("[WebSocket] Connection cancelled");
break; break;
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[WebSocket] Connection error: {ex.Message}"); _logger?.Log($"[WebSocket] Connection error: {ex.Message}");
} }
finally finally
{ {
var finalState = _ws?.State.ToString() ?? "null"; var finalState = _ws?.State.ToString() ?? "null";
PluginCore.WriteToChat($"[WebSocket] Cleaning up connection - Final state: {finalState}"); _logger?.Log($"[WebSocket] Cleaning up connection - Final state: {finalState}");
_ws?.Abort(); _ws?.Abort();
_ws?.Dispose(); _ws?.Dispose();
_ws = null; _ws = null;
@ -203,7 +206,7 @@ namespace MosswartMassacre
// Pause before reconnecting // Pause before reconnecting
if (_enabled) if (_enabled)
{ {
PluginCore.WriteToChat("[WebSocket] Reconnecting in 2 seconds..."); _logger?.Log("[WebSocket] Reconnecting in 2 seconds...");
try { await Task.Delay(2000, CancellationToken.None); } catch { } try { await Task.Delay(2000, CancellationToken.None); } catch { }
} }
} }
@ -334,7 +337,7 @@ namespace MosswartMassacre
} }
catch (Exception ex) catch (Exception ex)
{ {
PluginCore.WriteToChat($"[WebSocket] Send error: {ex.Message}"); _logger?.Log($"[WebSocket] Send error: {ex.Message}");
_ws?.Abort(); _ws?.Abort();
_ws?.Dispose(); _ws?.Dispose();
_ws = null; _ws = null;