diff --git a/CMakeLists.txt b/CMakeLists.txt index 057f48b..fba5b9b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.20) -project(MegaManXSNESRecomp VERSION 2.1.4 LANGUAGES C CXX) +project(MegaManXSNESRecomp VERSION 2.1.5 LANGUAGES C CXX) set(CMAKE_C_STANDARD 11) find_package(Python3 COMPONENTS Interpreter REQUIRED) add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/mmx_weapon_sources.h" diff --git a/src/mmx_coop.c b/src/mmx_coop.c index cd01fad..28b2681 100644 --- a/src/mmx_coop.c +++ b/src/mmx_coop.c @@ -902,10 +902,10 @@ static bool lift_elevator(unsigned d) { * Storm Eagle's E-tank elevator top ($59) and its column ($5A, 83 px * below), Flame Mammoth's scrap blocks dropped onto the conveyor * ($2A, from $87:9C7B/9D89), Armored Armadillo's minecart ($2B), and - * Kuwanger's red moving platforms ($3F). */ + * Kuwanger's red moving platforms ($3F), and D-Rex's lower body ($62). */ if(d<0xe68 || d>=0x1228 || (d-0xe68)%64 || !g_ram[d]) return false; unsigned c=g_ram[d+10]; - return c==0x59 || c==0x5a || c==0x2a || c==0x2b || c==0x3f; + return c==0x59 || c==0x5a || c==0x2a || c==0x2b || c==0x3f || c==0x62; } static void laser_reset(void); static void lift_reset(void) { @@ -962,7 +962,7 @@ static void kuwanger_lift_hook(CpuState *cpu,uint32_t pc) { static void kuwanger_carry_hook(CpuState *cpu,uint32_t pc) { unsigned d=cpu->D; if(!enabled || !state.initialized || d<0xe68 || d>=0x1228 || (d-0xe68)%64 || - !g_ram[d] || (g_ram[d+10]!=0x3d && g_ram[d+10]!=0x3f)) return; + !g_ram[d] || (g_ram[d+10]!=0x3d && g_ram[d+10]!=0x3f && g_ram[d+10]!=0x62)) return; if((pc&65535)==0xc715) { if(elevator_move.pass) return; unsigned riders=g_ram[d+0x3f]&3; @@ -1071,7 +1071,7 @@ static void lift_rtl_hook(CpuState *cpu,uint32_t pc) { } bool second_rides=g_ram[d+0x2c]&1; uint8_t combined=(uint8_t)(lift.first_2c|g_ram[d+0x2c]); - if(g_ram[d+10]==0x3f) + if(g_ram[d+10]==0x3f || g_ram[d+10]==0x62) g_ram[d+0x3f]=(uint8_t)(((lift.first_2c&1)?1u<D; + if(shot_ghost.pass || state.menu_owner || state.scene_owner || state.stage_pending || + g_ram[0xd3]!=4 || !ghost_partner_ready() || d<0xe68 || d>=0x1228 || + (d-0xe68)%64 || !g_ram[d] || g_ram[d+10]!=0x52) return; + shot_ghost_begin(cpu,GHOST_EAGLE_WIND,pc&0xffffff); +} +/* D-Rex's extra body-contact flags follow its ordinary solid query. Keep + * those flags private to each body too, without replaying its boss AI. */ +static void drex_contact_hook(CpuState *cpu,uint32_t pc) { + if(!enabled || !state.initialized) return; + if((pc&65535)==0xc35f) { + if(shot_ghost.pass==1 && shot_ghost.kind==GHOST_DREX_CONTACT) shot_ghost_end(cpu,pc); + return; + } + if(shot_ghost.pass==2 && shot_ghost.kind==GHOST_DREX_CONTACT) {shot_ghost.pass=0;return;} + unsigned d=cpu->D; + if(shot_ghost.pass || state.menu_owner || state.scene_owner || state.stage_pending || + g_ram[0xd3]!=4 || !ghost_partner_ready() || d<0xe68 || d>=0x1228 || + (d-0xe68)%64 || !g_ram[d] || g_ram[d+10]!=0x62) return; + shot_ghost_begin(cpu,GHOST_DREX_CONTACT,pc&0xffffff); +} /* Launch Octopus's vortex is effect $16, not the enemy current generator. * Its native update ($81:F493) clears/sets BE4 and moves only BA8. Replay * the effect for the other living body, retaining its contact and motion, @@ -2058,6 +2090,10 @@ void MmxCoopRegisterHooks(void) { interp_bridge_add_pre_opcode_hook(ghosts[i],object_ghost_hook); interp_bridge_add_pre_opcode_hook(0x00d359,current_ghost_hook); interp_bridge_add_pre_opcode_hook(0x00d35c,current_ghost_hook); + interp_bridge_set_pre_opcode_hook(0x87dac4,eagle_wind_hook); + interp_bridge_set_pre_opcode_hook(0x87db0e,eagle_wind_hook); + interp_bridge_set_pre_opcode_hook(0x88c333,drex_contact_hook); + interp_bridge_set_pre_opcode_hook(0x88c35f,drex_contact_hook); const unsigned views[]={0x00dcd7,0x00dd2d,0x82807d,0x82809e,0x8280c3,0x838957}; for(unsigned i=0;ibody+5,x);eagle_putword(p->body+8,y); + eagle_putword(p->body+0x22,x);eagle_putword(p->body+0x24,y); + memset(p->body+0x1a,0,4);p->body[2]=p->body[3]=0; + p->status=MMX_COOP_ALIVE;p->body[0x27]=0x98;p->zero.swap_phase=0; + memset(p->shots,0,sizeof(p->shots)); + } + unsigned other=anchor^1; + if(scenario==2) eagle_putword(s.players[other].body+5,x+120); + if(scenario==3) eagle_putword(s.players[anchor].body+8,y-48); + if(scenario==4) {s.players[other].status=MMX_COOP_FALLEN;s.players[other].body[0x27]=0;} + unsigned before[2]={coop_x(&s.players[0]),coop_x(&s.players[1])}; + s.current=s.anchor=(uint8_t)anchor;s.scene_owner=s.scene_phase=s.stage_pending=0; + memcpy(g_ram+0xba8,s.players[anchor].body,sizeof(s.players[0].body)); + MmxZeroSetState(s.players[anchor].zero);MmxCoopSetState(&s); + /* Wind substate, facing toward the players, with time remaining. */ + g_ram[boss+1]=4;g_ram[boss+2]=0;g_ram[boss+3]=4; + eagle_putword(g_ram+boss+5,scenario==1?x-100:x+100); + eagle_putword(g_ram+boss+8,scenario==3?y-48:y);eagle_putword(g_ram+boss+0x10,scenario==1?0x4000:0); + eagle_putword(g_ram+boss+0x34,100);g_ram[boss+0x39]=2; + coop_frames(1,0,0);s=MmxCoopGetState(); + int delta=scenario==1?2:-2; + fprintf(stderr,"Eagle wind anchor=%u case=%u x=%u/%u -> %u/%u timer=%u state=%u/%u/%u\n", + anchor,scenario,before[0],before[1],coop_x(&s.players[0]),coop_x(&s.players[1]), + eagle_word(g_ram+boss+0x34),g_ram[boss+1],g_ram[boss+2],g_ram[boss+3]); + check(coop_x(&s.players[anchor])==before[anchor]+delta,"native world player receives wind"); + if(scenario<4) check(coop_x(&s.players[other])==before[other]+(scenario<2?delta:0), + "partner wind obeys native facing and height rules"); + check(eagle_word(g_ram+boss+0x34)==99,"wind timer advances only once"); + } + size_t n=RtlSaveSnapshotToMemory(start,cap); + coop_frames(4,0,0);size_t en=RtlSaveSnapshotToMemory(expected,cap); + check(RtlLoadSnapshotFromMemory(start,n),"wind state reloads");coop_frames(4,0,0); + same(expected,en,actual,RtlSaveSnapshotToMemory(actual,cap),"wind snapshot replay is deterministic"); + puts("MMX CO-OP EAGLE WIND CHECKS PASSED"); +} + +static bool drex_move_probe; +static void drex_carry_probe(CpuState *cpu,uint32_t pc) { + (void)pc; + if(drex_move_probe && cpu->D==0xe68 && g_ram[cpu->D+10]==0x62) { + eagle_putword(g_ram+cpu->D+5,eagle_word(g_ram+cpu->D+5)+1); + drex_move_probe=false; + } +} +static void coop_drex_rider_checks(const char *fixture,uint8_t *start, + uint8_t *expected,uint8_t *actual,size_t cap) { + interp_bridge_add_pre_opcode_hook(0x82c715,drex_carry_probe); + for(unsigned roster=0;roster<2;++roster) for(unsigned anchor=0;anchor<2;++anchor) { + check(MmxCoopEnable(0) && RtlLoadSnapshot(fixture),"private D-Rex checkpoint loads"); + MmxCoopCapture(g_ram);MmxCoopState s=MmxCoopGetState(); + if(roster) {MmxCoopPlayer p=s.players[0];s.players[0]=s.players[1];s.players[1]=p;} + unsigned boss=0xe68,x=eagle_word(g_ram+boss+5),y=eagle_word(g_ram+boss+8); + check(g_ram[boss+10]==0x62,"slot contains D-Rex's solid lower body"); + /* Isolate the lower platform: preserve head state for boss coordination, + * but omit its solid update so landing cannot stop on the upper head. */ + g_ram[0xea8]=0; + for(unsigned seat=0;seat<2;++seat) { + MmxCoopPlayer *p=&s.players[seat]; + eagle_putword(p->body+5,x);eagle_putword(p->body+8,y-28); + eagle_putword(p->body+0x22,x);eagle_putword(p->body+0x24,y-30); + memset(p->body+0x1a,0,4);p->body[2]=8;p->body[3]=2; + p->body[0x2b]=p->body[0x2c]=0;p->body[0x26]=127;p->body[0x27]=0x98; + p->status=MMX_COOP_ALIVE;p->zero.swap_phase=0; + memset(p->shots,0,sizeof(p->shots)); + } + s.current=s.anchor=(uint8_t)anchor;s.scene_owner=s.scene_phase=s.stage_pending=0; + memcpy(g_ram+0xba8,s.players[anchor].body,sizeof(s.players[0].body)); + MmxZeroSetState(s.players[anchor].zero);MmxCoopSetState(&s); + unsigned together=0; + for(unsigned tick=0;tick<8;++tick) { + coop_frames(1,0,0);s=MmxCoopGetState(); + if((g_ram[boss+0x3f]&3)==3) ++together; + if(tick%10==0) fprintf(stderr,"D-Rex roster=%u anchor=%u t=%u body=%u/%u boss=%u/%u riders=%u ground=%u/%u\n", + roster,anchor,tick,coop_y(&s.players[0]),coop_y(&s.players[1]), + eagle_word(g_ram+boss+5),eagle_word(g_ram+boss+8),g_ram[boss+0x3f], + s.players[0].body[0x2c],s.players[1].body[0x2c]); + } + check(together>=5,"both characters land on and remain riders of D-Rex"); + check(abs((int)coop_x(&s.players[0])-(int)coop_x(&s.players[1]))<=2, + "D-Rex movement carries both riders equally"); + unsigned bx=eagle_word(g_ram+boss+5),px[2]={coop_x(&s.players[0]),coop_x(&s.players[1])}; + /* The owner's lower body is stationary in this phase. Introduce one + * pixel of movement at the actual native carry call, once for both seats. */ + drex_move_probe=true; + coop_frames(1,0,0);s=MmxCoopGetState(); + int dx=(int)eagle_word(g_ram+boss+5)-(int)bx; + fprintf(stderr,"D-Rex carry delta=%d players=%d/%d\n",dx, + (int)coop_x(&s.players[0])-(int)px[0],(int)coop_x(&s.players[1])-(int)px[1]); + check(dx!=0 && (int)coop_x(&s.players[0])-(int)px[0]==dx && + (int)coop_x(&s.players[1])-(int)px[1]==dx,"moving D-Rex carries each actual rider"); + size_t n=RtlSaveSnapshotToMemory(start,cap); + coop_frames(5,0,0);size_t en=RtlSaveSnapshotToMemory(expected,cap); + check(RtlLoadSnapshotFromMemory(start,n),"D-Rex riders reload");coop_frames(5,0,0); + same(expected,en,actual,RtlSaveSnapshotToMemory(actual,cap),"D-Rex rider ownership replays deterministically"); + } + puts("MMX CO-OP D-REX RIDER CHECKS PASSED"); +} + static void coop_x1_effect_checks(const char *fixture) { check(MmxCoopEnable(0) && RtlLoadSnapshot(fixture),"X1 effect fixture loads"); check(g_ram[0xbdb]==4,"Sting fixture has native Chameleon Sting selected"); diff --git a/tests/mmx_coop_test.inc b/tests/mmx_coop_test.inc index 23839cf..a436792 100644 --- a/tests/mmx_coop_test.inc +++ b/tests/mmx_coop_test.inc @@ -1054,6 +1054,8 @@ static void coop_checks(const char *assets, const char *fixture, MmxZeroRegisterHooks(); MmxCoopRegisterHooks(); const char *x2=getenv("MMX_COOP_X2_ASSETS"),*x3=getenv("MMX_COOP_X3_ASSETS"); if (x2 && x3) check(MmxWeaponsLoadPage(x2,1) && MmxWeaponsLoadPage(x3,2),"private original weapon source packs load"); + if(getenv("MMX_COOP_DREX_FIXTURE")) {coop_drex_rider_checks(getenv("MMX_COOP_DREX_FIXTURE"),start,expected,actual,cap);return;} + if(getenv("MMX_COOP_EAGLE_WIND_FIXTURES")) {coop_eagle_wind_checks(getenv("MMX_COOP_EAGLE_WIND_FIXTURES"),start,expected,actual,cap);return;} if(getenv("MMX_COOP_VILE_SCRIPT_FIXTURE")) { if(getenv("MMX_COOP_BOUNCE_TEST")) coop_bounce_checks(getenv("MMX_COOP_VILE_SCRIPT_FIXTURE")); else coop_vile_checks(getenv("MMX_COOP_VILE_SCRIPT_FIXTURE")); diff --git a/tools/apply_coop_hooks.py b/tools/apply_coop_hooks.py index 8d6ced2..91b1076 100644 --- a/tools/apply_coop_hooks.py +++ b/tools/apply_coop_hooks.py @@ -9,7 +9,8 @@ 0x049b03, 0x049b43, 0x019d67, 0xde9d, 0xdebc, 0xe543, 0x049c0e, 0x018a5c, 0x018a92, 0x018add, 0x9d9e, 0x01e70d, 0x01ec98, 0xd48d, 0xde40, 0x03a8bd, 0x04ab81, 0x04ab56, 0x07c07a, 0xf478, 0x019c70, 0x0897ba, - 0x079794, 0x07add8, 0x07af10, 0x07b91c, 0x07ba72, 0x07ba44, 0x07bb09, 0x02c715} + 0x079794, 0x07add8, 0x07af10, 0x07b91c, 0x07ba72, 0x07ba44, 0x07bb09, 0x02c715, + 0x07dabd, 0x08c333} VIEW_TARGETS={0x00dc36,0x00dcdb,0x02806e,0x02808f,0x00d4aa} TARGETS |= VIEW_TARGETS # Routed through the interpreter when the generated code has them as entries,