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

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