NightElfX01: Rise of the Naga flow investigation
Confirmed failure: cinematic trackers trigger the rescue sequence
The September 27, 2026 comparison used the installed TFT NightElfX01.w3x
from War3xlocal.mpq, retail 1.27.1.7085 under Wine, and OpenRealm with
skip_cutscene=0. Retail game.dll SHA-256:
d51e5680243fc90e19c9d6074f7fac433c466d3cf5f46e2364291725574d8236.
Recordings and Frida traces are retained locally under build/elves01-audit/;
they are ignored diagnostic artifacts, not portable test fixtures.
The opening's main dialogue/order sequence ran in both implementations. The first proven story-order divergence was the cleanup handoff:
| Milestone | Retail | OpenRealm before fix |
|---|---|---|
| Intro cleanup | Removes all fourteen trackers from two seven-unit groups | Skips alternating members and repeatedly removes the last member |
| Player handoff | Maiev's group assembles at the starting gate | Surviving cinematic trackers keep moving elsewhere on the map |
| Next story event without gameplay input | Discover the Illidan main quest | Rescue Archers, Wildkin voiceover, then Wildkin cinematic |
| Main quest | Discovered after the authored five-second gameplay delay | Waits behind the unintended queued rescue/Wildkin sequence |
The live native trace recorded Trig_Rescue_Archers_Actions after cleanup
without any gameplay input. A subsequent bounded headless trace identified the
cause in simulation time: cleanup at 37.1 seconds; surviving Archer edict 4244
entered the rescue rectangle at 39.8 seconds, at approximately
(-3881.88, -5178.20). The authored gg_rct_ArcherRescueGroup01 rectangle is
(-5472, -5184) to (-3520, -4000). The main quest was not discovered until
71.0 seconds, after the unintended queue entries.
Edict numbers and timing are observations from that run, not constants to put
in game logic. Maiev's six SetUnitPositionLoc calls did reach their authored
destinations: the camera jump was caused by the rescue script, not a failed
teleport. That script pans to gg_rct_Rescue01Huntress_Dest when outside a
cinematic, transfers three units, and enqueues the Wildkin dialogue/cinematic.
Root cause and ownership
Trig_Intro_Cleanup_Actions calls ForGroupBJ with callbacks that invoke
RemoveUnit(GetEnumUnit()). G_DeferFreeEdict immediately removes the unit
from every JASS group. ForGroup previously walked the mutable member array
while FOR_LOOP retained its initial count. Compaction skipped the next
member; later iterations read repeated entries beyond the new logical count.
The headless trace showed cleanup visiting:
first group: 4236, 4238, 4240, 4255, 4255, 4255, 4255
second group: 4241, 4243, 4245, 4254, 4254, 4254, 4254
Therefore a count of fourteen RemoveUnit calls was insufficient evidence of
successful cleanup: only eight distinct trackers were removed. The identity
of each removed unit matters.
The fix belongs to the generic ForGroup native. It snapshots member pointers
and spawn identities before calling JASS, skips removed/replaced entities,
and restores the enclosing enumeration unit after nested callbacks. There is
no campaign-specific branch. Group mutations affect subsequent enumerations;
they cannot shift the active enumeration's remaining members.
The local Ghidra analysis corroborates the snapshot contract in this retail
binary: native registration maps ForGroup to 0x6f1fe690, which calls
0x6f22ff30; its iterator at 0x6f22ed70 constructs a temporary retained
array through 0x6f2939f0, validates each agent's identity before its callback,
and releases the temporary references afterward. These are preferred-image
addresses (base 0x6f000000); Frida hooks must relocate against the actual
module base. Decompiler output alone is not a complete native ABI declaration.
Verification and evidence
wc3_jass_map.nightelfx01_cleanup_removes_every_tracker recreates the two
seven-unit groups and the real JASS ForGroup -> RemoveUnit path using fixture
units. It failed before the fix with cinematic tracker survived cleanup.
After draining deferred removals, it checks every original handle and both
groups, rather than only callback counts.
wc3_jass_map.forgroup_snapshot_survives_nested_mutation covers nested
enumeration, removal of a future member, group clearing/destruction, and
exclusion of newly added members.
Both ROC and TFT passed 67 JASS-map tests (175 assertions) and 1,944 Warcraft
tests (34,942 assertions) per mode. The aggregate make test TEST_JOBS=4 run
was blocked by an SDL crash in
client_input.menu_sdl_input_is_exclusive_with_world_presentation; that test
also crashed in isolation with SDL_VIDEODRIVER=dummy. Its stack and the
aggregate log are retained as full-test-crash.txt and full-test.log.
The concurrent client-input changes were not modified by this investigation.
Useful local artifacts:
retail-intro.mp4,retail-traced.mp4: retail opening and handoff recordings;native-intro.mp4: original native capture, including the premature rescue;retail-flow.jsonl,native-flow.jsonl: native-call order and timestamps;headless-flow.jsonl,causal-excerpt.json: unit identity, position, region entry, and simulation-time evidence;native-addresses.json,retail-forgroup-decompiled.json: retail hook map and iterator disassembly/decompilation evidence;regression-before-final.log,regression-after-final.log: automated regression results.
Use the authored cleanup regression for subsequent validation rather than replaying the opening to re-prove this bug.
Comparison pitfalls and remaining scope
build/parity/wc3 openrealm --map=elf1setsskip_cutscene=1; this also shortens later cinematics. Use--introfor normal timing in both runs.- The Xvfb renderer used software rasterization. Raw wall-clock durations are not simulation-time parity measurements. Align script milestones and record simulation time when available.
- Wine resolved the same installed DLL as
K:\Games\w3\Game.dll, while the existing audio probe expected aZ:path. Its initial rejected trace was discarded; compare resolved files and hashes before changing a path check. - An outdated engine executable paired with newer renderer modules crashed
in loading-screen
R_SelectUISequence. Rebuilding the engine restored the comparison. Those failed runs are not JASS-flow evidence. - War3Net/wc3libs provide script/format references; the installed mission and the hash-matched running retail binary are the authority for this finding.
- This establishes one cause of out-of-order campaign flow. It does not certify the village encounters, Watcher prison rescues, harbor/ship objectives, or victory sequence end to end. The optional Wildkin cleanup uses the same affected enumeration pattern, but those later gameplay sequences need their own controlled comparisons.
See also JASS groups, trigger dispatch, and the campaign audit.
See portrait animation and destructable hover parity for the retail comparison and renderer/snapshot regressions.