Phase 4: Extract ChatEventRouter and GameEventRouter

- ChatEventRouter.cs: routes chat events to KillTracker, RareTracker, handles
  allegiance report trigger and WebSocket chat streaming
- GameEventRouter.cs: routes ServerDispatch messages (0xF7B0, 0x02CF) to CharacterStats
- PluginCore no longer contains OnChatText, AllChatText, NormalizeChatLine,
  or EchoFilter_ServerDispatch methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-27 07:50:41 +00:00
parent c90e888d32
commit f9264f2767
4 changed files with 185 additions and 124 deletions

View file

@ -0,0 +1,90 @@
using System;
using System.Text.RegularExpressions;
using Decal.Adapter;
using Decal.Adapter.Wrappers;
namespace MosswartMassacre
{
/// <summary>
/// Routes chat events to the appropriate handler (KillTracker, RareTracker, etc.)
/// Replaces the big if/else chain in PluginCore.OnChatText.
/// </summary>
internal class ChatEventRouter
{
private readonly IPluginLogger _logger;
private readonly KillTracker _killTracker;
private RareTracker _rareTracker;
private readonly Action<int> _onRareCountChanged;
private readonly Action<string> _onAllegianceReport;
internal void SetRareTracker(RareTracker rareTracker) => _rareTracker = rareTracker;
internal ChatEventRouter(
IPluginLogger logger,
KillTracker killTracker,
RareTracker rareTracker,
Action<int> onRareCountChanged,
Action<string> onAllegianceReport)
{
_logger = logger;
_killTracker = killTracker;
_rareTracker = rareTracker;
_onRareCountChanged = onRareCountChanged;
_onAllegianceReport = onAllegianceReport;
}
internal void OnChatText(object sender, ChatTextInterceptEventArgs e)
{
try
{
_killTracker.CheckForKill(e.Text);
if (_rareTracker != null && _rareTracker.CheckForRare(e.Text, out string rareText))
{
_killTracker.RareCount = _rareTracker.RareCount;
_onRareCountChanged?.Invoke(_rareTracker.RareCount);
}
if (e.Color == 18 && e.Text.EndsWith("!report\""))
{
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: {_rareTracker?.RareCount ?? 0}";
_logger?.Log($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
_onAllegianceReport?.Invoke(reportMessage);
}
}
catch (Exception ex)
{
_logger?.Log("Error processing chat message: " + ex.Message);
}
}
/// <summary>
/// Streams all chat text to WebSocket (separate handler from the filtered one above).
/// </summary>
internal static async void AllChatText(object sender, ChatTextInterceptEventArgs e)
{
try
{
string cleaned = NormalizeChatLine(e.Text);
await WebSocket.SendChatTextAsync(e.Color, cleaned);
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WS] Chat send failed: {ex}");
}
}
private static string NormalizeChatLine(string raw)
{
if (string.IsNullOrEmpty(raw))
return raw;
var noTags = Regex.Replace(raw, "<[^>]+>", "");
var trimmed = noTags.TrimEnd('\r', '\n');
var collapsed = Regex.Replace(trimmed, @"[ ]{2,}", " ");
return collapsed;
}
}
}

View file

@ -0,0 +1,55 @@
using System;
using Decal.Adapter;
namespace MosswartMassacre
{
/// <summary>
/// Routes EchoFilter.ServerDispatch network messages to the appropriate handlers.
/// Owns the routing of 0xF7B0 sub-events and 0x02CF to CharacterStats.
/// </summary>
internal class GameEventRouter
{
private readonly IPluginLogger _logger;
internal GameEventRouter(IPluginLogger logger)
{
_logger = logger;
}
internal void OnServerDispatch(object sender, NetworkMessageEventArgs e)
{
try
{
if (e.Message.Type == Constants.GameEventMessageType)
{
int eventId = (int)e.Message["event"];
if (eventId == Constants.AllegianceInfoEvent)
{
CharacterStats.ProcessAllegianceInfoMessage(e);
}
else if (eventId == Constants.LoginCharacterEvent)
{
CharacterStats.ProcessCharacterPropertyData(e);
}
else if (eventId == Constants.TitlesListEvent)
{
CharacterStats.ProcessTitlesMessage(e);
}
else if (eventId == Constants.SetTitleEvent)
{
CharacterStats.ProcessSetTitleMessage(e);
}
}
else if (e.Message.Type == Constants.PrivateUpdatePropertyInt64)
{
CharacterStats.ProcessPropertyInt64Update(e);
}
}
catch (Exception ex)
{
_logger?.Log($"[CharStats] ServerDispatch error: {ex.Message}");
}
}
}
}

View file

