Need help getting started? Try the player guide. These notes include older work; the game overview explains what’s ready now.

Native projectile runtime

Boss self-projectile correction (2026-10-21)

The PC game enables combat.boss_projectile_immunity for both D7/D8 bosses. Their own projectiles exclude the firing body from movement contacts for the entire flight, including return paths. The combat runtime rejects damage to that same owner from its projectile, covering direct hits and radial blasts without marking the boss globally invulnerable. Player attacks, other enemies' shots, damage to other bosses, and player self-splash remain unchanged.

Boss splitting volleys originally gave children a pointer to the parent shot. The native adapter preserves the original boss in each child's source after construction, leaving spawn coordinates, inherited velocity, animation, timing and damage unchanged. This identity survives further splits and parent-slot release/reuse. No walk through an expired parent pointer is required. As with the existing runtime, entity storage uses stable pool addresses; there is no new generation-based entity handle system.

The option stays off in zero-initialized recovery runtimes. Original combat, projectile motion, constructor and controller routines remain unchanged. tests/test_boss_projectiles.py checks both actual boss initializers, native projectile construction/movement, direct-hit refusal, splitting/slot reuse, real expiry blasts near players and another boss, and player weapon damage on isolated map geometry. Gunrilla's existing return-impact regression runs with the boss policy enabled to check that D6 keeps its existing behavior. The focused boss test passes415 checks under MSVC and GCC ASan/UBSan/leak detection. Gunrilla passes446 checks including all8 return impacts; all8 focused CTests pass. The rebuilt Windows executable passes a hidden, muted 60-frame launch check. Reports: boss-projectile-validation.json and windows-boss-projectile-validation.json. No campaign playthrough was run.

Type67 cannonballs use their original no-gravity flight again, at the user's request. The temporary floor-rolling override has been removed; launch detachment and Gunrilla's self-collision fix remain. The PC adapter still enables ceiling_impacts: rockets65 explode and Prion69 stops at ceiling contact. Standalone recovery leaves that option off. See flight behavior and current checks.

Gunrilla launch collision correction

The native PC runtime now lets type67 cannonballs leave their typeD6 Gunrilla shooter. The original80-unit muzzle offset lies inside the combined90-unit shooter/16-unit projectile square collision bounds; the first16-unit move can hit the shooter and immediately detonate. Original32-unit entity broadphase padding can hide that overlap in some cell/heading combinations, making the failure position dependent.

The motion adapter calls ssa_world_try_move_ignoring only for unowned type67 shots whose registered typeD6 source currently overlaps their body. The query skips that one source, preserving all other entities, specials, walls and floor/ceiling commits. It retains no exclusion and changes no shooter flags or list membership. Once the current bounds separate, subsequent moves use the original query again; returning into the shooter can still collide. Player cannonballs and other enemy/projectile types retain their old behavior. The standalone recovered03D7C predicate and10A20 routine remain unchanged. This is the user's requested native gameplay correction, not a new byte-exact recovery.

decomp.ps1 test-gunrilla passes418 ASan/UBSan/leak assertions over39 projectile frames on actual map geometry. It constructs/activates the realD6 dimensions, reproduces the original shooter blocker in all8 headings, verifies escape without damage/particles, then proves8 return impacts and one real wall/player/ enemy impact each. Static clearance chooses a flat launch fixture; shooter relocation is test setup, not a change to spawning or AI. Creation and impacts run native shared-pool/combat code. The original world regression passes28,720 ROM comparisons plus its native pickup/jump/H cases, and the existing projectile integration remains37,132 assertions/3,270 frames on16 maps.

MSVC's ssa_native_gunrilla_check agrees on every focused counter. The game and all106 libraries rebuild; PC/reference180-frame hidden smoke launches pass. See gunrilla-validation.json, windows-gunrilla-validation.json and the refreshed world/projectile reports. Full campaign completions were not rerun for this fix.

Existing integration contracts

The playable composition enables ballistic_player_prion in addition to the existing rocket option. Player type69 detaches at commit, and its native weapon adapter removes the sideways muzzle displacement, restores the actual sight heading after original no-target aim probes, and projects its192-unit forward offset from live eye height along the aiming slope. It no longer follows later player yaw/position. Default recovered runtimes leave the option zero, and the original conversion/E2 logic remains unchanged. See weapon-fixes.md for launch alignment and trajectory tests.

The playable composition also enables ballistic_player_cannon: player type67 clears attachment4000 during weapon commit, preserving launch XYZ velocity and position instead of following later player view/translation. Original bounce, penetration, damage, source and owner exclusion stay unchanged. This option is default-off in the recovered runtime and does not apply to enemy cannonballs. Six player launch tests cover level/up/down aim in both views; Gunrilla's 418-check regression also passes with the option explicitly enabled.

projectile_runtime.c connects weapon creation, projectile controllers and motion to the native world, shared entity pool and combat adapter. It contains no ROM instruction execution. Supported controller types are 65, 66, 67, 69 and 6B; weapon creation currently routes the actual weapon 6, 7 and 10 requests (65, 69, 67). Attached conversion effects use the recovered E2 controller. Unsupported types/effects are errors at their integration boundary.

Initialize after combat_runtime, bind each existing weapon runtime with ssa_weapon_runtime_bind_projectiles, then run individual projectile slots in the original active pool order during the entity phase. The separate entity_runtime now binds the native 04E04 scheduler to players, CD enemies, projectile/E2 controllers and the owned-E1 timer wrapper. Other recognized handlers still fail explicitly; this is not a complete game loop or every enemy controller. A successful projectile step returns 0 while active or 1 when destroyed; negative returns report invalid integration or callback failure. The frame order remains entities before movers/VM before weapons; projectiles created in the weapon phase first receive their entity update on the next frame.

