diff --git a/src/mmx_coop.c b/src/mmx_coop.c index 737dda2b..16115855 100644 --- a/src/mmx_coop.c +++ b/src/mmx_coop.c @@ -1029,6 +1029,7 @@ static void door_hook(CpuState *cpu,uint32_t pc) { if(!state.door_pass || cpu->S!=state.door_s || cpu->D!=state.door_d) return; TRACE(DOOR,pc,state.door_pass,0,cpu); if(at==0xe725 || at==0xecc7) { + MmxCoopViewsAdoptPlayer(g_ram,&state,state.current); state.anchor=state.current;state.door_pass=0;begin_scene(g_ram);return; } if(state.door_pass==1) { diff --git a/src/mmx_coop_view.c b/src/mmx_coop_view.c index 99cb7f3f..93496563 100644 --- a/src/mmx_coop_view.c +++ b/src/mmx_coop_view.c @@ -40,6 +40,7 @@ void MmxCoopViewsMark(unsigned flag,bool seen) { void MmxCoopViewsSetOnline(bool active) { online=active; } bool MmxCoopViewsOnline(void) { return online; } static int word(const uint8_t *r) { return r[0]|r[1]<<8; } +static void putword(uint8_t *r,unsigned value) {r[0]=(uint8_t)value;r[1]=(uint8_t)(value>>8);} static int clamp(int v,int lo,int hi) { if(hihi?hi:v; } static bool living(const MmxCoopPlayer *p) { return p->status==MMX_COOP_ALIVE && (p->body[0x27]&127) && p->body[2]!=12; @@ -131,6 +132,26 @@ MmxCoopView MmxCoopViewWithBounds(const uint8_t *r,const MmxCoopState *s,unsigne MmxCoopView MmxCoopViewForPlayer(const uint8_t *r,const MmxCoopState *s,unsigned seat) { return MmxCoopViewWithBounds(r,s,seat,&bounds); } +void MmxCoopViewsAdoptPlayer(uint8_t *r,const MmxCoopState *s,unsigned seat) { + if(!online || !s->initialized || seat>1 || seat==s->anchor || s->scene_owner || + s->stage_pending || r[0xd3]!=4 || bounds.stage!=r[0x1f7a] || + !(bounds.valid&(1u<players[seat]) || + s->players[seat].zero.swap_phase) return; + MmxCoopView v=MmxCoopViewForPlayer(r,s,seat); + /* Sigma 1's upper door ($81:EC98) can be reached while the world actor + * is still at the shaft bottom. Once the door freezes gameplay, native + * scrolling must start from its entrant's view and room limits; keeping + * the lower actor's camera leaves the door waiting forever. Copy both + * current and target limits, plus the camera's previous-position pair, + * before the caller changes the anchor and begins scene transport. */ + static const unsigned targets[4]={0x1e5e,0x1e60,0x1e68,0x1e6e}; + for(unsigned i=0;i<4;++i) { + putword(r+0x1e56+i*2,bounds.limits[seat][i]); + putword(r+targets[i],bounds.limits[seat][i]); + } + putword(r+0x1e4d,(unsigned)v.x);putword(r+0x1e6a,(unsigned)v.x); + putword(r+0x1e50,(unsigned)v.y);putword(r+0x1e6c,(unsigned)v.y); +} bool MmxCoopViewContains(const uint8_t *r,const MmxCoopState *s,int x,int y, int left,int right,int top,int bottom,int margin) { for(unsigned seat=0;seat<2;++seat) { diff --git a/src/mmx_coop_view.h b/src/mmx_coop_view.h index bd115da0..ec73e787 100644 --- a/src/mmx_coop_view.h +++ b/src/mmx_coop_view.h @@ -20,6 +20,9 @@ MmxCoopViewBoundsState MmxCoopViewsGetBoundsState(void); bool MmxCoopViewsValidBoundsState(const MmxCoopViewBoundsState *bounds); void MmxCoopViewsSetBoundsState(const MmxCoopViewBoundsState *bounds); void MmxCoopViewsUpdateBounds(const uint8_t *ram, const MmxCoopState *state); +/* Before a remote player takes a native door, transfer its authored view to + * the shared script camera while both independent views still exist. */ +void MmxCoopViewsAdoptPlayer(uint8_t *ram, const MmxCoopState *state, unsigned seat); void MmxCoopViewsPlayerLimits(const uint8_t *ram, const MmxCoopState *state, unsigned seat, int limits[4]); MmxCoopView MmxCoopViewWithBounds(const uint8_t *ram, const MmxCoopState *state, diff --git a/tests/mmx_adaptive_state_test.c b/tests/mmx_adaptive_state_test.c index 8535f4de..26b553e0 100644 --- a/tests/mmx_adaptive_state_test.c +++ b/tests/mmx_adaptive_state_test.c @@ -17,7 +17,7 @@ static void frame(unsigned input) { getenv("MMX_COOP_ONLINE_FIXTURE") || getenv("MMX_COOP_LIFT_REPLAY_FIXTURE") || getenv("MMX_COOP_REGION_REPLAY_FIXTURE") || getenv("MMX_COOP_WRAP_REPLAY_FIXTURE") || getenv("MMX_COOP_SIGMA1_INTRO_FIXTURE") || getenv("MMX_COOP_SIGMA1_LOCKED_FIXTURE") || - getenv("MMX_COOP_SIGMA1_VILE_FIXTURE")) MmxCoopViewsSetOnline(!getenv("MMX_COOP_SIGMA1_UNIFIED")); + getenv("MMX_COOP_SIGMA1_VILE_FIXTURE") || getenv("MMX_COOP_SIGMA1_LADDER_FIXTURE")) MmxCoopViewsSetOnline(!getenv("MMX_COOP_SIGMA1_UNIFIED")); if(!MmxReplayPlaying() && getenv("MMX_REPLAY_TEST") && !strcmp(getenv("MMX_REPLAY_TEST"),"online")) { MmxCoopViewsSetIndependent(true);MmxCoopViewsSetOnline(true);MmxRendererSetPeerView(1); } @@ -1968,6 +1968,9 @@ int main(int argc, char **argv) { } const char *zero_assets = getenv("MMX_ZERO_TEST_ASSETS"); const char *zero_title = getenv("MMX_ZERO_TITLE_FIXTURE"); + if(getenv("MMX_COOP_SIGMA1_LADDER_FIXTURE")) { + sigma1_ladder_checks(zero_assets,getenv("MMX_COOP_SIGMA1_LADDER_FIXTURE"),start,expected,actual,cap);return 0; + } if(getenv("MMX_COOP_LIFT_REPLAY_FIXTURE")) { coop_eagle_replay_checks(zero_assets,getenv("MMX_COOP_LIFT_REPLAY_FIXTURE"),start,expected,actual,cap);return 0; } diff --git a/tests/mmx_coop_sigma1_replay_test.inc b/tests/mmx_coop_sigma1_replay_test.inc index c27a3b63..5246326a 100644 --- a/tests/mmx_coop_sigma1_replay_test.inc +++ b/tests/mmx_coop_sigma1_replay_test.inc @@ -20,6 +20,70 @@ static const uint8_t *sigma1_body(const MmxCoopState *s,unsigned seat) { return seat==s->current ? g_ram+0xba8 : s->players[seat].body; } static bool sigma1_forced(const uint8_t *b) { return b[2]==0x1e || b[0x6e]; } +/* Session 10: P2 climbs to the upper door while X remains below. The door + * must take the entrant's camera and authored bounds before it freezes + * gameplay; replay inputs alone then complete its native scroll and release. + * MMX_PERF_MOD_ROOT/X1_ROM/X3_ROM activate the full recorded Saber behavior. */ +static void saber_perf_activate(void); +static void sigma1_ladder_checks(const char *assets,const char *fixture, + uint8_t *start,uint8_t *expected,uint8_t *actual,size_t cap) { + coop_replay_start(assets);RtlEnableExtendedFrameTiming(); + saber_perf_activate(); + g_config.no_sprite_limits=true;MmxCoopViewsSetIndependent(true); + static unsigned p1[1200],p2[1200]; + unsigned count=sigma1_inputs(getenv("MMX_COOP_SIGMA1_INPUTS"),p1,p2,1200); + check(count==600,"ladder trace covers the recorded upper-door approach"); + for(unsigned driver=0;driver<2;++driver) { + check(RtlLoadSnapshot(fixture),"captured Sigma 1 ladder checkpoint loads"); + if(driver) { + sigma1_swap_seats(); + MmxCoopViewBoundsState bounds=MmxCoopViewsGetBoundsState(); + for(unsigned i=0;i<4;++i) { + uint16_t v=bounds.limits[0][i];bounds.limits[0][i]=bounds.limits[1][i];bounds.limits[1][i]=v; + } + bounds.valid=(uint8_t)((bounds.valid>>1)|((bounds.valid&1)<<1)); + MmxCoopViewsSetBoundsState(&bounds); + } + MmxCoopState s=MmxCoopGetState();unsigned entrant=driver^1; + check(g_ram[0x1f7a]==9 && !s.scene_owner && s.anchor==driver && + coop_y(&s.players[entrant])<700 && coop_y(&s.players[driver])>1200, + "entrant starts above the world actor in the native ladder shaft"); + bool entered=false,scrolled=false,released=false; + unsigned tick; + for(tick=0;tick<1400;++tick) { + unsigned a=tick=4096 && coop_y(&s.players[seat])<480, + "both living seats return inside the upper room with native controls released"); + } + unsigned x[2]={coop_x(&s.players[0]),coop_x(&s.players[1])}; + coop_frames(4,SNES_PAD_RIGHT,SNES_PAD_RIGHT);s=MmxCoopGetState(); + check(coop_x(&s.players[0])>x[0]+2 && coop_x(&s.players[1])>x[1]+2, + "both returned players walk under their own input after the door"); + size_t n=RtlSaveSnapshotToMemory(start,cap);check(n,"ladder handoff snapshot saves"); + coop_frames(24,SNES_PAD_RIGHT,SNES_PAD_LEFT);size_t en=RtlSaveSnapshotToMemory(expected,cap); + check(RtlLoadSnapshotFromMemory(start,n),"ladder handoff snapshot restores"); + coop_frames(24,SNES_PAD_RIGHT,SNES_PAD_LEFT); + same(expected,en,actual,RtlSaveSnapshotToMemory(actual,cap),"ladder handoff replays byte-exactly after rollback"); + } + puts("MMX ISSUE110 SIGMA1 LADDER CHECKS PASSED"); +} static void sigma1_moves(unsigned seat,const char *what) { MmxCoopState s=MmxCoopGetState();unsigned x=coop_x(&s.players[seat]); coop_frames(30,seat?0:SNES_PAD_RIGHT,seat?SNES_PAD_RIGHT:0);s=MmxCoopGetState();