From 5aa16d0ad861a9ea88d65c9facb94f278856109f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 04:31:43 +0000 Subject: [PATCH] Co-op: let Launch Octopus's currents lift the partner too The current generator (enemy $28) sits about 180 px from the current and asks $82:D7D7 for each of its four boxes whether the projected body is inside; it only flags that body (.3C, action $08) and the player's own movement lifts it. The partner replay skipped it (128 px range) and kept only motion fields. Widen the replay to 256 px and keep every byte an object's update writes to the partner's body, contact damage included: the real update's contact retry then finds the partner invulnerable. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Tu7jQG5NbXoKrTYTT7y7n3 --- docs/coop-interaction-ownership.md | 2 +- src/mmx_coop.c | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/coop-interaction-ownership.md b/docs/coop-interaction-ownership.md index 4b1375b..3e49ee6 100644 --- a/docs/coop-interaction-ownership.md +++ b/docs/coop-interaction-ownership.md @@ -58,7 +58,7 @@ Each player gets the native result. Behaviour is the same for X and Zero. | Armored Armadillo minecart (enemy `$2B`) | `lift_contact_hook`, `cart_hook`, `$82:D7D7`, `$88:9821..9867` | Query each player, move the cart once, then apply its native carry code separately to both riders. `.2C/.38` retain per-seat rider bits in WRAM; bit 0 remains the native "any rider" test. | Old single-rider snapshots are imported on first contact. | | Pickups (health, energy, Sub Tanks, Heart Tanks, 1-ups) | `pickup_hook`, `$84:9C0E..9D06`, `$00:D2E6..D31B` | Contact retried for the partner. The first seat to touch an item owns it (`pickup_owner`) and receives its effect. While a collector is parked for a refill (`$1F19`, the Heart Tank pause, or action `$18` alone, as a Sub Tank does), the other player's update is parked too (`refill_paused`, `partner_parked`). | **Review:** Heart Tanks and Sub Tanks are shared progression. Decide whether Zero may collect them, or only X. | | Native player shots that test `$0BA8` (charged Shotgun Ice's sled) | `object_hook` (`shot_ghost_*`), `$00:D3DD..D3F9` | Each seat's projectile loop first replays once with the other seat's body projected over the same shots, when a native shot is within 64 px of that body. Only that body's position, velocity and ground flags (`.04..09`, `.1A..1D`, `.2B`) are kept; WRAM, weapon combat, Zero state and renderer pieces are restored, so every shot still advances once. A `shot-rider` trace row marks frames where the replay moved the other body. | Untested in play. If the sled's rider test is in the player's terrain code rather than the shot, this does nothing. | -| Enemies and enemy projectiles that move `$0BA8` directly (Launch Octopus's upward currents) | `object_ghost_hook`, `$00:D4F6 -> D4F9` (enemies), `$00:D499 -> D49C` (enemy projectiles) | Couch co-op: when an object is within 128 px of the partner, its update first replays once with the partner's body projected. The partner keeps that run's position, velocity, ground flags and action (`.02..09`, `.1A..1D`, `.2B`); everything else is restored and the real update runs for the world actor. Skipped: bosses, lifts and minecarts, Dr. Light's capsule, Gulpfer (`$1D`, nearest-player instead) and Slimer's puddle (`$19`). `body-moved` trace rows name any object whose update moved the projected body; `ghost-moved` rows mark replays that moved the partner. | Untested in play. Review any enemy that grabs or scripts the player. | +| Enemies and enemy projectiles that act on `$0BA8` (Launch Octopus's upward-current generator, `$28`) | `object_ghost_hook`, `$00:D4F6 -> D4F9` (enemies), `$00:D499 -> D49C` (enemy projectiles) | Couch co-op: when an object is within 256 px of the partner, its update first replays once with the partner's body projected. The partner keeps every byte that run wrote to its body (the current generator `$28` only flags a body found in its four `$82:D7D7` boxes; the player's own movement then lifts it). Everything else is restored and the real update runs for the world actor. Contact damage taken in the replay is kept, and the real update's contact retry then finds the partner invulnerable. Skipped: bosses, lifts and minecarts, Dr. Light's capsule, Gulpfer (`$1D`, nearest-player instead) and Slimer's puddle (`$19`). `body-moved` trace rows name any object whose update moved the projected body; `ghost-moved` rows mark replays that moved the partner. | Untested in play. Review any enemy that grabs or scripts the player. | | Player-terrain and object helpers | `object_hook`, `$00:D2BD..D47F`, `$81:9D67` | The anchor's helper call is replayed for the partner, then the anchor's registers are restored. | — | | Dash effects | `dash_effect_hook` (`$81:9C86`, `$80:F478/F47C`) | Each seat's dust and effects belong to that seat. | — | | Enemy AI target | `view_actor_hook` (`$00:D4F6/D515/D499/D4B8`, returns `D4F9/D522/D49C/D4C5`) | Online co-op: each enemy's update runs with the nearest living player projected, so it aims at and reacts to that player. Couch co-op: the native current body, except Launch Octopus's Gulpfer (`$1D`, `couch_nearest_target`), which chases and swallows the body it targets and also uses the nearest player. | Includes story actors; check capsule/Light objects if online behaviour differs. | diff --git a/src/mmx_coop.c b/src/mmx_coop.c index 156047a..bc6cb34 100644 --- a/src/mmx_coop.c +++ b/src/mmx_coop.c @@ -1266,11 +1266,16 @@ static uint8_t shot_ghost_lift[sizeof(lift)],shot_ghost_cart[sizeof(cart)]; static uint8_t shot_ghost_ram[0x20000]; static void shot_ghost_reset(void) { shot_ghost.pass=0; } static bool ghost_active(void) { return shot_ghost.pass==1; } -/* Position (with subpixels), velocity and the ground/contact flags; object - * replays also keep the action an object forced (a current's lift). */ +/* Shot replays keep position (with subpixels), velocity and the ground and + * contact flags: a seat's shot pass also maintains that seat's own shot + * count and firing state, which the other body must not inherit. + * Object replays keep every byte the update wrote to the body. Launch + * Octopus's current generator ($28) only flags a body it finds in one of its + * four $82:D7D7 boxes (.3C, action $08); the player's own movement lifts it. + * Contact damage taken during the replay is kept too, and the real update's + * contact retry then finds the partner already hit and invulnerable. */ static bool shot_ghost_field(unsigned i) { - return (i>=4 && i<=9) || (i>=0x1a && i<=0x1d) || i==0x2b || - (shot_ghost.kind==GHOST_OBJECT && (i==2 || i==3)); + return shot_ghost.kind==GHOST_OBJECT || (i>=4 && i<=9) || (i>=0x1a && i<=0x1d) || i==0x2b; } static bool ghost_partner_ready(void) { const MmxCoopPlayer *o=&state.players[state.current^1]; @@ -1303,7 +1308,8 @@ static bool object_ghost_wanted(unsigned d) { /* Bosses script the player (intros, victory pose); they stay single-seat. */ if(enemy && MmxWidePolicy_IsBossEncounter((uint8_t)c)) return false; if(projectile && c==0x19) return false; - return ghost_near(d,128); + /* The current generator's boxes reach about 200 px from its origin. */ + return ghost_near(d,256); } static void shot_ghost_begin(CpuState *cpu,unsigned kind,uint32_t resume) { cpu_mirrors_to_p(cpu);