Creation uses 10830/109F0 and inserts through ssa_world_insert. The source and one-based player-owner fields are assigned after insertion, preserving ROM ordering. The weapon controller subsequently commits its travel speed, owner and weapon fields to the same stable allocation. In particular, near geometry can set travel to zero, making the next appropriate controller update explode without a motion call. Pool/world exhaustion returns a null allocation; the existing weapon-controller contract treats a missing mandatory projectile as an error. Failed insertion retains the allocated slot for explicit cleanup.

The adapter owns projectiles[100], effects[100] and active flags but uses script_spawn.actors and its existing 100-slot pool for bodies. It neither duplicates active entities nor infers original slot order from world lists. Actor storage stays stable. Allocation clears an unused payload, assigns its activation entity pointer, and retains ownership until explicit or controller release. Release unlinks, performs the original swap-last pool operation and then clears both actor and projection. It sends no script group notification.

activation.health, activation.timer, activation.state and facing are canonical between calls. The projectile projection retains speed, source, owner and weapon. Animation IDs/periods and completion flags are mirrored to the actor metadata; frames/counters reset on animation changes. This adapter does not provide entity animation playback. Projectile and script mirrors synchronize on callback failures as well as successful updates.

Motion composes actual ssa_world_try_move, vertical physics, the world's blocking entity and a dedicated wall-reflection workspace. The type-67 wall path unlinks, restores its frame-entry XYZ and velocity, reinserts and executes the original three-ray reflection. These ray queries do not replace movement contacts or the world's blocking entity. Detailed original behavior and isolated comparisons are recorded in projectile-motion.md, projectile-bounce.md, projectile-create.md and projectile-controller.md.

All updates run under the private combat scope, holding combat, all players and script-spawn locks through callbacks. The weapon helper reuses the weapon's already-held scope instead of attempting to lock twice. Damage source is the projectile body and source_owner is its firing source: original 04E30–04E40 stores each dispatched entity at 0300443C before invoking its controller. Credit and selected hit counters follow the motion/controller callbacks; type-67 direct penetration leaves their incoming values unchanged while explosions select the projectile owner and weapon. The runtime restores the caller's shared credit/hit-counter/credited-kill pointers after its step.

Bind hit_counts[player] to ten persistent weapon counters and credited_kills[player] to that shooter's persistent multiplayer byte. Keep the weapon and counter storage alive until the projectile runtime is freed. Damage uses canonical player inventory health and actor activation health. Sound/network requests use the existing combat presentation callback; particles use the required projectile callback. These observers must not mutate/reenter world, pool, actor, player or adapter state. Earlier damage and effects remain if a later presentation callback fails. Destruction is deferred until the controller returns, keeping its source body valid during every callback.

Conversion sets target body flag 400 and actor.parameter34=30 before attempting the E2 allocation, preserving actor.parameter36. Allocation exhaustion still leaves the target converted. The effect starts at target Z+height+16, with flag 20, animation 5B and period 5. Its subsequent 113E4 updates unlink, follow target XYZ at Z+height+48, then reinsert; target flag 20 requests its destruction instead. Flag 400 activates alternate enemy targeting through shared navigation; it does not freeze movement. parameter34 is the health-drain period, and parameter36 is its countdown. The CD runtime now consumes and synchronizes these canonical actor fields, including partial failures. Only CD has that complete native integration so far. No freeze/thaw gate should be added to the dispatcher. The impact callback name freeze is retained for compatibility; its documented behavior is conversion. See projectile-conversion.md for the recovered instructions, strict-negative drain cadence and native conversion-to-death integration.

Do not externally release/reuse bound projectile actors through script-spawn, or release/reuse a converted target while an E2 still follows it. The adapter validates target pointer membership, current ownership and type before following; the original host projection has no generation identity to detect a same-type slot reuse. A violated ownership contract yields an error where detectable. Free this adapter before weapon, combat, script-spawn and world teardown.

test-projectile-runtime runs the C integration fixture under ASan and UBSan with -no-pie and ASAN_OPTIONS=detect_leaks=0. Its finalized counts and source hashes are recorded in native-projectile-runtime-validation.json; the fixture uses real map geometry, native weapon creation, flight, damage and shared-pool ownership. Presentation remains observed callbacks and controlled enemy corpse animation is not claimed as playback. Independent original-ROM comparisons remain separate from native integration assertions and exact compiler matching.

The final PowerShell driver passes 37,132 assertions on all 16 maps, covering 3,270 projectile frames and 3,202 actual moves. There are ten verified wall reflections (unchanged position, an intercept within range and changed velocity), two penetration cases, a conversion effect following its target and releasing on death, a full 99-slot allocation test with reuse, and two callback/allocation failure scenarios. Direct cannon penetration checks both zero incoming credit and a separate selected hit counter changing from41 to42 without replacement.

Two multiplayer rocket kills credit distinct shooter counters and preserve the caller's borrowed counter. Four network requests are observed; these tests do not apply remote packets or claim a network transport. Another 2,048 player frames retain jumping (1,760 airborne) and H-view (1,536 third person). The older weapon suite still passes 100,466 assertions and combat integration passes 26,435 assertions. At that validation checkpoint, all 54 native CMake libraries built. The later entity scheduler and conversion integration have their own validation reports.