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
15 changes: 15 additions & 0 deletions src/mmx_zero.c
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,21 @@ unsigned MmxZeroUpgradeBits(unsigned pc, unsigned original) {
default: return original;
}
}
unsigned MmxZeroHeadProbe(const uint8_t r[0x20000], unsigned object, unsigned original) {
/* $81:9EAC probes one pixel above X's standing terrain top (-18).
* Zero's standing top is -26, so the original -19 query misses a block
* that his real ceiling collision has already stopped against. Keep the
* native horizontal probes and destruction path, and keep X/dash boxes. */
return MmxZeroActive() && r && object == 0xba8 && (original == 0xed || original == 0xffed) &&
word(r + object + 0x20) == 0xa552
? original - 8 : original;
}
unsigned MmxZeroHeadBreakY(const uint8_t r[0x20000], unsigned object, unsigned original) {
/* $81:9EF9 stores the independently calculated removal coordinate. Move
* that coordinate by the same amount as the collision query, preserving
* the guest ADC flags just as the original STA does. */
return (original + MmxZeroHeadProbe(r,object,0xed) - 0xed) & 65535;
}

static bool own_projectile(const uint8_t *r, unsigned d) {
return d >= 0x1228 && d < 0x1428 && (d & 63) == 0x28 &&
Expand Down
3 changes: 3 additions & 0 deletions src/mmx_zero.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ bool MmxZeroSlideHold(uint8_t ram[0x20000], unsigned pc);
void MmxZeroSlideTick(uint8_t ram[0x20000]);
/* Character abilities at native instruction PCs, without changing inventory. */
unsigned MmxZeroUpgradeBits(unsigned pc, unsigned original);
/* Native helmet query must reach the active body's ceiling contact row. */
unsigned MmxZeroHeadProbe(const uint8_t ram[0x20000], unsigned object, unsigned original);
unsigned MmxZeroHeadBreakY(const uint8_t ram[0x20000], unsigned object, unsigned original);
void MmxZeroPlayerTick(uint8_t ram[0x20000]);
void MmxZeroPlayerEnd(uint8_t ram[0x20000]);
unsigned MmxZeroChargeTier(const MmxZeroState *snapshot);
Expand Down
20 changes: 19 additions & 1 deletion src/mods/mmx_zero_plugin.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ static void weapon_menu_hook(CpuState *cpu, uint32_t pc) {
static void hook(CpuState *cpu, uint32_t pc) {
if (!MmxZeroEnabled() && !MmxWeaponsEnabled()) return;
switch (pc & 0x7fffff) {
case 0x019ef9:
/* Intercept the STA, which preserves the native ADC flags. */
cpu->A = (uint16_t)MmxZeroHeadBreakY(g_ram, cpu->D, cpu->A);
break;
case 0x019f59: {
unsigned value = MmxZeroHeadProbe(g_ram, cpu->D, cpu->Y);
cpu->Y = (uint16_t)value;
cpu->_flag_Z = !value; cpu->_flag_N = (value & 32768) != 0;
cpu->P = (cpu->P & ~0x82) | (cpu->_flag_Z ? 2 : 0) | (cpu->_flag_N ? 128 : 0);
break;
}
case 0x019eae: {
unsigned value = MmxZeroHeadProbe(g_ram, cpu->D, cpu->A & 255);
cpu->A = (cpu->A & 0xff00) | value;
cpu->_flag_Z = !value; cpu->_flag_N = (value & 128) != 0;
cpu->P = (cpu->P & ~0x82) | (cpu->_flag_Z ? 2 : 0) | (cpu->_flag_N ? 128 : 0);
break;
}
case 0x009dca: MmxZeroHealthRespawn(g_ram); break;
case 0x00d6a7: MmxRendererObserveObject(g_ram, (uint16_t)(cpu->D + cpu->X)); break;
case 0x00d76a: MmxRendererRecordPiece(g_ram, cpu->D); break;
Expand Down Expand Up @@ -198,7 +216,7 @@ static bool zero_terrain_solid(const uint8_t *r, int x, int y) {
void MmxZeroRegisterHooks(void) {
MmxZeroSetTerrainQuery(zero_terrain_solid);
const unsigned pcs[] = {0x009dca, 0x00d6a7, 0x00d76a, 0x01971f, 0x019796, 0x0198ff,
0x019ea8, 0x0499a7,
0x019ea8, 0x019eae, 0x019ef9, 0x019f59, 0x0499a7,
0x01815c, 0x018165, 0x019d47, 0x0194af, 0x00d3e7, 0x00d4f4, 0x00d511, 0x049e1d, 0x049e3a, 0x049e45, 0x049e76, 0x049c19, 0x048f07, 0x048eea, 0x018b04, 0x0491dc, 0x02823e,
0x01898e, 0x018999, 0x018965,
0x028403,0x03958f,0x039dcf,
Expand Down
42 changes: 41 additions & 1 deletion tests/mmx_zero_blocks_test.inc
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,50 @@ static unsigned blocks_cell(unsigned x,unsigned y) {
unsigned screen=g_ram[0xe800+(y>>8)*32+(x>>8)];
return 0x2000+((screen*512+((y&240)<<1)+((x&240)>>3))&65535);
}
/* The old direct-call checks bypassed the movement/contact ordering that
* fails in #137. Arrange only an ordinary grounded body below the actual
* Storm Eagle column; the guest jump must find and destroy its own tiles. */
static void zero_head_contact_checks(void) {
g_ram[0xd1]=2;g_ram[0xd2]=2;g_ram[0xd3]=g_ram[0xd4]=0;
g_ram[0x1f7a]=5;g_ram[0x1f81]=g_ram[0x1f82]=0;g_ram[0x1f99]=0;
for(unsigned i=0;i<1800;++i) {
frame(0);
if(g_ram[0xd2]==4 && g_ram[0xd3]==4 && g_ram[0xba9]==2 &&
g_ram[0xbb6] && g_ram[0xbaa]==0 && g_ram[0xbab]==2 && (g_ram[0xbd3]&4)) break;
}
check(g_ram[0x1f7a]==5 && g_ram[0xd2]==4 && g_ram[0xd3]==4,
"head contact enters original Storm Eagle stage");
check(g_ram[0xbb6] && g_ram[0xbaa]==0 && g_ram[0xbab]==2 && (g_ram[0xbd3]&4),
"head fixture waits for the native teleport and collision enable");
uint8_t *start=malloc(2*1024*1024);check(start!=NULL,"head fixture allocates");
size_t n=RtlSaveSnapshotToMemory(start,2*1024*1024);check(n,"head fixture saves");
for(unsigned modern=0;modern<2;++modern) for(unsigned x=0;x<2;++x)
for(unsigned owned=0;owned<2;++owned) {
check(RtlLoadSnapshotFromMemory(start,n),"head fixture restores");
MmxZeroSetModern(modern);MmxZeroResetState();
MmxZeroState z=MmxZeroGetState();z.active_x=x;MmxZeroSelectState(z);
g_ram[0x1f99]=owned?1:0;
eagle_putword(g_ram+0xbad,3996);eagle_putword(g_ram+0xbb0,655);
eagle_putword(g_ram+0xbca,3996);eagle_putword(g_ram+0xbcc,655);
eagle_putword(g_ram+0xbc2,0);eagle_putword(g_ram+0xbc4,0);
g_ram[0xba9]=2;g_ram[0xbaa]=0;g_ram[0xbab]=2;g_ram[0xbd3]=4;
eagle_putword(g_ram+0x1e4d,3840);eagle_putword(g_ram+0x1e50,512);
unsigned cell=blocks_cell(3984,608),before=eagle_word(g_ram+cell);
check(before==0x48,"head fixture has intact original breakable column");
for(unsigned i=0;i<50;++i) frame(SNES_PAD_B);
bool changed=eagle_word(g_ram+cell)!=before;
printf("HEAD CONTACT modern=%u x=%u owned=%u tile=%04x->%04x y=%u\n",
modern,x,owned,before,eagle_word(g_ram+cell),eagle_word(g_ram+0xbb0));
check(changed==(!x || owned),"normal jump crushes for Zero or helmet-equipped X");
check(g_ram[0x1f99]==(owned?1:0),"normal head contact preserves inventory");
}
free(start);MmxZeroSetModern(false);MmxZeroResetState();
}
static void zero_blocks_checks(const char *assets) {
check(assets && MmxZeroLoad(assets),"block test loads X3 Zero");
MmxZeroRegisterHooks();
hitbox_boot();
zero_head_contact_checks();
g_ram[0xd1]=2;g_ram[0xd2]=2;g_ram[0xd3]=g_ram[0xd4]=0;
g_ram[0x1f7a]=4;g_ram[0x1f81]=g_ram[0x1f82]=0;g_ram[0x1f99]=0;
for(unsigned i=0;i<1800;++i) {
Expand Down Expand Up @@ -35,7 +75,7 @@ static void zero_blocks_checks(const char *assets) {
MmxZeroSetModern(modern);MmxZeroResetState();
MmxZeroState z=MmxZeroGetState();z.active_x=x;MmxZeroSelectState(z);
g_ram[0x1f99]=owned?(foot?8:1):0;
eagle_putword(g_ram+0xbad,bx+8);eagle_putword(g_ram+0xbb0,by+(foot?-8:19));
eagle_putword(g_ram+0xbad,bx+8);eagle_putword(g_ram+0xbb0,by+(foot?-8:x?19:27));
g_ram[0xbd1]=0;
unsigned cell=blocks_cell(bx,by),before=eagle_word(g_ram+cell),inventory=g_ram[0x1f99];
CpuState cpu=g_cpu;cpu.PB=0x81;cpu.DB=0;cpu.D=0xba8;cpu.S=0x1e00;cpu.P=0x30;
Expand Down
6 changes: 6 additions & 0 deletions tests/mmx_zero_test.c
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ int main(void) {
assert(MmxZeroUpgradeBits(0x81971c, 0) == 0);
asset("zero-test.bin"); assert(MmxZeroLoad("zero-test.bin"));
assert(MmxZeroUpgradeBits(0x81971c, 2) == 10);
ram[0xbc8]=0x52;ram[0xbc9]=0xa5;
assert(MmxZeroHeadProbe(ram,0xba8,0xed)==0xe5);
assert(MmxZeroHeadProbe(ram,0xba8,0)==0);
assert(MmxZeroHeadProbe(ram,0xe68,0xed)==0xed);
ram[0xbc8]=0x38;ram[0xbc9]=0xbb;
assert(MmxZeroHeadProbe(ram,0xba8,0xed)==0xed); /* Dash keeps X's terrain height. */
const unsigned crush_sites[] = {0x819ea5, 0x8499a4, 0x8198fc};
for (unsigned modern = 0; modern < 2; ++modern) {
MmxZeroSetModern(modern);
Expand Down
16 changes: 16 additions & 0 deletions tools/apply_zero_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
# Dash exits that would stand Zero up, and the dash blocks that continue it.
SLIDE_PCS = {0x81898e: '8991', 0x818999: '899C', 0x818965: '8971'}
REQUIRED |= SLIDE_PCS.keys() | {0x81a062}
REQUIRED |= {0x819eac, 0x819ef3, 0x819f36}


def apply(text):
Expand All @@ -42,7 +43,22 @@ def apply(text):
if store and int(store[1] == '8') == axis:
output.append(f' {MARKER} {{ extern uint8_t g_ram[0x20000]; extern unsigned MmxZeroWeaponOrigin(const uint8_t *, unsigned, unsigned, unsigned); {store[2]} = (uint16)MmxZeroWeaponOrigin(g_ram, cpu->D, {axis}, {store[2]}); cpu_write_a_m(cpu, {store[2]}); }}\n')
found.add(pc)
if pc == 0x819ef3:
store = re.search(r'cpu_write16\(cpu, cpu->DB, \(uint16\)\(0x0002\), (_v\d+)\);', line)
if store:
output.append(f' {MARKER} {{ extern uint8_t g_ram[0x20000]; extern unsigned MmxZeroHeadBreakY(const uint8_t *, unsigned, unsigned); {store[1]} = (uint16)MmxZeroHeadBreakY(g_ram,cpu->D,{store[1]}); cpu_write_a_m(cpu,{store[1]}); }}\n')
found.add(pc)
output.append(line)
if pc == 0x819f36:
load = re.search(r'uint16 (_v\d+) = 0xffed;', line)
if load:
output.append(f' {MARKER} {{ extern uint8_t g_ram[0x20000]; extern unsigned MmxZeroHeadProbe(const uint8_t *, unsigned, unsigned); {load[1]} = (uint16)MmxZeroHeadProbe(g_ram,cpu->D,{load[1]}); }}\n')
found.add(pc)
if pc == 0x819eac:
load = re.search(r'uint8 (_v\d+) = 0xed;', line)
if load:
output.append(f' {MARKER} {{ extern uint8_t g_ram[0x20000]; extern unsigned MmxZeroHeadProbe(const uint8_t *, unsigned, unsigned); {load[1]} = (uint8)MmxZeroHeadProbe(g_ram,cpu->D,{load[1]}); }}\n')
found.add(pc)
if (pc & 0x7fffff) == 0x049e15:
load = re.search(r'uint16 (_v\d+) = cpu_read16\(cpu,', line) if '0x000a' in line and 'cpu->X' in line else None
if load:
Expand Down
Loading