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
4 changes: 4 additions & 0 deletions docs/coop-interaction-ownership.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ Each player gets the native result. Behaviour is the same for X and Zero.
| Item platforms `$0E/$0F/$10/$13/$14` | `platform_hook`, `$84:AB81/AB56` (returns `$84:AC34/AB80`) | Top and side contact retried for the partner, with the caller's entry registers. `.2C` rider bits are kept per seat. | — |
| Solid enemies using `$82:D7D7`: Storm Eagle E-tank elevator (`$59` top, `$5A` column), Flame Mammoth scrap blocks (`$2A`) | `lift_contact_hook`, `lift_rtl_hook`, `lift_elevator()` | The contact query is retried for the partner and both answers are OR-ed into `.2C`. The handler gets the first seat's registers back. Only the elevator top carries the partner (`MmxCoopLiftCarry`). | `$82:D7D7` callers that are not listed still touch only the current seat. The `lift-contact` trace rows name every caller class. |
| Armored Armadillo minecart (enemy `$2B`) | `lift_contact_hook`, `cart_hook`, `$82:D7D7`, `$88:9821..9867` | Query each player, move the cart once, then apply its native carry code separately to both riders. `.2C/.38` retain per-seat rider bits in WRAM; bit 0 remains the native "any rider" test. | Old single-rider snapshots are imported on first contact. |
| Kuwanger small moving lift (enemy `$16`) | `platform_hook`, `$84:AB81/AB56` | Uses the same per-seat `.2C` support as item platforms, from an enemy slot. | Both characters in either seat. |
| Kuwanger main elevator (enemy `$3D`) | `kuwanger_lift_hook`, `$87:AF10..AF5C`; `kuwanger_carry_hook`, `$82:C715..C733` | Retry only the custom boarding check, then carry each actual rider by the native delta. Its stage/camera script runs once. `.3F` keeps the seat bits in WRAM. | A partner boarding alone does not carry the player left on the ledge. |
| Kuwanger red platforms (enemy `$3F`) | `lift_contact_hook`, `$82:D7D7`; `kuwanger_carry_hook`, `$82:C715..C733` | Retry contact and carry only the actual riders. `.3F` keeps the seat bits in WRAM. | Native movement and firing advance once. |
| Kuwanger laser sensors/turrets (enemies `$43/$44`) | `laser_contact_hook`, `$84:9C0E`; `laser_target_hook`, `$87:BA5C/BB09/BB0D` | Either seat may trip a sensor. Its `.3F` seat is copied to each armed turret, which projects that player only while calculating its delayed shot direction. | Target ownership survives snapshots; normal world projection is restored afterwards. |
| Pickups (health, energy, Sub Tanks, Heart Tanks, 1-ups) | `pickup_hook`, `$84:9C0E..9D06`, `$00:D2E6..D31B` | Contact retried for the partner. The first seat to touch an item owns it (`pickup_owner`) and receives its effect. While a collector is parked for a refill (`$1F19`, the Heart Tank pause, or action `$18` alone, as a Sub Tank does), the other player's update is parked too (`refill_paused`, `partner_parked`). | **Review:** Heart Tanks and Sub Tanks are shared progression. Decide whether Zero may collect them, or only X. |
| Player-terrain and object helpers | `object_hook`, `$00:D2BD..D47F`, `$81:9D67` | The anchor's helper call is replayed for the partner, then the anchor's registers are restored. | — |
| Dash effects | `dash_effect_hook` (`$81:9C86`, `$80:F478/F47C`) | Each seat's dust and effects belong to that seat. | — |
Expand Down
152 changes: 149 additions & 3 deletions src/mmx_coop.c
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,9 @@ static struct {
bool ready;uint8_t pass,first,flags,previous,p,db;
uint16_t d,s,a,x,y,ra,rx,ry;uint8_t rp,rdb;
} cart;
static struct {
uint8_t pass,first,original,p,db,rp,rdb;uint16_t d,s,a,x,y,ra,rx,ry;
} elevator_move;
static uint32_t lift_stack_ret(const CpuState *cpu) {
return cpu->S<0x1ffd ? (uint32_t)(g_ram[cpu->S+1]|g_ram[cpu->S+2]<<8|g_ram[cpu->S+3]<<16) : 0;
}
Expand All @@ -862,12 +865,16 @@ static bool lift_elevator(unsigned d) {
/* Solid enemies that query $82:D7D7 for the current seat only:
* 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), and Armored Armadillo's minecart ($2B). */
* ($2A, from $87:9C7B/9D89), Armored Armadillo's minecart ($2B), and
* Kuwanger's red moving platforms ($3F). */
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;
return c==0x59 || c==0x5a || c==0x2a || c==0x2b || c==0x3f;
}
static void laser_reset(void);
static void lift_reset(void) {
lift.pass=0;lift.carry=0;cart.ready=false;cart.pass=0;elevator_move.pass=0;laser_reset();
}
static void lift_reset(void) { lift.pass=0;lift.carry=0;cart.ready=false;cart.pass=0; }
static void lift_close(void) {
if(lift.pass==2) MmxCoopSelect(g_ram,lift.first);
lift.pass=0;
Expand All @@ -886,6 +893,130 @@ static void lift_contact_hook(CpuState *cpu,uint32_t pc) {
lift.a=cpu->A;lift.x=cpu->X;lift.y=cpu->Y;lift.p=cpu->P;lift.db=cpu->DB;
lift.entry_2c=g_ram[cpu->D+0x2c];lift.ret=lift_stack_ret(cpu);
}
/* Kuwanger's main elevator uses its own fixed-width top check rather than
* $82:D7D7. Replay only that check; its movement and camera script run once. */
static void kuwanger_lift_hook(CpuState *cpu,uint32_t pc) {
unsigned d=cpu->D;
if(!enabled || !state.initialized || state.menu_owner || state.scene_owner ||
d<0xe68 || d>=0x1228 || (d-0xe68)%64 || !g_ram[d] || g_ram[d+10]!=0x3d) return;
if((pc&65535)==0xaf10) {
if(lift.pass || state.players[state.current^1].status!=MMX_COOP_ALIVE ||
!(state.players[state.current^1].body[0x27]&127)) return;
cpu_mirrors_to_p(cpu);
lift.pass=1;lift.first=state.current;lift.d=(uint16_t)d;lift.s=cpu->S;
lift.a=cpu->A;lift.x=cpu->X;lift.y=cpu->Y;lift.p=cpu->P;lift.db=cpu->DB;
lift.entry_2c=g_ram[d+0x2c];g_ram[d+0x3f]=0;return;
}
if(!lift.pass || cpu->S!=lift.s || d!=lift.d) return;
if(lift.pass==1) {
if(g_ram[d+0x2c]&4) g_ram[d+0x3f]|=1u<<state.current;
lift.first_2c=g_ram[d+0x2c];cpu_mirrors_to_p(cpu);
lift.ra=cpu->A;lift.rx=cpu->X;lift.ry=cpu->Y;lift.rp=cpu->P;lift.rdb=cpu->DB;
MmxCoopSelect(g_ram,lift.first^1);lift.pass=2;g_ram[d+0x2c]=lift.entry_2c;
cpu->A=lift.a;cpu->X=lift.x;cpu->Y=lift.y;cpu->P=lift.p;cpu->DB=lift.db;cpu_p_to_mirrors(cpu);
interp_bridge_pre_opcode_redirect(0x87af10);return;
}
if(g_ram[d+0x2c]&4) g_ram[d+0x3f]|=1u<<state.current;
g_ram[d+0x2c]|=lift.first_2c;MmxCoopSelect(g_ram,lift.first);
cpu->A=lift.ra;cpu->X=lift.rx;cpu->Y=lift.ry;cpu->P=lift.rp;cpu->DB=lift.rdb;cpu_p_to_mirrors(cpu);
lift.pass=0;
}
/* .3F records the elevator/red platform's two answers. Apply the native delta only
* to those riders, so a partner boarding alone cannot drag the other body. */
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;
if((pc&65535)==0xc715) {
if(elevator_move.pass) return;
unsigned riders=g_ram[d+0x3f]&3;
for(unsigned seat=0;seat<2;++seat)
if(state.players[seat].status!=MMX_COOP_ALIVE || !(state.players[seat].body[0x27]&127)) riders&=~(1u<<seat);
if(!riders) riders=1u<<state.current; /* Original single-rider save/pass. */
elevator_move.pass=1;elevator_move.original=state.current;
elevator_move.first=(riders&(1u<<state.current)) ? state.current : state.current^1;
elevator_move.d=(uint16_t)d;elevator_move.s=cpu->S;
cpu_mirrors_to_p(cpu);elevator_move.a=cpu->A;elevator_move.x=cpu->X;elevator_move.y=cpu->Y;
elevator_move.p=cpu->P;elevator_move.db=cpu->DB;
MmxCoopSelect(g_ram,elevator_move.first);return;
}
if(!elevator_move.pass || elevator_move.d!=d || elevator_move.s!=cpu->S) return;
if(elevator_move.pass==1 && (g_ram[d+0x3f]&(1u<<(elevator_move.first^1))) &&
state.players[elevator_move.first^1].status==MMX_COOP_ALIVE &&
(state.players[elevator_move.first^1].body[0x27]&127)) {
cpu_mirrors_to_p(cpu);elevator_move.ra=cpu->A;elevator_move.rx=cpu->X;elevator_move.ry=cpu->Y;
elevator_move.rp=cpu->P;elevator_move.rdb=cpu->DB;
MmxCoopSelect(g_ram,elevator_move.first^1);elevator_move.pass=2;
cpu->A=elevator_move.a;cpu->X=elevator_move.x;cpu->Y=elevator_move.y;
cpu->P=elevator_move.p;cpu->DB=elevator_move.db;cpu_p_to_mirrors(cpu);
interp_bridge_pre_opcode_redirect(0x82c715);return;
}
MmxCoopSelect(g_ram,elevator_move.original);
if(elevator_move.pass==2) {
cpu->A=elevator_move.ra;cpu->X=elevator_move.rx;cpu->Y=elevator_move.ry;
cpu->P=elevator_move.rp;cpu->DB=elevator_move.rdb;cpu_p_to_mirrors(cpu);
}
elevator_move.pass=0;
}

