Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/mmx_coop.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
21 changes: 21 additions & 0 deletions src/mmx_coop_view.c
Original file line number Diff line number Diff line change
Expand Up @@ -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(hi<lo) hi=lo;return v<lo?lo:v>hi?hi:v; }
static bool living(const MmxCoopPlayer *p) {
return p->status==MMX_COOP_ALIVE && (p->body[0x27]&127) && p->body[2]!=12;
Expand Down Expand Up @@ -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<<seat)) || !living(&s->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) {
Expand Down
3 changes: 3 additions & 0 deletions src/mmx_coop_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion tests/mmx_adaptive_state_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down
64 changes: 64 additions & 0 deletions tests/mmx_coop_sigma1_replay_test.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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<count?p1[tick]:0,b=tick<count?p2[tick]:0;
coop_frames(1,driver?b:a,driver?a:b);s=MmxCoopGetState();
if(s.scene_owner && !entered) {
entered=true;
check(s.scene_owner==entrant+1 && eagle_word(g_ram+0x1e50)==256,
"upper door keeps its entrant's camera on the first scripted frame");
}
scrolled|=entered && eagle_word(g_ram+0x1e4d)==4096 && !g_ram[0x1f13];
if(entered && scrolled && !s.scene_owner) {released=true;break;}
}
fprintf(stderr,"sigma1 ladder driver=%u tick=%u entered=%u scrolled=%u returned=%u camera=%u,%u xy=%u,%u/%u,%u\n",
driver,tick,entered,scrolled,released,eagle_word(g_ram+0x1e4d),eagle_word(g_ram+0x1e50),
coop_x(&s.players[0]),coop_y(&s.players[0]),coop_x(&s.players[1]),coop_y(&s.players[1]));
check(entered && scrolled && released,"native upper-door scroll finishes and returns the distant partner");
for(unsigned seat=0;seat<2;++seat) {
const uint8_t *body=sigma1_body(&s,seat);
check(s.players[seat].status==MMX_COOP_ALIVE && (body[0x27]&127) &&
!body[0x6e] && !s.players[seat].zero.swap_phase &&
coop_x(&s.players[seat])>=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();
Expand Down
Loading