From 09f8fe5fbf933e0ceb88de643223b4f9b7afbe53 Mon Sep 17 00:00:00 2001 From: EmoSaru Date: Sun, 17 May 2026 15:44:10 -0700 Subject: [PATCH] Fix autotracker reload lockup, stale state, and NWA address map detection (#93) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AutoTrackerExtension.OnAnyPackageLoadStarting: fully stop autotracking on pack reload instead of preserving the connection. Previously the extension was left in an "active/running" state that didn't actually refresh after reload — user had to manually stop+start. Now the reload drains the in-flight poll, disconnects the active provider, fires AutoTrackerStopped, and emits property-change for Active / StatusBarControl so bindings reflect the stopped state immediately. - LuaMemorySegment.OnSegmentDataUpdated: re-read mCallback inside the dispatched lambda and check ScriptManager.IsLuaLoaded before invoking SafeCall. A Dispatch.BeginInvoke posted from an in-flight poll just before reload would otherwise land after ScriptManager.Reset closed the Lua state, hanging the UI thread inside NLua — issue #93. - NwaDevice.ReadRawDomainAsync: replace stray space with the protocol's ';' separator in the CORE_READ command used by the SNES address-map initializer. The malformed command was rejected by snes9x-nwa as a protocol error, causing ROM-header reads to throw and SNES layout detection to silently fall back to LoROM regardless of the actual cartridge mapping. Co-Authored-By: Claude Opus 4.7 --- .../AutoTracking/LuaMemorySegment.cs | 14 +++++-- .../AutoTracker/AutoTrackerExtension.cs | 37 ++++++++++--------- EmoTracker/Providers/NWA/NwaDevice.cs | 2 +- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/EmoTracker.Data/AutoTracking/LuaMemorySegment.cs b/EmoTracker.Data/AutoTracking/LuaMemorySegment.cs index 28e77d0..7db262c 100644 --- a/EmoTracker.Data/AutoTracking/LuaMemorySegment.cs +++ b/EmoTracker.Data/AutoTracking/LuaMemorySegment.cs @@ -62,8 +62,7 @@ protected override void OnSegmentDataUpdated() if (state == null) return; var scripts = state.Scripts; if (scripts == null) return; - var callback = mCallback; - if (callback == null) return; + if (mCallback == null) return; // Memory polling fires on a worker thread. The callback may // mutate items / locations, both of which expect UI-thread @@ -74,10 +73,19 @@ protected override void OnSegmentDataUpdated() { try { + // Re-read mCallback at execution time. A pack reload + // between the queue-time check above and now would have + // torn down our Lua state and called Dispose() on this + // segment (which nulls mCallback). Invoking SafeCall + // with a LuaFunction whose underlying state is closed + // hangs the UI thread inside NLua — issue #93. + var liveCallback = mCallback; + if (liveCallback == null) return; + if (!scripts.IsLuaLoaded) return; object[] result; using (new LocationDatabase.SuspendRefreshScope(state.Locations)) { - result = scripts.SafeCall(callback, this); + result = scripts.SafeCall(liveCallback, this); } bool succeeded = result != null && result.Length > 0 && result[0] != null && !(result[0] is bool b && !b); diff --git a/EmoTracker/Extensions/AutoTracker/AutoTrackerExtension.cs b/EmoTracker/Extensions/AutoTracker/AutoTrackerExtension.cs index a4ebfcf..bc3d24c 100644 --- a/EmoTracker/Extensions/AutoTracker/AutoTrackerExtension.cs +++ b/EmoTracker/Extensions/AutoTracker/AutoTrackerExtension.cs @@ -186,27 +186,28 @@ void OnAnyPackageLoadStarting(object sender, EmoTracker.Data.Sessions.PackageLoa if (!ReferenceEquals(e.Target, mState)) return; // The state's Lua interpreter is about to be Reset() — close + - // re-open. Drop our memory segments now so we don't carry - // callbacks that reference the soon-to-be-closed interpreter. - // StopAutoTracking first to drain any in-flight poll cleanly; - // the active provider connection itself is preserved (the user - // chose to stay connected across reloads), but the watch list - // is rebuilt by the new init.lua's AddMemoryWatch calls. + // re-open. Fully stop autotracking before the reload: keeping + // the connection alive across reload left the extension stuck + // in an "active/running" state that didn't actually refresh + // (the new pack's init.lua re-registers watches against the + // new Lua state, but the throttle / dirty bookkeeping carried + // from the old run prevents updates from landing until the + // user manually stops and restarts). // - // Without this hook, the next memory poll after reload+ - // reconnect invokes SafeCall on a LuaFunction whose underlying - // Lua state is closed, which hangs the UI thread inside NLua. - bool wasConnected = ActiveProvider != null; - var preservedProvider = SelectedProvider; - - // Drain in-flight + dispose stale segments. Note Clear() also - // clears the pending update queue. + // StopAutoTracking drains any in-flight poll, disconnects the + // active provider, and fires the AutoTrackerStopped callback. + // Clear() then drops the pending-update queue. The user will + // re-Start once the pack finishes loading. + StopAutoTracking(); Clear(); - // Restore the connection target so the user doesn't have to - // re-pick it after init.lua repopulates the watch list. - if (preservedProvider != null) - SelectedProvider = preservedProvider; + // Surface the now-stopped state to bindings; OnAnyPackageLoad- + // Complete will re-emit these too once new segments / timers + // are registered, but raise them here so the status bar + // reflects "not running" during the reload window rather than + // showing stale running state. + NotifyPropertyChanged(nameof(Active)); + NotifyPropertyChanged(nameof(StatusBarControl)); } void OnAnyPackageLoadComplete(object sender, EmoTracker.Data.Sessions.PackageLoader.PackageLoadEventArgs e) diff --git a/EmoTracker/Providers/NWA/NwaDevice.cs b/EmoTracker/Providers/NWA/NwaDevice.cs index 1f954de..1598ad3 100644 --- a/EmoTracker/Providers/NWA/NwaDevice.cs +++ b/EmoTracker/Providers/NWA/NwaDevice.cs @@ -190,7 +190,7 @@ static INwaAddressMap CreatePlatformAddressMap(GamePlatform platform) /// async Task ReadRawDomainAsync(string domain, ulong offset, int length) { - string command = $"CORE_READ {domain} ${offset:X};${length:X}"; + string command = $"CORE_READ {domain};${offset:X};${length:X}"; byte[] data = await SendReadCommandAsync(command).ConfigureAwait(false); return data ?? Array.Empty(); }