@ -304,8 +304,10 @@
<Compile Include="..\Shared\VCS_Connector.cs"> <Compile Include="..\Shared\VCS_Connector.cs">
<Link>Shared\VCS_Connector.cs</Link> <Link>Shared\VCS_Connector.cs</Link>
</Compile> </Compile>
<Compile Include="ChatEventRouter.cs" />
<Compile Include="CommandRouter.cs" /> <Compile Include="CommandRouter.cs" />
<Compile Include="Constants.cs" /> <Compile Include="Constants.cs" />
<Compile Include="GameEventRouter.cs" />
<Compile Include="IPluginLogger.cs" /> <Compile Include="IPluginLogger.cs" />
<Compile Include="InventoryMonitor.cs" /> <Compile Include="InventoryMonitor.cs" />
<Compile Include="KillTracker.cs" /> <Compile Include="KillTracker.cs" />

View file

@ -130,6 +130,8 @@ namespace MosswartMassacre
private KillTracker _killTracker; private KillTracker _killTracker;
private RareTracker _rareTracker; private RareTracker _rareTracker;
private InventoryMonitor _inventoryMonitor; private InventoryMonitor _inventoryMonitor;
private ChatEventRouter _chatEventRouter;
private GameEventRouter _gameEventRouter;
private CommandRouter _commandRouter; private CommandRouter _commandRouter;
protected override void Startup() protected override void Startup()
@ -171,25 +173,6 @@ namespace MosswartMassacre
} }
} }
// Note: Startup messages will appear after character login
// Subscribe to chat message event
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(OnChatText);
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(AllChatText);
CoreManager.Current.CommandLineText += OnChatCommand;
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
CoreManager.Current.CharacterFilter.Death += OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject += OnSpawn;
CoreManager.Current.WorldFilter.CreateObject += OnPortalDetected;
CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn;
// Initialize inventory monitor (taper tracking)
_inventoryMonitor = new InventoryMonitor(this);
_staticInventoryMonitor = _inventoryMonitor;
CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject += _inventoryMonitor.OnInventoryChange;
// Initialize VVS view after character login
ViewManager.ViewInit();
// Initialize kill tracker (owns the 1-sec stats timer) // Initialize kill tracker (owns the 1-sec stats timer)
_killTracker = new KillTracker( _killTracker = new KillTracker(
this, this,
@ -198,6 +181,36 @@ namespace MosswartMassacre
_staticKillTracker = _killTracker; _staticKillTracker = _killTracker;
_killTracker.Start(); _killTracker.Start();
// Initialize inventory monitor (taper tracking)
_inventoryMonitor = new InventoryMonitor(this);
_staticInventoryMonitor = _inventoryMonitor;
// Initialize chat event router (rareTracker set later in LoginComplete)
_chatEventRouter = new ChatEventRouter(
this, _killTracker, null,
count => ViewManager.UpdateRareCount(count),
msg => MyHost?.Actions.InvokeChatParser($"/a {msg}"));
// Initialize game event router
_gameEventRouter = new GameEventRouter(this);
// Note: Startup messages will appear after character login
// Subscribe to events
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(_chatEventRouter.OnChatText);
CoreManager.Current.ChatBoxMessage += new EventHandler<ChatTextInterceptEventArgs>(ChatEventRouter.AllChatText);
CoreManager.Current.CommandLineText += OnChatCommand;
CoreManager.Current.CharacterFilter.LoginComplete += CharacterFilter_LoginComplete;
CoreManager.Current.CharacterFilter.Death += OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject += OnSpawn;
CoreManager.Current.WorldFilter.CreateObject += OnPortalDetected;
CoreManager.Current.WorldFilter.ReleaseObject += OnDespawn;
CoreManager.Current.WorldFilter.CreateObject += _inventoryMonitor.OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject += _inventoryMonitor.OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject += _inventoryMonitor.OnInventoryChange;
// Initialize VVS view after character login
ViewManager.ViewInit();
// Initialize vitals streaming timer // Initialize vitals streaming timer
vitalsTimer = new Timer(Constants.VitalsUpdateIntervalMs); vitalsTimer = new Timer(Constants.VitalsUpdateIntervalMs);
vitalsTimer.Elapsed += SendVitalsUpdate; vitalsTimer.Elapsed += SendVitalsUpdate;
@ -215,7 +228,7 @@ namespace MosswartMassacre
// 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(this); CharacterStats.Init(this);
CoreManager.Current.EchoFilter.ServerDispatch += EchoFilter_ServerDispatch; CoreManager.Current.EchoFilter.ServerDispatch += _gameEventRouter.OnServerDispatch;
// Enable TLS1.2 // Enable TLS1.2
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
@ -258,9 +271,9 @@ namespace MosswartMassacre
// Unsubscribe from chat message event // Unsubscribe from chat message event
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(OnChatText); CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(_chatEventRouter.OnChatText);
CoreManager.Current.CommandLineText -= OnChatCommand; CoreManager.Current.CommandLineText -= OnChatCommand;
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(AllChatText); CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(ChatEventRouter.AllChatText);
CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath; CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject -= OnSpawn; CoreManager.Current.WorldFilter.CreateObject -= OnSpawn;
CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected; CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected;
@ -273,7 +286,7 @@ namespace MosswartMassacre
CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange; CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange;
} }
// Unsubscribe from server dispatch // Unsubscribe from server dispatch
CoreManager.Current.EchoFilter.ServerDispatch -= EchoFilter_ServerDispatch; CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch;
// Stop kill tracker // Stop kill tracker
_killTracker?.Stop(); _killTracker?.Stop();
@ -372,9 +385,10 @@ namespace MosswartMassacre
WriteToChat($"[ChestLooter] Initialization failed: {ex.Message}"); WriteToChat($"[ChestLooter] Initialization failed: {ex.Message}");
} }
// Initialize rare tracker // Initialize rare tracker and wire to chat router
_rareTracker = new RareTracker(this); _rareTracker = new RareTracker(this);
_staticRareTracker = _rareTracker; _staticRareTracker = _rareTracker;
_chatEventRouter.SetRareTracker(_rareTracker);
// Apply the values // Apply the values
_rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled; _rareTracker.RareMetaEnabled = PluginSettings.Instance.RareMetaEnabled;
@ -800,39 +814,6 @@ namespace MosswartMassacre
// $"[Despawn] {mob.Name} @ (NS={c.NorthSouth:F1}, EW={c.EastWest:F1})"); // $"[Despawn] {mob.Name} @ (NS={c.NorthSouth:F1}, EW={c.EastWest:F1})");
} }
private async void AllChatText(object sender, ChatTextInterceptEventArgs e)
{
try
{
string cleaned = NormalizeChatLine(e.Text);
// Send to WebSocket
await WebSocket.SendChatTextAsync(e.Color, cleaned);
// Note: Plugin message analysis is now handled by Harmony patches
}
catch (Exception ex)
{
PluginCore.WriteToChat($"[WS] Chat send failed: {ex}");
}
}
private static string NormalizeChatLine(string raw)
{
if (string.IsNullOrEmpty(raw))
return raw;
// 1) Remove all <…> tags
var noTags = Regex.Replace(raw, "<[^>]+>", "");
// 2) Trim trailing newline or carriage-return
var trimmed = noTags.TrimEnd('\r', '\n');
// 3) Collapse multiple spaces into one
var collapsed = Regex.Replace(trimmed, @"[ ]{2,}", " ");
return collapsed;
}
private void OnCharacterDeath(object sender, Decal.Adapter.Wrappers.DeathEventArgs e) private void OnCharacterDeath(object sender, Decal.Adapter.Wrappers.DeathEventArgs e)
{ {
@ -874,40 +855,8 @@ namespace MosswartMassacre
} }
} }
private void OnChatText(object sender, ChatTextInterceptEventArgs e)
{
try
{
_killTracker.CheckForKill(e.Text);
if (_rareTracker != null && _rareTracker.CheckForRare(e.Text, out string rareText))
{
_killTracker.RareCount = _rareTracker.RareCount;
ViewManager.UpdateRareCount(_rareTracker.RareCount);
}
if (e.Color == 18 && e.Text.EndsWith("!report\""))
{
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: {_rareTracker?.RareCount ?? 0}";
WriteToChat($"[Mosswart Massacre] Reporting to allegiance: {reportMessage}");
MyHost.Actions.InvokeChatParser($"/a {reportMessage}");
}
}
catch (Exception ex)
{
WriteToChat("Error processing chat message: " + ex.Message);
}
}
private void OnChatCommand(object sender, ChatParserInterceptEventArgs e) private void OnChatCommand(object sender, ChatParserInterceptEventArgs e)
{ {
try try
@ -983,41 +932,6 @@ namespace MosswartMassacre
} }
} }
private void EchoFilter_ServerDispatch(object sender, NetworkMessageEventArgs e)
{
try
{
if (e.Message.Type == Constants.GameEventMessageType)
{
int eventId = (int)e.Message["event"];
if (eventId == Constants.AllegianceInfoEvent)
{
CharacterStats.ProcessAllegianceInfoMessage(e);
}
else if (eventId == Constants.LoginCharacterEvent)
{
CharacterStats.ProcessCharacterPropertyData(e);
}
else if (eventId == Constants.TitlesListEvent)
{
CharacterStats.ProcessTitlesMessage(e);
}
else if (eventId == Constants.SetTitleEvent)
{
CharacterStats.ProcessSetTitleMessage(e);
}
}
else if (e.Message.Type == Constants.PrivateUpdatePropertyInt64)
{
CharacterStats.ProcessPropertyInt64Update(e);
}
}
catch (Exception ex)
{
WriteToChat($"[CharStats] ServerDispatch error: {ex.Message}");
}
}
public static void WriteToChat(string message) public static void WriteToChat(string message)
{ {