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
14 changes: 14 additions & 0 deletions docs/boss-rush.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,20 @@ set `MMX_BOSS_RUSH_TEST=1` and pass the verified USA ROM to run the title-menu,
28-pair entrance, replacement, 600-frame combat and snapshot-replay checks in
an empty private working directory. Add `MMX_BOSS_RUSH_COOP=<private Zero asset
file>` to run those checks in local co-op, including one-player and team deaths.
`MMX_BOSS_RUSH_COOP_ISOLATION_TEST=1` checks real fatal boss contacts and damage
to either survivor against all eight Mavericks. It separately restores an
interrupted-hurt state after a partner death and verifies that the native
protection countdown expires; active hurt, scripted locks, existing countdowns
and charged Sting remain intact for both seats.
Ordinary partner deaths did not reproduce the owner's report of permanent
survivor invulnerability on current source. The interrupted-hurt state is
explicitly seeded in its separate regression; recovery for the reported
death-triggered issue remains tentative pending a capture or owner retest.
`MMX_BOSS_RUSH_COOP_WEAPON_TEST=1` quick-switches both players through different
native specials and fires normal and charged shots together. It checks private
graphics for every visible projectile class and writes CSV traces and images.
Both fixtures use the same co-op asset setting above; add
`MMX_BOSS_RUSH_COOP_P1_ZERO=1` to exchange the two characters.
`MMX_BOSS_RUSH_REPORT_TEST=1` checks issue #124: native death updates in both
boss slots without permanent player protection, the retail subtank refill cap
and empty-tank menu navigation, Penguin's native ice lock, and prolonged
Expand Down
1 change: 1 addition & 0 deletions src/mmx_boss_rush_runtime.c
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ void MmxBossRushAfterFrame(uint8_t *r) {
MmxBossRushSetState(&s);
for(unsigned i=0;i<2;++i) if(s.bosses[i].phase==MMX_RUSH_FIGHTING && !s.bosses[i].health)
MmxBossRushDefeat(i);
MmxCoopResumeHurtTimers(r);
bool alive=(r[0xbcf]&127)!=0;
bool death_complete=r[0xbaa]==12 && r[0xbab]>=6;
if(s.coop) {
Expand Down
17 changes: 17 additions & 0 deletions src/mmx_coop.c
Original file line number Diff line number Diff line change
Expand Up @@ -1458,6 +1458,23 @@ void MmxCoopLiftCarry(uint8_t *r) {
uint16_t x=(uint16_t)((body[5]|body[6]<<8)+dx),y=(uint16_t)((body[8]|body[9]<<8)+dy);
body[5]=(uint8_t)x;body[6]=(uint8_t)(x>>8);body[8]=(uint8_t)y;body[9]=(uint8_t)(y>>8);
}
static void resume_hurt_timer(uint8_t *body) {
/* Native $0E starts $0C13, which later clears $0BD8/$0C32. A refill
* or stun can replace that action first while Rush bosses keep fighting.
* Resume the native countdown; never revoke a real hurt reaction, death,
* scripted hold, charged Sting or an already running protection interval. */
if((body[0x27]&127) && body[0x30] && !body[0x6b] && !body[0x6e] && !body[0x89] &&
body[2]!=0x0e && body[2]!=0x0c && body[2]!=0x1e)
body[0x6b]=0x3c;
}
void MmxCoopResumeHurtTimers(uint8_t *r) {
if(!r) return;
resume_hurt_timer(r+0xba8);
if(enabled && state.initialized) {
MmxCoopPlayer *partner=&state.players[state.current^1];
if(partner->status==MMX_COOP_ALIVE) resume_hurt_timer(partner->body);
}
}
/* Stage sections set the player's OBJ priority once, on the world actor:
* entering Storm Eagle's ship writes .11 bit 4 (priority 3, in front of the
* hull's high-priority foreground) to $0BB9 and clears it at the boss lift.
Expand Down
3 changes: 3 additions & 0 deletions src/mmx_coop.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,8 @@ void MmxCoopRegisterHooks(void);
void MmxCoopHostFrame(void);
void MmxCoopLiftCarry(uint8_t *ram);
void MmxCoopSyncPriority(uint8_t *ram);
/* Rush's end-of-frame recovery for a hurt reaction interrupted by a refill
* or another player action. Includes the live body in solo mode. */
void MmxCoopResumeHurtTimers(uint8_t *ram);
/* End-of-frame hook for --coop-trace; no effect unless tracing. */
void MmxCoopTraceFrame(const uint8_t ram[0x20000]);
45 changes: 45 additions & 0 deletions src/mmx_render_assets.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ static MmxSpriteAsset charged_buster[2][21];
static bool charged_buster_ready[2][21];
static MmxSpriteAsset player_weapons[9][2];
static uint8_t player_weapon_ready[9];
static MmxSpriteAsset native_shots[2][8];
static uint64_t native_shot_keys[2][8];
static uint8_t ready[256], sprite_resource[256];
static uint16_t asset_palette[256], rush_palette[13][256];
static unsigned cached_stage = ~0u, cached_section = ~0u;
Expand Down Expand Up @@ -52,6 +54,7 @@ void MmxRenderAssetsSetRom(const uint8_t *bytes, size_t size) {
memset(dash_effect_ready,0,sizeof(dash_effect_ready));
memset(charged_buster_ready,0,sizeof(charged_buster_ready));
memset(player_weapon_ready,0,sizeof(player_weapon_ready));
memset(native_shot_keys,0,sizeof(native_shot_keys));
for(unsigned stage=0;stage<13;++stage) for(unsigned sprite=0;sprite<256;++sprite) {
free(rush_assets[stage][sprite]);rush_assets[stage][sprite]=NULL;
}
Expand Down Expand Up @@ -139,6 +142,48 @@ const MmxSpriteAsset *MmxRenderAssetsChargedBuster(unsigned group,unsigned pose)
art->attributes=6;art->live_colors=true;charged_buster_ready[kind][pose]=true;
return art;
}
const MmxSpriteAsset *MmxRenderAssetsNativeShot(const uint8_t ram[0x20000],
unsigned object,unsigned group,unsigned pose,unsigned seat) {
if(!rom || !ram || seat>1 || group>255 || pose>127 ||
object<0x1228 || object>=0x1428 || (object-0x1228)%64)
return NULL;
unsigned kind=ram[object+10];
unsigned weapon=kind>=7 && kind<=14 ? kind-6 : kind>=16 && kind<=23 ? kind-15 : 0;
if(!weapon) return NULL;
const MmxSpriteAsset *base=MmxRenderAssetsWeaponX(weapon,false);
if(!base) return NULL;
unsigned table=ram_word(ram,object+0x31);
uint64_t key=((uint64_t)kind<<48)|((uint64_t)group<<40)|((uint64_t)table<<24)|
(pose<<8)|1;
unsigned slot=(object-0x1228)/64;
MmxSpriteAsset *art=&native_shots[seat][slot];
if(native_shot_keys[seat][slot]==key) return art;
native_shot_keys[seat][slot]=0;
*art=*base;
/* $84:8FCA reads the object's bank-$85 pose directory. Rebuild its
* uploads privately: empty/short records retain earlier rows. A shot can
* be restored mid-animation, so derive these rows from ROM, not host history. */
if(table>=0x8000) {
size_t directory=lorom(0x850000|table);
for(unsigned frame=0;frame<=pose;++frame) {
size_t list=directory+word(directory+frame*2);
bool complete=false;
for(unsigned n=0;n<32;++n,list+=5) {
if(!range(list,5)) return NULL;
unsigned count=rom[list]*16;
if(!count) {complete=true;break;}
int dest=(rom[list+4]&127)*512-0xc000;
size_t source=lorom(word(list+1)|(rom[list+3]<<16));
if(dest<0 || dest+(int)count>sizeof(art->tiles) || !range(source,count) || rom[list+3]==0x7f)
return NULL;
memcpy(art->tiles+dest,rom+source,count);
if(rom[list+4]&128) {complete=true;break;}
}
if(!complete) return NULL;
}
}
native_shot_keys[seat][slot]=key;return art;
}
bool MmxRenderAssetsStingPalette(unsigned phase,uint16_t colors[16]) {
if(!rom || !colors || phase>14 || (phase&1)) return false;
size_t list=0x30000+(word(0x30133+0x1a0+phase)&0x7fff);
Expand Down
4 changes: 4 additions & 0 deletions src/mmx_render_assets.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ const MmxSpriteAsset *MmxRenderAssetsTeleportX(unsigned pose);
const MmxSpriteAsset *MmxRenderAssetsWeaponX(unsigned weapon, bool body);
/* Full native buster pose, independent of another co-op shot's CHR upload. */
const MmxSpriteAsset *MmxRenderAssetsChargedBuster(unsigned group, unsigned pose);
/* Native special-shot CHR/palette, including pose uploads and child objects.
* Index with the submitted native tile number, independently of shared VRAM. */
const MmxSpriteAsset *MmxRenderAssetsNativeShot(const uint8_t ram[0x20000],
unsigned object,unsigned group,unsigned pose,unsigned seat);
/* Charged Sting's original eight-step body palette sequence. */
bool MmxRenderAssetsStingPalette(unsigned phase, uint16_t colors[16]);
const MmxSpriteAsset *MmxRenderAssetsObjectSprite(const uint8_t ram[0x20000],
Expand Down
30 changes: 17 additions & 13 deletions src/mmx_renderer.c
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ static MmxBossRushState frame_rush;
static MmxBossRushVisualState frame_rush_visual;
typedef struct Piece {
int16_t x, y; uint16_t attr; uint8_t size, animation;
/* Palette occupies bits 1..3; the five spare bits retain charged-shot
* pose 0..20 at submission, without changing the capture record size. */
/* Palette occupies bits 1..3; the five spare bits retain native-shot
* pose 0..31 at submission, without changing the capture record size. */
uint8_t tile, palette_pose; uint16_t object;
} Piece;
static unsigned piece_pose(const Piece *p) {
Expand Down Expand Up @@ -256,7 +256,7 @@ static Piece make_piece(const uint8_t *p, int x, int y, unsigned flip,
y += flip & 0x80 ? -(int8_t)p[2] - size : (int8_t)p[2];
unsigned attr = (((p[4] & 0xce) | attributes) ^ flip) << 8;
attr |= (p[3] + base) & 255;
unsigned retained_pose=(animation==0x0e || animation==0x9e) ? pose&31 : 0;
unsigned retained_pose=object>=0x1228 && object<0x1428 ? pose&31 : 0;
return (Piece){(int16_t)x, (int16_t)y, (uint16_t)attr, (uint8_t)size,
(uint8_t)animation, p[3], (uint8_t)((p[4]&14)|(retained_pose&1)|((retained_pose&30)<<3)), (uint16_t)object};
}
Expand Down Expand Up @@ -1756,16 +1756,17 @@ static void coop_partner_row(const Ppu *ppu,const Raster *r,int y,MmxRenderView
Piece s = make_piece(a+j*4,x,sy,ram[d+17]&64,ram[d+17]&63,ram[d+24],group,d,ram[d+23]&127);
const MmxSpriteAsset *asset=!zero && weapon_colors && zero_actor(d,group) &&
(s.attr&0x0e00) ? &weapon_palette : NULL;
/* Normal Spark and its two wall fragments share original group $47.
* Another seat can replace the shared $6200/$6300 weapon upload. */
if(d>=0x1228 && ram[d+10]==0x0c && group==0x47 && (s.attr&0x0e00)==0x0600)
asset=MmxRenderAssetsWeaponX(6,false);
unsigned tile=s.tile;
if(d>=0x1228) {
const MmxSpriteAsset *shot=MmxRenderAssetsNativeShot(ram,d,group,ram[d+23]&127,frame_coop.current^1);
if(shot) {asset=shot;tile=s.attr&255;}
}
if(d>=0x1228 && ((ram[d+10]==3 && group==0x9e) ||
((ram[d+10]==1 || ram[d+10]==2) && group==0x0e))) {
const MmxSpriteAsset *beam=MmxRenderAssetsChargedBuster(group,ram[d+23]&127);
if(beam) asset=beam;
}
sprite(ppu,zero?r:&partner_raster,s.x,s.y,s.attr,s.size,y,view,objects,false,asset,s.tile,colors,true,false,false);
sprite(ppu,zero?r:&partner_raster,s.x,s.y,s.attr,s.size,y,view,objects,false,asset,tile,colors,true,false,false);
}
}
}
Expand Down Expand Up @@ -2217,6 +2218,10 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) {
bool coop_buster=stage && frame_coop.initialized && s.object>=0x1228 && s.object<0x1428 &&
((frame.ram[s.object+10]==3 && s.animation==0x9e) ||
((frame.ram[s.object+10]==1 || frame.ram[s.object+10]==2) && s.animation==0x0e));
const MmxSpriteAsset *coop_special=stage && frame_coop.initialized &&
s.object>=0x1228 && s.object<0x1428 &&
!frame_weapon_combat.shots[(s.object-0x1228)/64].active ?
MmxRenderAssetsNativeShot(frame.ram,s.object,s.animation,piece_pose(&s),frame_coop.current) : NULL;
bool menu_body = s.object == 0x1988 && (s.animation == 0 || s.animation == 0x18);
bool zero_body = zero && (s.object == 0xba8 || menu_body);
bool triad_x_body=cast_body && !zero && s.object==0xba8;
Expand All @@ -2227,7 +2232,7 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) {
bool zero_armor = zero && (s.object == 0xc38 || s.object == 0xc58 || s.object == 0xc78 ||
(zero_menu && (s.object == 0x1928 || s.object == 0x1948 || s.object == 0x1968)));
bool oam_match = false;
if (g_mmx_render_asset_repairs || coop_buster || zero_body || zero_armor || zero_charge || swap_actor || red_ready || red_death || triad_x_body || triad_armor || frozen_enemy) for (int slot = 16; slot < 128; ++slot) {
if (g_mmx_render_asset_repairs || coop_buster || coop_special || zero_body || zero_armor || zero_charge || swap_actor || red_ready || red_death || triad_x_body || triad_armor || frozen_enemy) for (int slot = 16; slot < 128; ++slot) {
unsigned pos = r->oam[slot * 2], hi = r->high_oam[slot / 4] >> (slot % 4 * 2);
int ox = (pos & 255) | ((hi & 1) << 8); if (ox >= 256) ox -= 512;
if (ox == s.x && (pos >> 8) == ((unsigned)s.y & 255) && r->oam[slot * 2 + 1] == s.attr) {
Expand Down Expand Up @@ -2355,14 +2360,13 @@ bool MmxRendererDraw(uint32_t *out, MmxRenderView view, bool hud) {
* the boots. */
if ((frame_zero.active_x || knc_bugfix) && weapon_colors && zero_actor(s.object, s.animation) &&
(menu || ((attr & 0x0e00) == 0x0200 && (!x_charging(frame.ram) || knc_bugfix)))) asset = &x_weapon_palette;
if(stage && frame_coop.initialized && s.object>=0x1228 && s.object<0x1428 &&
frame.ram[s.object+10]==0x0c && s.animation==0x47 && (s.attr&0x0e00)==0x0600)
asset=MmxRenderAssetsWeaponX(6,false);
if(coop_special) asset=coop_special;
if(coop_buster) {
const MmxSpriteAsset *beam=MmxRenderAssetsChargedBuster(s.animation,piece_pose(&s));
if(beam) asset=beam;
}
sprite(&p, r, s.x, s.y, attr, s.size, y, view, objects, !center, asset, s.tile, object_colors, true, false, red_ready || red_death);
sprite(&p, r, s.x, s.y, attr, s.size, y, view, objects, !center, asset,
coop_special?s.attr&255:s.tile, object_colors, true, false, red_ready || red_death);
}
if (stage) coop_partner_row(&p,r,y,view,objects,object_colors);
int bar_first = -1, bar_count = 0;
Expand Down
Loading
Loading