/* Laser sensors ($43) test a body directly through $84:9C0E. A successful
* contact arms all $44 turrets, whose delayed aim must keep that seat rather
* than use whichever body happens to drive the world twenty frames later.
* Native $43/$44 code leaves .3F unused; ownership stays in snapshot WRAM. */
static struct {uint8_t pass,first,p,db,aim_return,sensor_return;uint16_t d,s,a,x,y;} laser;
static void laser_reset(void) {memset(&laser,0,sizeof(laser));}
static void laser_contact_hook(CpuState *cpu,uint32_t pc) {
unsigned d=cpu->D,at=pc&65535;
if(!enabled || !state.initialized || state.menu_owner || state.scene_owner ||
d<0xe68 || d>=0x1228 || (d-0xe68)%64 || !g_ram[d] || g_ram[d+10]!=0x43) return;
if(at==0x9c0e) {
if(laser.pass || cpu->X!=0xba8) return;
cpu_mirrors_to_p(cpu);laser.pass=1;laser.first=state.current;laser.d=(uint16_t)d;laser.s=cpu->S;
laser.a=cpu->A;laser.x=cpu->X;laser.y=cpu->Y;laser.p=cpu->P;laser.db=cpu->DB;return;
}
if(!laser.pass || laser.s!=cpu->S || laser.d!=d) return;
if(cpu->_flag_C) g_ram[d+0x3f]=(uint8_t)(state.current+1);
else if(laser.pass==1 && state.players[laser.first^1].status==MMX_COOP_ALIVE &&
(state.players[laser.first^1].body[0x27]&127)) {
MmxCoopSelect(g_ram,laser.first^1);laser.pass=2;
cpu->A=laser.a;cpu->X=laser.x;cpu->Y=laser.y;cpu->P=laser.p;cpu->DB=laser.db;cpu_p_to_mirrors(cpu);
interp_bridge_pre_opcode_redirect(0x849c0e);return;
}
if(laser.pass==2) {
/* Directional sensors also aim an immediate shot after this query.
* Keep its tripper projected through that handler, then restore the
* original world actor even if the one-shot sensor deletes itself. */
if(cpu->_flag_C) laser.sensor_return=(uint8_t)(laser.first+1);
else MmxCoopSelect(g_ram,laser.first);
}
laser.pass=0;
}
static void laser_target_hook(CpuState *cpu,uint32_t pc) {
if(!enabled || !state.initialized) return;
unsigned d=cpu->D,at=pc&65535;
if(at==0xb92f && laser.sensor_return && d==laser.d) {
MmxCoopSelect(g_ram,laser.sensor_return-1);laser.sensor_return=0;return;
}
if(d<0xe68 || d>=0x1228 || (d-0xe68)%64 || !g_ram[d]) return;
if(at==0xb91c || at==0xba72) {
if(!g_ram[d+1]) g_ram[d+0x3f]=0;
return;
}
if(g_ram[d+10]!=0x44) return;
if(at==0xba5c) {
unsigned sensor=word(g_ram+2);
if(sensor>=0xe68 && sensor<0x1228 && !((sensor-0xe68)%64) && g_ram[sensor+10]==0x43)
g_ram[d+0x3f]=g_ram[sensor+0x3f];
} else if(at==0xbb09) {
unsigned owner=g_ram[d+0x3f];
if(owner && owner<=2 && state.players[owner-1].status==MMX_COOP_ALIVE &&
(state.players[owner-1].body[0x27]&127)) {
laser.aim_return=(uint8_t)(state.current+1);MmxCoopSelect(g_ram,owner-1);
}
} else if(at==0xbb0d && laser.aim_return) {
MmxCoopSelect(g_ram,laser.aim_return-1);laser.aim_return=0;
}
}
static void lift_rtl_hook(CpuState *cpu,uint32_t pc) {
/* Same stack depth, slot and return address: D7D7's own RTL, not another
* bank $82 routine returning to a different caller. */
Expand All @@ -904,6 +1035,9 @@ 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)
g_ram[d+0x3f]=(uint8_t)(((lift.first_2c&1)?1u<<lift.first:0)|
(second_rides?1u<<(lift.first^1):0));
if(g_ram[d+10]==0x2b) {
cart.ready=true;cart.pass=0;cart.first=lift.first;cart.d=(uint16_t)d;
cart.flags=(combined&~6u)|((lift.first_2c&1)<<(lift.first+1))|
Expand Down Expand Up @@ -1271,6 +1405,8 @@ static void object_hook(CpuState *cpu, uint32_t pc) {
* use .2C as a boolean rider latch. (Storm Eagle's E-tank elevator, enemy
* $59, does not call these helpers; see storm-eagle-collision-handoff.md.) */
static bool platform_item(unsigned d) {
/* Kuwanger's little lift uses the same helpers from an enemy slot. */
if(d>=0xe68 && d<0x1228 && !((d-0xe68)%64)) return g_ram[d] && g_ram[d+10]==0x16;
if(d<0x1628 || d>=0x1928 || (d-0x1628)%48) return false;
unsigned c=g_ram[d+10];
return (c>=0x0e && c<=0x10) || c==0x13 || c==0x14;
Expand Down Expand Up @@ -1659,6 +1795,13 @@ void MmxCoopRegisterHooks(void) {
interp_bridge_set_pre_opcode_hook(0x87c0ae,eagle_lift_hook);
interp_bridge_set_pre_opcode_hook(0x87c0b4,eagle_lift_hook);
interp_bridge_set_pre_opcode_hook(0x82d7d7,lift_contact_hook);
interp_bridge_set_pre_opcode_hook(0x87af10,kuwanger_lift_hook);
interp_bridge_set_pre_opcode_hook(0x87af5c,kuwanger_lift_hook);
interp_bridge_set_pre_opcode_hook(0x82c715,kuwanger_carry_hook);
interp_bridge_set_pre_opcode_hook(0x82c733,kuwanger_carry_hook);
const unsigned turrets[]={0x87b91c,0x87b92f,0x87ba72,0x87ba5c,0x87bb09,0x87bb0d};
for(unsigned i=0;i<sizeof(turrets)/sizeof(turrets[0]);++i)
interp_bridge_set_pre_opcode_hook(turrets[i],laser_target_hook);
interp_bridge_set_pre_opcode_hook(0x889821,cart_hook);
interp_bridge_set_pre_opcode_hook(0x889867,cart_hook);
const unsigned platforms[]={0x84ab81,0x84ac34,0x84ab56,0x84ab80};
Expand Down Expand Up @@ -1688,6 +1831,9 @@ void MmxCoopRegisterHooks(void) {
const unsigned pickups[]={0xd2e6,0xd2ed,0xd308,0xd31b,0x849c0e,0x849c15,0x849c1d,0x849d06};
for(unsigned i=0;i<sizeof(pickups)/sizeof(pickups[0]);++i)
interp_bridge_set_pre_opcode_hook(pickups[i],pickup_hook);
const unsigned sensors[]={0x849c0e,0x849c15,0x849c1d,0x849d06};
for(unsigned i=0;i<sizeof(sensors)/sizeof(sensors[0]);++i)
interp_bridge_add_pre_opcode_hook(sensors[i],laser_contact_hook);
const unsigned doors[]={0x81e70d,0x81e724,0x81e725,0x81ec98,0x81ecc6,0x81ecc7};
for(unsigned i=0;i<sizeof(doors)/sizeof(doors[0]);++i)
interp_bridge_set_pre_opcode_hook(doors[i],door_hook);
Expand Down
91 changes: 91 additions & 0 deletions tests/mmx_coop_kuwanger_test.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/* Private owner saves: slot 01 sensors, 09 small lift, 10 red platforms,
* and 12 main elevator. Check both character assignments and world actors. */
static void kuwanger_dump(unsigned t) {
MmxCoopState s=MmxCoopGetState();
fprintf(stderr,"kuwanger t=%u anchor=%u p1=%u/%u p2=%u/%u hp=%u/%u\n",t,s.anchor,
coop_x(&s.players[0]),coop_y(&s.players[0]),coop_x(&s.players[1]),coop_y(&s.players[1]),
s.players[0].body[0x27]&127,s.players[1].body[0x27]&127);
for(unsigned d=0xe68;d<0x1228;d+=64) if(g_ram[d])
fprintf(stderr," enemy %04x kind=%02x state=%02x/%02x xy=%u/%u contact=%02x timer=%02x owner=%02x\n",
d,g_ram[d+10],g_ram[d+1],g_ram[d+2],eagle_word(g_ram+d+5),eagle_word(g_ram+d+8),
g_ram[d+0x2c],g_ram[d+0x36],g_ram[d+0x3f]);
}
static void kuwanger_roster(unsigned roster,unsigned anchor) {
MmxCoopState s=MmxCoopGetState();
for(unsigned seat=0;seat<2;++seat) {
s.players[seat].character=(uint8_t)(roster^seat);s.players[seat].zero.active_x=!(roster^seat);
}
MmxZeroSetState(s.players[s.current].zero);MmxCoopSetState(&s);MmxCoopSelect(g_ram,anchor);
s=MmxCoopGetState();s.anchor=(uint8_t)anchor;MmxCoopSetState(&s);
}
static void kuwanger_place(unsigned seat,unsigned x,unsigned y) {
unsigned current=MmxCoopGetState().current;MmxCoopSelect(g_ram,seat);
eagle_putword(g_ram+0xbad,x);eagle_putword(g_ram+0xbca,x);
eagle_putword(g_ram+0xbb0,y);eagle_putword(g_ram+0xbcc,y-2);
eagle_putword(g_ram+0xbc2,0);eagle_putword(g_ram+0xbc4,0xfd00);
g_ram[0xbaa]=8;g_ram[0xbab]=2;g_ram[0xbd3]=g_ram[0xbd4]=0;
g_ram[0xbce]=127;g_ram[0xc11]=0;MmxCoopCapture(g_ram);MmxCoopSelect(g_ram,current);
}
static unsigned kuwanger_aims[2];
static void kuwanger_aim_probe(CpuState *cpu,uint32_t pc) {
(void)pc;
if(cpu->D>=0xe68 && cpu->D<0x1228 && !((cpu->D-0xe68)%64) && g_ram[cpu->D+10]==0x44)
++kuwanger_aims[MmxCoopGetState().current];
}
static void coop_kuwanger_checks(const char *directory) {
for(unsigned roster=0;roster<2;++roster) for(unsigned anchor=0;anchor<2;++anchor) {
fprintf(stderr,"kuwanger support roster=%u anchor=%u\n",roster,anchor);
owner_wide_load(directory,11);kuwanger_roster(roster,anchor);
coop_frames(60,SNES_PAD_RIGHT,SNES_PAD_RIGHT);coop_frames(120,0,0);
MmxCoopState s=MmxCoopGetState();kuwanger_dump(180);
check(abs((int)coop_y(&s.players[0])-(int)coop_y(&s.players[1]))<=2 &&
coop_y(&s.players[0])<4440 && (g_ram[0xea8+0x3f]&3)==3,
"both players naturally board and ascend Kuwanger's main elevator");
owner_wide_load(directory,11);kuwanger_roster(roster,anchor);
s=MmxCoopGetState();unsigned original_x=coop_x(&s.players[0]);
coop_frames(60,0,SNES_PAD_RIGHT);coop_frames(120,0,0);s=MmxCoopGetState();kuwanger_dump(180);
check(coop_x(&s.players[0])<=original_x+16 && coop_y(&s.players[0])==4431 &&
coop_y(&s.players[1])<4440 && g_ram[0xea8+0x3f]==2,
"P2 can start and ride the elevator without carrying P1 from the ledge");
owner_wide_load(directory,8);kuwanger_roster(roster,anchor);
kuwanger_place(1,eagle_word(g_ram+0xe68+5)+4,eagle_word(g_ram+0xe68+8)-36);
coop_frames(50,0,0);s=MmxCoopGetState();kuwanger_dump(50);
check(g_ram[0xe68+0x2c]==3 && abs((int)coop_y(&s.players[0])-(int)coop_y(&s.players[1]))<=2,
"P2 lands on and follows the small moving lift beside P1");
owner_wide_load(directory,9);kuwanger_roster(roster,anchor);
unsigned px=eagle_word(g_ram+0xe68+5),py=eagle_word(g_ram+0xe68+8);
/* Board the exposed right end; the left half initially crosses a wall. */
kuwanger_place(0,px+24,py-28);kuwanger_place(1,px+28,py-28);
coop_frames(40,0,0);s=MmxCoopGetState();kuwanger_dump(40);
check(g_ram[0xe68+0x3f]==3 && abs((int)coop_y(&s.players[0])-(int)coop_y(&s.players[1]))<=2 &&
coop_x(&s.players[0])>px+24 && coop_x(&s.players[1])>px+28,
"both players land on and move with the red platform");
owner_wide_load(directory,9);kuwanger_roster(roster,anchor);
kuwanger_place(1,px+28,py-28);coop_frames(40,0,0);s=MmxCoopGetState();
check(g_ram[0xe68+0x3f]==2 && coop_y(&s.players[0])==2495 &&
coop_x(&s.players[1])>px+28 && coop_y(&s.players[1])<2440,
"a lone P2 rides the red platform without carrying P1 underneath");
}
interp_bridge_add_pre_opcode_hook(0x87bb09,kuwanger_aim_probe);
for(unsigned roster=0;roster<2;++roster) for(unsigned anchor=0;anchor<2;++anchor)
for(unsigned tripper=0;tripper<2;++tripper) {
owner_wide_load(directory,0);kuwanger_roster(roster,anchor);
unsigned d=0xe68,turret=0xea8,x=eagle_word(g_ram+d+5),y=eagle_word(g_ram+d+8);
g_ram[d+1]=2;g_ram[d+0x34]=50;g_ram[d+0x3a]=0;g_ram[d+0x33]=1;
kuwanger_place(tripper,x+20,y+10);kuwanger_place(tripper^1,x+110,y+59);
memset(kuwanger_aims,0,sizeof(kuwanger_aims));coop_frames(1,0,0);kuwanger_dump(1);
check(g_ram[turret+0x36] && g_ram[turret+0x3f]==tripper+1,
"either player's laser contact arms the turret with its actual tripper");
check(MmxCoopGetState().current==anchor,"sensor returns the normal world actor after its trigger");
const size_t cap=2u*1024u*1024u;uint8_t *before=malloc(cap),*expected=malloc(cap),*actual=malloc(cap);
check(before && expected && actual,"laser ownership replay buffers allocate");
size_t n=RtlSaveSnapshotToMemory(before,cap);coop_frames(40,0,0);
check(kuwanger_aims[tripper]>0 && !kuwanger_aims[tripper^1],
"the native delayed laser shot aims at the player who tripped the sensor");
size_t en=RtlSaveSnapshotToMemory(expected,cap);
check(RtlLoadSnapshotFromMemory(before,n),"armed laser snapshot restores");coop_frames(40,0,0);
same(expected,en,actual,RtlSaveSnapshotToMemory(actual,cap),"laser target ownership survives snapshot replay");
free(before);free(expected);free(actual);
}
puts("MMX CO-OP KUWANGER CHECKS PASSED");
}
Loading
Loading