diff --git a/docs/coop-interaction-ownership.md b/docs/coop-interaction-ownership.md index 1536c06..1e26639 100644 --- a/docs/coop-interaction-ownership.md +++ b/docs/coop-interaction-ownership.md @@ -62,7 +62,7 @@ Each player gets the native result. Behaviour is the same for X and Zero. | Kuwanger laser sensors/turrets (enemies `$43/$44`) | `laser_contact_hook`, `$84:9C0E`; `laser_target_hook`, `$87:BA5C/BB09/BB0D` | Either seat may trip a sensor. Its `.3F` seat is copied to each armed turret, which projects that player only while calculating its delayed shot direction. | Target ownership survives snapshots; normal world projection is restored afterwards. | | 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 7d99582..ab9e17d 100644 --- a/src/mmx_coop.c +++ b/src/mmx_coop.c @@ -1400,11 +1400,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]; @@ -1441,7 +1446,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);