Autopilot · write-up
Native capture and lifecycle session (ABI 85)
Hatari integration (protocol 16, 2026-09-13)
Hatari can now drive Xenon directly through a resident C session. The normal path needs no Python process, control socket, JSON/hex encoding, ctypes call, or per-frame pause handshake. The standalone shared library remains available to Python tests and reference runs. Hatari links the same sources statically; the browser target includes the same adapter and exports the lifecycle API (no browser build or WASM performance measurement was performed in this step).
C lifecycle
Include src/includes/xenonControl.h and call on the emulator thread after RAM initialization:
if (!XenonControl_StartAutopilot(NULL)) {
/* Allocation failed, RAM is unavailable, or another controller owns input. */
}
/* Emulation advances normally; the frame hook calls xap_session_step internally. */
XenonControl_StopAutopilot();
/* A subsequent start creates a fresh session. */
StartAutopilot(config): NULL selects standard maneuver/spans budgets and the 2/1 fire pulse. Starting an already active pilot succeeds without resetting it or changing its configuration.StopAutopilot(): idempotently frees resident state, clears held direction/fire and shop latches, and releases input to normal Hatari controls. It never releases a different controller's input.IsAutopilotRunning(): reports whether the resident native owner is enabled.GetAutopilotStatus(output): copies compact status on demand; returns false before the first decision or after stopping. No world/geometry inspection is performed by normal driving.
Neither start nor stop changes the emulator's pause state. Port 0 disables the optional TCP service only. External CLAIM/SET/RELEASE input requests are rejected while the native pilot owns input; stop it explicitly before switching controllers. An observer disconnect does not stop it. A start in the middle of a draw pass discards that partial capture. Snapshot restoration rebuilds world/map/mission history and synchronizes live RAM again while retaining native ownership and configuration. It replans from restored facts; this adapter does not yet save the session's menu acknowledgement/fire-phase resume payload alongside emulator snapshots.
Module and timing boundaries
src/xenonCapture.candsrc/includes/xenonCapture.h: shared RAM addresses, capture records, collision/equipment/shop sampling, pending wave scripts. Recording and native driving use the same extraction helpers rather than separate sets of magic addresses.src/xenonAutopilot.c: resident session lifetime, reusable object/draw/raw buffers and direct typed input assembly. The first frame of each level borrows ST RAM for destructible-map sync; ordinary frames do not copy a RAM image. Capture overflow stops the pilot rather than silently dropping hazards.src/xenonControl.c: input ownership, canonical hook, existing ship/shop input consumers, optional recording, TCP commands and explicit debug stepping.
At the canonical frame boundary, recording (if enabled) first captures the completed frame's actual inputs. C then evaluates that same capture and installs the next mask before the next ship/shop input consumer. There is no additional queued frame of input delay. The shop's accepted direction latch survives intervening redraws, as in the existing external controller path.
Continuous native driving skips BuildFrameBatch entirely unless a frame subscriber or event
recording needs it. Explicit single-step still waits inside the canonical hook: merely setting
Hatari's outer pause flag permits extra shop redraws before the CPU loop returns. This wait is
only a debugging operation; there is no wait or client dependency during ordinary native play.
Recording and test control
The existing START_LOG/STOP_LOG commands still write .x2events. Protocol 16 adds one native-owner
byte in control trailer 0x5840; older recording versions remain readable. Replay discovers
maneuver/spans/C metadata directly from native recordings without a Python trace sidecar, and
shows native versus external/manual ownership for the selected frame. Detailed planner geometry
is reconstructed only when replay requests inspection.
For an already running matching Hatari build:
python xenon_tools/xenon_client.py --port 6943 start-autopilot
python xenon_tools/xenon_client.py --port 6943 run
python xenon_tools/xenon_client.py --port 6943 stop-autopilot
These are optional one-off commands (24/25) calling the public C lifecycle, not a Python driving loop. Use the updated client with protocol 16. STEP keeps the native decision; its supplied legacy joystick byte does not override the native pilot.
Reusable isolated live validation:
python xenon_tools/validate_native_hatari.py --port 6943 --snapshot assets/xenonplay.sav --recording xenon_tools/run_logs/new-native-test.x2events --frames 500
The script replaces the dedicated emulator's state and leaves it paused with native control released. It checks decisions against the Python-hosted C session, repeated lifecycle calls, input ownership conflicts, exact stepping, disconnected continuous operation, snapshot reset, and binary recording. It is a functional check, not a throughput benchmark.
Accepted live checks: 500 gameplay frames and 120 shop frames, zero control mismatches. The pilot continued for 30 gameplay / 45 shop redraw frames with no client or recorder during two-second intervals. Both snapshot-reset/ownership checks passed. Reports and recordings:
xenon_tools/run_logs/native-hatari-gameplay-0913-fixed.validation.jsonxenon_tools/run_logs/native-hatari-shop-0913-fixed.validation.json
The Hatari executable and standalone Python DLL build successfully. All 1,010 affected Python tests pass, along with the standalone C session smoke test. A real native recording was opened through the replay model and identified as maneuver/spans/C without a sidecar.
The first shop check exposed extra frames during explicit stepping; the corrected boundary wait passed the repeat. All captured shop decisions matched even in the initial run. These checks do not constitute full-campaign acceptance or a controlled end-to-end performance measurement.
Reusable continuous campaign validation
xenon_tools/validate_native_campaign.py observes continuous native driving. It does not call
Python policy/prediction or step the game between decisions. Start a dedicated emulator first:
powershell -File xenon_tools/hatari_dev.ps1 run -ControlPort 6944 -Hidden -NoWinConsole
python xenon_tools/validate_native_campaign.py --port 6944 --output xenon_tools/run_logs/my-native-campaign --fast-forward
The output directory must be new. --snapshot defaults to assets/xenonplay.sav; the selected
emulator is reset to it. Omit --fast-forward for normal-speed playback. Outputs are
campaign.x2events, incidents.jsonl, incidents.md, summary.json, initial terrain RAM and
an end snapshot. Paired campaign-original.avi and campaign-sprite.avi videos are now
recorded by default; --no-avi explicitly disables them, and --avi-png-level controls compression.
Replay automatically discovers these filenames and their VBL alignment sidecars. Each incident has the replay filename, canonical frame number and a one-sentence
problem description. Damage and life loss on the same frame are separate incidents. The report
is flushed as events happen, so it can be read during the campaign.
A wall stall means a small position range with repeated collision state over 150 frames. The broader detector reports 900 frames without meaningful position or score progress; this is a suspected stall, not proof that a stationary attack cannot eventually succeed. Shop stalls track unchanged cursor, cash and acknowledgement state. The script stops after 3,000 frames of persistent stagnation, on final completion or no lives remaining, and records the reason without calling an incomplete run successful. All thresholds are CLI options. It also rejects capture gaps/stream overflow instead of silently omitting incident frames.
Create a file named STOP in the output directory to end gracefully; the script pauses Hatari,
releases native input and finishes the recording. --frames N is an optional bounded smoke run,
not a full-campaign success. Five detector tests cover damage/life loss, stall episode grouping,
recovery, wall/menu stalls and capture gaps (python -m unittest test_native_campaign, with
PYTHONPATH=xenon_tools).
First continuous campaign result (2026-09-13)
The campaign failed in Level 2; levels 3–5 are not validated. Three recorded segments cover 12,318 captured frames, with two explicitly recorded fresh-session interventions and no external health/lives/cash/position changes. There were eleven damage/hit incidents, one lost life, one persistent Level 2 stall and two native-controller shutdowns. The emulator was left paused.
- First shop entry: C rejected decision frame 3854; ownership was off in frame 3855.
- Level 2: stall confirmed at 11395, beginning at 10496 near world (164,4153), and still present at 13496. Starting a fresh C session from the same saved state cleared the stall.
- After damage and life loss at 14064: C rejected frame 14115, with ownership off at 14116.
Both rejected decisions reproduce by passing their recordings to the Python-hosted C session;
this is not solely a live socket/monitor issue. Investigation and fixes are still required before
claiming full-campaign native acceptance. The existing short live comparisons did not cover these
transitions. The complete per-occurrence table and replay links are in
xenon_tools/run_logs/native-campaign-20260913-report.md; individual folders retain event logs,
initial RAM, final emulator snapshots, incident lists and summaries. The recorded segments are
native-campaign-20260913-reusable, native-campaign-20260913-part2, and native-campaign-20260913-part3.
Replay inspection corrections
Record count and game-frame number are distinct. The first campaign segment has 2,061 records, covering canonical frames 1797-3857. Frame 2174 is record 378; frame 3854 is record 2058. Replay startup now reports both the count and range, and the toolbar shows the selected game frame beside the record position.
Frame 2174 records a four-damage enemy projectile hit from identity 465. At collision time, the projectile's inclusive bounds were (103,110)-(108,115), and the player's were (107,115)-(128,135): a 2-by-1-pixel overlap. The projectile was already in its destruction update by the subsequent object/draw capture. The scene overlay now draws recorded collision-time source/player bounds and their overlap, translating the captured scroll to the displayed scroll; these are separate from current object geometry or a predicted future collision.
When C rejects a replay decision, the inspector now abandons the failed session and continues showing captured objects, memory and hits with an explicit analysis-unavailable error. It does not fabricate a replacement planner decision or retry the broken session on every frame. Rewinding to an earlier valid checkpoint restores normal analysis. The original recording was verified through frame 3857, including cached seeks before and after rejected frame 3854.
The original campaign had no AVIs; those cannot be recovered from its event stream alone.
A fresh 450-record run with both AVIs reproduced exactly the same captured hit at frame 14495
(record 378), including its source identity, collision cycle and bounds:
xenon_tools/run_logs/native-collision-avi-20260913/campaign.x2events.
Both AVI streams decode successfully and select exact VBL tags at that incident. This new clip
is a reproduced run, not video retroactively recovered from the old recording.
Stage 1: shop policy and acknowledgements
xenon_autopilot_shop_plan.c owns budget allocation, generated recipes, mount compatibility
and surplus purchases. xenon_autopilot_shop.c owns cursor navigation, fire/release edges,
quote settling, cash-animation acknowledgements and page rebuilding. Neither requires Python
callbacks. Fixed arrays match the seven physical attachment slots, twenty menu cells and
25 merchandise types. No per-frame dictionaries, sets or plan allocations are required.
The normal policy is preserved: fund required capabilities before optional upgrades, sell only funded replacements, retain Side/Rear incompatibility checks, and avoid temporary Nashwan purchases. Acknowledgement failures return an error instead of firing indefinitely into uncertain menu state. Compact transaction counters are retained; detailed transaction history can be recorded by observers.
Regenerate item identifiers, extracted prices/mounts and reviewed recipe tables manually:
python xenon_tools/generate_native_shop_data.py
python xenon_tools/generate_native_shop_data.py --check
The aggregate generate_native_assets.py includes both shop outputs. Generation is never a build
step. Source data remains in the existing extracted pickup catalog and reviewed shop_strategy.py
recipes; the Python planner/controller remain the reference implementation.
Validation: 500 randomized inventory/budget/level/visit comparisons match Python purchase limits, order, funded sales and projected cash. Menu sequences match quote settling, release edges, animated cash and exit-button navigation. The three new native-shop tests pass; standalone build passes. These functions are now called inside the complete C session; production Python does not call a shop kernel separately.
Complete captured-frame entry point
xap_session_step(session, input, output) now produces the final joystick mask, including fire.
It owns the canonical world, persistent map, collision/navigation environment, mission/evaluator,
shop acknowledgements and input phases. It accepts raw object/draw capture, typed memory fields,
pending dispatcher records and optional RAM. It needs no Python callback, JSON file or host-side
mission/player/map preparation. Input buffers need survive only the call; borrowed diagnostic
outputs survive until the next step or free.
The implementation is separated by responsibility:
xenon_autopilot_session.c: owner lifetime, frame deltas and the play/shop/respawn/transition/completion branches.xenon_autopilot_session_drive.c: bind resident maps and geometry, prepare pending spawns, invoke the driver and apply fire timing.xenon_autopilot_session_checkpoint.c: explicit snapshot resume of shop acknowledgement and input phases.- Existing level files select tactics; the existing driver and evaluator verify safety and combat.
xenon_autopilot_shop_plan.candxenon_autopilot_shop.c: purchase policy and menu protocol respectively.
Captured inventory uses numeric item IDs and weapon bits. Global collision bounds are retained for next-frame newborn classification. Death resets the driver; temporary absence skips prediction without destroying the mission. Level changes and rewinds invalidate the relevant owners. Repeated reads of the same frame do not advance fire/menu state. Exact native movement models are prepared inside the session; unused eager velocity fitting stays disabled.
Python boundary and observability
The standard autoplay.py live loop and raw ReplayModel path select the complete session with
--planner maneuver --kernel-backend c --navigation-backend spans and shipped collision assets.
native_session.py only serializes capture and owns the C handle. It makes one session call per
frame. native_session_host.py publishes compact status to existing tooling.
native_session_views.py is permanent, optional observability support. World objects, game-memory
models, historical objects, map cells, routes and predicted geometry are created/read only when a
catalog, trace, console diagnostic, UI or explicit inspection requests them. Normal driving does not
call WorldState.update, GameMemoryState.from_frame, the Python map updater, mission preparation,
fire scheduler or shop controller. Replay's cache_frames=0/--driving-only also avoids checkpoint
inspection. Live scalar damage/score accounting remains available without exporting objects.
Set XENON_NATIVE_SESSION=0 to retain the previous component path. The Python kernel, legacy traces
with object overrides, custom atlases and unsupported component configurations retain their reference
paths. Old Python-only live resume files also retain that path; newly saved native resume files select
the C session. This compatibility code is separate from the C driving implementation.
Trace metrics identify session_backend: c, native call counts and the lifecycle stage. Replay reads
this metadata and shows the recorded/analyzed session backend. Native shop diagnostics expose phase
and target item; richer shop state is available through the explicit xap_session_shop accessor.
Checkpoints and memory lifetime
xap_session_clone preserves the complete current driver/world/map state for replay. It shares
immutable observations and geometry. The world detaches only when one branch advances while another
still needs its old observation; it copies tracking history, not prediction graphs. Maps clone only
at explicit checkpoints. Related clones must be called serially because navigation graphs share
query scratch. The normal single-session loop does not clone a world or map.
Snapshot-file resume deliberately saves only menu acknowledgement and input-phase state. As with the previous native driver, gameplay predictions, retained collision verdicts and native mission state are rebuilt from the restored game capture and RAM. These files are same-ABI local checkpoints; Python validates ABI and byte size before C validates counts/indices. They contain no pointers. In-process replay clones retain the richer state and do not use this reduced snapshot format.
Removed work
- No mandatory Python player, inventory, camera, map or mission assembly between C stages.
- No Python fire-state or shop-policy loop on the normal native path.
- No population/map export to construct another native request.
- No per-frame shop plan allocation, dictionary/set creation, sorting or transaction log.
- Reusable pending-spawn storage and two resident fire schedules; geometry changes only with map/footprint changes.
- No full prediction-graph copy for replay checkpoints, and no duplicate inspection when copying an already-inspected frame.
The transport still decodes captured JSON/hex and packs C input structures in Python. Replacing TCP serialization with direct in-memory capture is the remaining large integration opportunity. Its end-to-end benefit needs measurement separately from the processing timings below. No Hatari build integration or WASM profiling was performed in this change.
Validation
A standalone C-only executable drives all five levels using embedded assets and cloned sessions:
cmake --build out/build/autopilot-native
ctest --test-dir out/build/autopilot-native --output-on-failure
src/autopilot/tests/session_smoke.c is also a small, complete native caller example. The test is
included only in the standalone native CMake project, not the Hatari or WASM builds.
Python tests cover all-level execution while prohibiting Python preparation/inspection, the real live loop with mocked transport, death/absence/transitions/completion, duplicate input, invalid spans, checkpoint branching/lifetime, deferred world/map/history/route inspection, fire resume and shop acknowledgement resume. Existing native, gameplay, planner, replay and map tests remain Python tests. These tests and recorded comparisons are not a new live full-campaign acceptance run.
Recorded processing measurements
Two fresh-process, CPU-pinned repeats in reverse order compare c-session-old with c.
The sources stayed fixed during each comparison. Driving excludes object inspection/checkpoints,
emulator execution, sockets and rendering. All compared verdicts matched.
| Sample | Frames | Previous component path | Complete C session | Reduction |
|---|---|---|---|---|
| Level 1 driving | 2367–3367 | 0.523760 ms | 0.366856 ms | 30.0% |
| Level 3 driving | 49751–50020 | 0.729458 ms | 0.540633 ms | 25.9% |
| Level 5 barrier driving | 90160–90459 | 0.650670 ms | 0.550384 ms | 15.4% |
| Level 1 inspected replay | 2367–3367 | 1.031876 ms | 0.657890 ms | 36.2% |
The final suite comprises 989 distinct Python tests (308 native, 575 gameplay, 67 planner,
24 replay and 15 map tests), plus the standalone C smoke test. Gameplay was also checked with
native world preparation forced. Eight generated native asset outputs pass --check.
The Level 5 sample measures the recorded barrier encounter; it is not evidence that the known
Level 5 campaign tactics limitations are resolved.
Level 1 preventive wall attacks (2026-09-13, ABI 86)
Level 1 now chooses a nearby visible wall shooter before corridor, formation, pickup and travel missions, while shells and the boss core retain priority. It uses the actual installed forward mounts and persistent navigation graphs to find a firing station at least 48 pixels below the target's collision bottom (measured from the player's collision top), and no higher than screen y=112. Already-passed guns and lateral detours beyond 112 pixels are excluded. Dynamic candidate safety remains active; this tactic adds no projectile forecasting.
Only one selected target triggers routing; a valid route is reused until its normal refresh, terrain change, or mounted weapon change. An optional attack has 96 frames to succeed, followed by a 128-frame cooldown after timeout or failed routing. This prevents a reachable-looking emplacement from pinning progression. The new Level 1 checkpoint fields require native ABI 86, mirrored by ctypes.
Validation: the native library compiled; 65 mission/session/replay tests and the
standalone C session smoke test passed. Replaying the 450 captured frames in
native-collision-avi-20260913/campaign.x2events selects wall attack at frame
14405 (target #336), before the recorded hit at 14495.
A fresh 650-frame native Hatari run from the same snapshot completed with shield
39, three lives, no damage, and no stall incidents. Recording and both AVIs:
xenon_tools/run_logs/native-wall-attack-20260913/campaign.x2events.
Canonical counters restarted at 1 in the new emulator. Right-wall shooters at
(240,4288) and (256,3968) entered the destruction routine at frames 353 and 640.
The left gun at (64,4320), which caused the previous hit, was passed without
damage; it was not killed before passing. This is a local validation, not a full
campaign regression run. The default native DLL and Hatari build were updated
after the replay UI released its DLL lock.
First-shop camera discontinuity (2026-09-13)
The repeated first-shop shutdown came from scene preparation, not shop purchase logic. At original frame 3854 (fresh continuation frame 2046), the camera moves from world scroll 2576 to 2495, while the applied scrolling step remains 1. The shop-active flag appears on the following frame. The session incorrectly used the 81-pixel absolute reposition as prediction velocity, exceeding the scene's supported motion range and releasing native control one frame later.
Prediction now uses the captured applied scroll with the proper world-coordinate sign. Older inputs without that telemetry retain their measured-delta fallback. Large camera repositions also discard retained mission/maneuver state. No range limit was relaxed, and no failed decision is silently ignored. Temporary failure tracing was removed after locating the scene creation guard.
Both old recordings now replay through their final shop frames without a native session rejection. A targeted regression covers the 2577 -> 2576 -> 2495 sequence and the following shop frame. Session/shop/mission/replay tests (69) and the standalone C smoke test pass.
Live verification: native-first-shop-verified-20260913/campaign.x2events, with
paired original/Sprite Stream AVIs, contains 2600 frames (652-3251). Native control
remains active in every capture. Shop entry is frame 2047; exit is frame 2926;
play continues through scroll 2170 at frame 3251. Shield remains 39, lives remain
three, and the incident report is empty. The original failing continuation is
native-first-shop-20260913/campaign.x2events: rejection at 2046, native control
released at 2047. An intermediate native-first-shop-fixed-20260913 attempt was
stopped before the shop because a locked executable prevented the new build;
only the verified recording validates the fix.
Right side of the final Level 1 passage (2026-09-13)
The old part2 recording loses 20 shield at frames 6809/6822/6823: swarm bodies
5565/#5566 and projectile #5696 hit the ship at x=149. Formation holding preserves
that exposed x position; the subsequent upward escape follows the swarm and moves the ship from screen y=136 toward y=76. Recomputed approach decisions also show a conflicting shell alignment toward x=92 before the swarm arrives.
A short Level 1 corridor tactic now precedes target detours between player world y=920 and y=624, for ships already on this side (x<224). Its entry/width-change waypoints are (188,824), (188,716), and (172,624). The persistent map's reserved anchor spans in the passage are 148..188 and then 148..172: these stations follow the right edge of the current passage, not the separate branch around x=260. Every route still uses the current persistent navigation graph and dynamic collision evaluation. Route reuse accepts the connector's nearby projected anchor and avoids a fresh search merely because it differs slightly from the authored point. Normal camera scrolling provides forward progress while the ship maintains the central screen band; shell alignment and generic formation holding cannot pull it left during this section. Normal tactics resume afterward. No additional swarm prediction or host bridge was added.
Three targeted mission tests cover priority over a nearby shell/swarm, route reuse and narrowing, exclusion of the separate right passage, and entry routing against the real persistent map. All 72 related tests and the C smoke test pass.
Identical-snapshot live comparison (both with original/Sprite Stream AVI):
native-final-corridor-baseline-20260913/campaign.x2events: 2100 records, canonical frames 3253-5352, final shield 39/lives 3, final scroll 139, score 34110.native-final-corridor-right-side-20260913/campaign.x2events: same frame range, final shield 39/lives 3, final scroll 143, score 33610.- In player world y=628..832, the baseline spent 207 captured frames with x=135..191; the new tactic spent 170 with x=168..186 and stayed in the current passage.
- Both captures contain an
other_damage_callerhit event at frame 4319 without shield loss, corresponding to the earlier part2 event at 6221. Neither fresh run reproduced the old shield-loss cluster, so these results verify positioning and local regression safety, not a demonstrated damage reduction over the current baseline. The new run's slightly lower score remains a comparison detail; full-campaign impact is not established.
Recorded pickup audit (2026-09-13)
python xenon_tools/analyze_run.py RECORDING.x2events --pickups --pickup-report REPORT.json
audits Level 1 and Level 2 object lifetimes directly, without replaying controller
choices. The report identifies each pickup, its first/last frame, closest captured
collision-box gap, and retirement frame. $4EA0 retirement at screen Y >= 200 is
an observed miss; retirement beside the ship is likely collected, not an
explicit collection event. Frame gaps, run/level changes and recording ends leave
incomplete lifetimes unresolved. Rear Shot expiry is an allowed skip only if
Side Shot was installed throughout the observed opportunity.
The original native-campaign-20260913-part2/campaign.x2events contains 18 observed
Level 1 misses (17 cash and CANNON, closest at frame 6959) and 12 Level 2 misses
(11 cash and HEALTH POWER 1, closest at frame 10401). The latest prior Level 1
comparison native-final-corridor-right-side-20260913/campaign.x2events contains
16 observed misses, including CANNON. A close pass is not counted as collection
when the same identity later expires. These figures include cash, not just weapon
and health pickups; incomplete lifetimes are reported separately.
Pickup collection corrections
Collection scoring no longer uses the enlarged hazard envelope. It requires
contact with a conservative central ship box at the pre-movement position.
ReVa confirms that $6734 rebuilds the core collision rectangle before calling
movement at $6882; $4F76 calls $5D86, which reads that stored rectangle.
Recorded raw core bounds visibly lag the final moving ship anchor. Testing only
the post-movement envelope can therefore count a near pass as collection.
Hazard and incompatible-pickup avoidance retain their larger safety geometry.
Level 1 now uses the existing compass/drop kernel to find a reachable crossing in the central reaction band over 56 frames. It routes to that horizontal station rather than chasing an orbiting pickup toward the top or far left. The final left corridor retains its right-side traversal priority, with setup now beginning at world Y=1100 through (188,1008) before the existing narrowing waypoints. This prevents a preceding left-wall firing station from holding the ship beside the swarm until the old Y=920 trigger. Level 2 uses bounded direct detours for nearby items; applying the Level 1 waiting tactic there caused extra projectile damage around its health pickup and was rejected. In both levels, nearby equipment/health takes priority over cash, and only Rear Shot is excluded when it would displace installed Side Shot. Other levels retain their previous attachment policy.
The low-level four-frame detour proposals use pickup rectangles already prepared for evaluation and choose the closest two forecast positions, instead of the first two objects' old positions. No new hazard prediction or Python/C bridge is needed. The Level 1 mission makes a bounded pickup-only forecast before scene preparation; it does not duplicate the much larger enemy scene.
The audit exposes separate totals by level and by cash versus equipment/health. Observed expiry remains distinct from inferred collection and incomplete capture.
Valuable upgrades and Zapper (ABI 87)
Candidate pickup records now retain their numeric game code. Safe routes score Cannon at 100, Zapper at 80, other equipment/health at 20, and cash at 1; collision and clearance requirements still precede pickup value. When a route collects an upgrade but predicts a later collision, search tries up to nine collect-then-evade repairs before accepting an empty safe station. It reuses the existing trajectory prefix and repair machinery; an unsuccessful attempt retains the safe fallback.
Zapper is deferred while no live damageable enemy or hostile projectile is visible.
Mission routing and candidate exclusion share that rule, so an empty-screen Zapper
is not accidentally consumed by a cash detour. The scan is lazy and runs only when
Zapper is present. Once the route reaches Zapper, evaluation ends after checking
that frame's safety and the next observation supplies the actual changed scene.
It does not forecast nonexistent post-Zapper enemies or assume newly spawned enemies
were also destroyed. ReVa traced pickup table entry 18 at $5088 to $5190: the
handler destroys eligible objects and directional projectiles, with object-type
exceptions. Reobserving avoids duplicating that destruction policy in prediction.
Targeted tests cover empty-screen deferral, visible-enemy activation, collection before scene invalidation, wall rejection on the collection frame, and upgrade repair despite an available empty safe route. The retained Python reference search still agrees on its 300 bounded scenarios with unspecified pickup values.
Live validation from the saved Level 1 post-shop checkpoint:
xenon_tools/run_logs/native-high-value-pickups-20260913/campaign.x2events, frames 3253–5552: Zapper collected at 3376, destroying five visible enemies and four hostile projectiles. Cannon collected at 5085; attachment slot 1 subsequently contains code$34, shop item 15, confirming acquisition beyond the audit's retirement heuristic. No lives were lost. A shell projectile caused 4 shield damage at 4848 (39→35), before the Cannon section. Recomputed diagnostics predict that contact in advance but fail to find an escape while crossing the constrained corridor; this is a remaining safety issue.- The same interval still misses 19 cash items and HEALTH POWER 1 at 4340 while shields are full. Eight cash items and both valuable pickups were collected. This validates the Cannon/Zapper correction, not an all-pickups guarantee.
xenon_tools/run_logs/native-high-value-level2-20260913/campaign.x2events, frames 7284–8783: health collected at 8285, shields 23→39, no observed shield decrease or life loss. The capture also emits another_damage_callerevent on that healing frame; the raw incident report retains it. Four cash items were collected and fourteen expired. Later Level 2 progression is not validated by this bounded run.
Both directories contain original/sprite AVI, pickups.json, and incident reports.
The final focused suite passes 94 tests plus xap_session_smoke. DLL and Hatari
builds both succeed. Remaining work is collecting more cash without disrupting
safe corridor staging and avoiding the shell shot at the recorded frame above.
Level 1 central-shell clearance
The final middle passage now gives its two shells, at world X=128 and X=208, priority over corridor travel and optional pickups. The rule is confined to the Level 1 approach (player world Y=624..1008, middle-lane entry) and shell pocket (world Y=500..820). It selects the retained target first, uses the existing mounted forward-fire station calculation, and reuses its route. It does not target the four outer-lane shells as part of this encounter. If no firing route is currently available, it holds instead of treating route failure as permission to advance.
The central pair is eligible while waiting up to 64 pixels above the viewport. Waiting until a shell becomes visible delayed leftward alignment until the ship was already crowded by the next wave. The first trial stayed around X=162 while the left shell's captured collision interval was X=128..139. Its retirement at 4852 still had health 2: disappearance was not proof of destruction. Validation must observe health reaching zero for these damageable shells.
After clearance, a centered exit covers world Y=480..624, below pickup priority. This prevents immediately choosing an outer shell after leaving the narrow lane. A nearby Level 1 Cannon also permits a short direct interception of its four-frame forecast instead of always waiting for its eventual central-band drop. This is limited to targets at screen Y=72..168, within 48 horizontal and 56 vertical pixels; normal candidate collision/clearance checks remain in effect. Other pickups retain their previous crossing-station policy.
For that nearby Cannon interception in the central exit, horizon_limit=24
selects a short collection plan and restores the configured horizon on the next
mission. This does not disable collision checks or change clearance margins; it
stops judging the pickup against a distant firing scenario using the old weapon
loadout. The driver applies the mission limit before preparing anticipated shots
and candidate geometry, so the shorter plan also avoids unused preparation.
XapMissionResult.horizon_limit is mirrored in ctypes and requires ABI 88;
the existing replay horizon statistic already exposes the effective value.
ReVa reconfirmed $4F8DC: a shell fires once at a random time in its vertical
trigger band, clears its movement step, and leaves the damageable list. Checking
possible release times is deliberate conservative prediction, not evidence of
repeated real shots.
Validated recording:
xenon_tools/run_logs/native-central-shells-short-intercept-20260913/campaign.x2events
(canonical frames 4654–5453, 800 frames). The right shell at X=208 reaches
health zero at 4750; the left shell at X=128 reaches health zero at 4802,
with the ship correctly aligned at X=129. Cannon is collected and present in the
attachment slots at 5034. Shields remain 39 and lives remain 3 throughout;
the incident report is empty. Original/sprite AVI, pickups.json, and
encounter-audit.json are beside the recording. This is a bounded encounter test,
not a complete campaign validation. The saved approach checkpoint is
native-central-shells-approach-20260913/end.sav.
Validation: 99 focused Python tests, including sequential central-shell targeting,
early leftward alignment, unavailable firing-route hold, centered exit, and nearby
Cannon interception/restoration of the normal horizon; native xap_session_smoke
also passes. DLL and Hatari builds succeed. No Python gameplay bridge was added.