Fix hot reload: prevent duplicate event handlers and timer leaks

Unsubscribe all event handlers and stop/dispose timers at the start of
Startup() before re-creating objects. On first load the -= calls are
no-ops; on hot reload they remove stale handlers that would otherwise
compound with each reload. Also adds LoginComplete unsubscription to
Shutdown() for completeness.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
erik 2026-02-28 12:55:58 +00:00
parent 1ffa163501
commit aed74984c6
2 changed files with 37 additions and 0 deletions

View file

@ -163,6 +163,42 @@ namespace MosswartMassacre
var isCharacterLoaded = CoreManager.Current.CharacterFilter.LoginStatus == 3;
var needsHotReload = IsHotReload || isCharacterLoaded;
// Clean up old event subscriptions to prevent duplicates on hot reload.
// C# -= with a non-subscribed handler is a no-op, so safe on first load.
if (_chatEventRouter != null)
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;
if (_inventoryMonitor != null)
{
CoreManager.Current.WorldFilter.CreateObject -= _inventoryMonitor.OnInventoryCreate;
CoreManager.Current.WorldFilter.ReleaseObject -= _inventoryMonitor.OnInventoryRelease;
CoreManager.Current.WorldFilter.ChangeObject -= _inventoryMonitor.OnInventoryChange;
}
if (_gameEventRouter != null)
CoreManager.Current.EchoFilter.ServerDispatch -= _gameEventRouter.OnServerDispatch;
WebSocket.OnServerCommand -= HandleServerCommand;
// Stop old timers before recreating (prevents timer leaks on hot reload)
_killTracker?.Stop();
if (vitalsTimer != null)
{
vitalsTimer.Stop();
vitalsTimer.Dispose();
vitalsTimer = null;
}
if (commandTimer != null)
{
commandTimer.Stop();
commandTimer.Dispose();
commandTimer = null;
}
// Initialize kill tracker (owns the 1-sec stats timer)
_killTracker = new KillTracker(
this,
@ -280,6 +316,7 @@ namespace MosswartMassacre
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(_chatEventRouter.OnChatText);
CoreManager.Current.CommandLineText -= OnChatCommand;
CoreManager.Current.ChatBoxMessage -= new EventHandler<ChatTextInterceptEventArgs>(ChatEventRouter.AllChatText);
CoreManager.Current.CharacterFilter.LoginComplete -= CharacterFilter_LoginComplete;
CoreManager.Current.CharacterFilter.Death -= OnCharacterDeath;
CoreManager.Current.WorldFilter.CreateObject -= OnSpawn;
CoreManager.Current.WorldFilter.CreateObject -= OnPortalDetected;