#!/usr/bin/env python3
"""Native hold/release gating, power, settings separation and physical input."""
from pathlib import Path
from menu_protocol import row_word,down_count,render_probe,requested_menu_expectations
from concurrent.futures import ThreadPoolExecutor
import argparse,hashlib,json,os,struct,subprocess,math
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);ap.add_argument('--baseline',type=Path,required=True);ap.add_argument('--seed',type=Path);ap.add_argument('--section',default='all',choices=['all','gate','power','storage','menu','input','physics','guards']);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(exist_ok=True,parents=True);env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
assert sha(a.baseline)=='443c96a23923e96529120cd19c486db1483101896787c76a8d2082e5836e6186'
def asm(p,body,origin=0x2d5000):
 (p/'driver.s').write_text(('' if '.globl _start' in body else ' .text\n .globl _start\n_start:\n')+body+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),f'{origin:X}',str(p/'code.bin')],stdout=subprocess.DEVNULL,check=True);return(p/'code.bin').read_bytes()
def run(p,body,size,patches=(),profile='current'):
 p.mkdir(exist_ok=True,parents=True);b=bytearray((a.baseline if profile=='parent' else a.rom).read_bytes());code=asm(p,' move.w #0x2700,%sr\n movea.l #0xfffff900,%sp\n'+body+'\n move.l #0x43485247,0xffff3ff0\nend: bra.s end');assert len(code)<0x4000 and b[0x2d5000:0x2d5000+len(code)]==b'\xff'*len(code);b[0x2d5000:0x2d5000+len(code)]=code;struct.pack_into('>I',b,4,0x2d5000)
 for at,v in patches:b[at:at+len(v)]=v
 (p/'rom.bin').write_bytes(b);(p/'commands.txt').write_text(f'1500 dump ff3ff0 4 done\n1500 dump ff4000 {size} result\n1501 exit\n')
 with(p/'log').open('w') as f:subprocess.run(['python3',str(h/'test_core.py'),str(p/'rom.bin'),str(p/'commands.txt'),str(p)],env=env|({'GPGX_SRM':str(p/'battery.srm')} if (p/'battery.srm').exists() else {}),stdout=f,stderr=f,check=True,timeout=90)
 rows={t[1]:bytes.fromhex(' '.join(t[2:])) for l in(p/'commands.out').read_text().splitlines() if(t:=l.split())};assert rows['done']==b'CHRG',('native return',p);return rows['result']
def branch(b,at):assert b[at:at+2]==bytes.fromhex('6000');return at+2+int.from_bytes(b[at+2:at+4],'big',signed=True)
def setup(pad=0,team=0,keeper=False,on=1):
 idx=6*team+(5 if keeper else 4);actor=0xffffb04a+128*idx
 return [f' lea 0x{actor:x},%a3',f' move.w #{idx},0x52(%a3)',f' move.w #{idx},0xffffb7aa',f' move.w #{0 if keeper else 4},0x34(%a3)',f' move.b #{8+64*team},0x62(%a3)',' clr.b 0x63(%a3)',' clr.w 0xffffc2ea',' clr.w 0xffffc2ec',' clr.w 0xffffc2f2',' clr.w 0xffffc2fa',' clr.w 0xffffc2f6',' clr.w 0xffffd412',f' move.w #{0 if pad>=2 else -1},0xffffc318',f' moveq #{2*(pad%2)},%d4',f' move.w #{on},0xfffff008',' clr.w 0xffffef70',' move.w #8,%d0',' clr.w 0xfffff0fc',' clr.w 0xfffff0fe',' move.w #2,0xffffbedc',f' move.l %a3,0x{0xfffff110+8*pad:x}',f' clr.w 0x{0xfffff114+8*pad:x}',f' move.w #1,0x{0xfffff116+8*pad:x}']
def gate():
 b=a.rom.read_bytes();custom=branch(b,0x2e6808);assisted=branch(b,0x2e8218);stub=lambda n:bytes.fromhex('40e731fc')+n.to_bytes(2,'big')+bytes.fromhex('ef7046df4e75');patches=[(0xbc64,stub(1)),(custom,stub(2)),(assisted,stub(1))]
 specs=[dict(pad=p,mode=m,ticks=t,team=side,keeper=k) for p in range(4) for m in [0,1] for t in [0,1,10,30,80] for side,k in [(0,False),(1,False),(0,True),(1,True)]]
 def trial(z):
  p=o/f'gate-{z["pad"]}-{z["mode"]}-{z["ticks"]}-{z["team"]}-{z["keeper"]}';body=setup(z['pad'],z['team'],z['keeper'])+[f' move.w #{z["mode"]},0xfffff0fc',' moveq #16,%d3',' moveq #0,%d2']
  for i in range(z['ticks']):body+=[' moveq #2,%d0',' jsr 0xbc40']
  body+=[' move.w 0xffffef70,0xffff4000',' moveq #0,%d3',' moveq #16,%d2',' moveq #4,%d0',' move.w #0x271f,%sr',' jsr 0xbc40',' move.w %sr,0xffff4002',' move.w 0xffffef70,0xffff4004',f' move.w 0x{0xfffff114+8*z["pad"]:x},0xffff4006',f' move.w 0x{0xfffff116+8*z["pad"]:x},0xffff4008',' move.w 0xffffbedc,0xffff400a',' move.l %sp,0xffff400c']
  raw=run(p,'\n'.join(body),16,patches);before,sr,end,ticks,state,aim,sp=struct.unpack('>6HI',raw);assert before==0 and end==z['mode']+1 and ticks==min(z['ticks'],30) and state==2 and aim==4 and sp==0xfffff900,('hold/release contract',z,raw.hex());return z|dict(held_emits=before,release_endpoint=end,held_ticks=ticks,stack=sp)
 with ThreadPoolExecutor(max_workers=3) as pool:rows=list(pool.map(trial,specs))
 print('PASS',len(rows),'native human hold/release endpoints, both teams/P1-P4/manual keepers',flush=True);return dict(cases=rows)
def power():
 specs=[dict(pad=p,ticks=t,power=v,ccr=c) for p in range(4) for t in [0,1,9,15,29,30] for v in [176,190,220] for c in [0,31]]
 def trial(batch):
  ss=specs[batch*12:batch*12+12];body=[]
  for j,z in enumerate(ss):
   addr=0xffff4000+72*j;body+=setup(z['pad'])+[f' move.w #{z["ticks"]},0x{0xfffff114+8*z["pad"]:x}',f' move.w #2,0x{0xfffff116+8*z["pad"]:x}',f' move.w #{z["power"]},0xffffbede',' move.l #0x12345678,%d0',' move.l #0x23456789,%d1',f' move.w #{0x2700|z["ccr"]},%sr',' jsr 0x2e8208',f' move.w %sr,0x{addr:x}',f' movem.l %d0-%d7/%a0-%a6,0x{addr+2:x}',f' move.l %sp,0x{addr+62:x}',f' move.w 0xffffbede,0x{addr+66:x}',f' move.w 0x{0xfffff116+8*z["pad"]:x},0x{addr+68:x}']
  raw=run(o/f'power-{batch}','\n'.join(body),72*len(ss));out=[]
  for j,z in enumerate(ss):
   r=raw[72*j:72*j+72];sr=int.from_bytes(r[:2],'big');regs=struct.unpack('>15I',r[2:62]);sp,power,state=struct.unpack('>IHH',r[62:70]);expected=z['power']*(z['ticks']+10)//40;assert power==expected and state==0 and sr==0x2700|z['ccr'] and regs[:2]==(0x12345678,0x23456789) and sp==0xfffff900,('scaled power/ABI',z,r.hex(),expected);out.append(z|dict(actual_power=power))
  return out
 with ThreadPoolExecutor(max_workers=3) as pool:rows=sum(pool.map(trial,range((len(specs)+11)//12)),[])
 print('PASS',len(rows),'power/CCR/stack cases',flush=True);return dict(cases=rows)

def pack(b):
 for off in range(0,len(b),8192):b[off+0x1fff]=sum(b[off:off+0x1ffe])&255;b[off+0x1ffe]=b[off+0x1fff]^255
 raw=bytearray(2*len(b));raw[1::2]=b;return raw

def encode(height,speed,mode,cone):
 value=(height+2*speed+4*mode+8*cone)|(0x80 if mode or cone else 0x50);return bytes([value,value^255])

def battery(gp=0,choices=((1,0,1,1),(0,1,0,2),(1,1,1,3)),native=(0,1,1,2),bad=None):
 base=bytearray(a.seed.read_bytes()[1:16384:2]);base[0xe20:0x1ffe]=bytes(0x11de);banks=[]
 for slot,length in [(1,84),(2,31),(3,7)]:
  b=bytearray(base);struct.pack_into('>4s6H',b,0x1000,b'SESN',gp,11 if gp else 0,gp,length,0 if gp else slot,3);struct.pack_into('>4s4H',b,0x1ea0,b'S3SV',1,slot,7,1);b[0x1ef0:0x1ef6]=b'PC\x01\xfe\x01\xfe';b[0x1ff6:0x1ffe]=bytes([0x55,0,0,2,2,0,0x5a,0]);b[0x1eee:0x1ef0]=encode(*choices[slot-1]);banks.append(b)
 b=bytearray(banks[0])+b''.join(banks);b[0x2e18:0x2e20]=b'RP\x01\xfeCT\x01\xfe';b[0x4d20:0x4d22]=encode(*native)
 if bad is not None:
  for bank in [0,0x2000]:b[bank+0x1eee:bank+0x1ef0]=bytes.fromhex(bad)
 return pack(b)

def game_core(p,rom,commands,settings):
 (p/'commands.txt').write_text('\n'.join(commands)+'\n')
 with(p/'log').open('w') as log:subprocess.run(['nice','-n','19','python3',str(h/'test_core.py'),str(rom.resolve()),str(p/'commands.txt'),str(p)],env=env|settings,stdout=log,stderr=log,check=True,timeout=100)

def input_suite():
 p=o/'input-probes';p.mkdir(exist_ok=True);b=bytearray(a.rom.read_bytes());custom=branch(b,0x2e6808);assisted=branch(b,0x2e8218);clone=b.find(bytes.fromhex('48e7fcc008b80002c2ec'),assisted,0x2e8800);assert clone>=0
 c=asm(p,f' .equ CUSTOM_CONT,0x{custom+10:x}\n .equ CHARGE_CONT,0x{clone+10:x}\n'+(h/'testcases/pass_charge_input_probe.s').read_text(),0x2d9000);from pass_input_observer_checks import verify as observer_abi
 observer_proof=observer_abi(a.rom,c,h,p/'observer-ABI',profile='charge');q=p/'banks';q.mkdir(exist_ok=True);bank=asm(q,(h/'testcases/manual_keeper_banks.s').read_text(),0x2ef400)
 for at,code in [(0x2d9000,c),(0x2ef400,bank)]:assert set(b[at:at+len(code)])=={255};b[at:at+len(code)]=code
 for at,target in [(0xbc64,0x2d9300),(custom,0x2d9350),(clone,0x2d9380)]:
  assert b[at:at+10]==bytes.fromhex('48e7fcc008b80002c2ec');b[at:at+10]=bytes.fromhex('4ef9')+struct.pack('>I',target)+bytes.fromhex('4e714e71')
 assert b[0xbdd8:0xbde0]==bytes.fromhex('3038b7723238b774');b[0xbdd8:0xbde0]=bytes.fromhex('4ef9002d94004e71')
 for at,target,expected in [(0xad66,0x2ef400,'3f38c32831f8c32cc328'),(0xadce,0x2ef440,'31f8c320c32431dfc320'),(0xae00,0x2ef480,'3f38c32a31f8c32ec32a'),(0xae68,0x2ef4c0,'31f8c322c32631dfc322')]:
  assert b[at:at+10]==bytes.fromhex(expected);b[at:at+10]=bytes.fromhex('4ef9')+struct.pack('>I',target)+bytes.fromhex('4e714e71')
 observed=p/'rom.bin';observed.write_bytes(b);cases=[dict(count=1,adapter=False,mode=0,cone=0,height=0,speed=0,end=25000),dict(count=1,adapter=False,mode=1,cone=0,height=0,speed=0,end=25000),dict(count=2,adapter=False,mode=1,cone=0,height=1,speed=1,end=25000),dict(count=3,adapter=True,mode=1,cone=0,height=1,speed=0,end=25000),dict(count=4,adapter=True,mode=1,cone=0,height=0,speed=1,end=25000),dict(count=4,adapter=True,mode=0,cone=0,height=1,speed=1,end=25000)]
 def trial(item):
  i,z=item;p=o/f'input-{i}';p.mkdir(exist_ok=True);raw=battery(0,((z['height'],z['speed'],z['mode'],z['cone']),)*3);b=bytearray(raw[1::2])
  for off in [0,0x2000,0x4000,0x6000]:b[off+0x1ef2:off+0x1ef4]=bytes([z['count'],z['count']^255])
  for off in [0x4d22,0x4d24,0x4d26,0x4d28]:b[off:off+2]=bytes.fromhex('619e')
  (p/'battery.srm').write_bytes(pack(b));(p/'spec.json').write_text(json.dumps(z));game_core(p,observed,['1500 press 1 Start 5','1900 press 1 Start 5','9000 snap',f'{z["end"]+1} exit'],dict(GPGX_DEV='1281' if z['adapter'] else '513',GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(h/'testcases/pass_charge_input_hook.py')));return json.loads((p/'results.json').read_text())
 with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(trial,enumerate(cases)))
 for d in results:
  assert set(d['charged_pads'])==set(range(d['spec']['count'])),('configured pads not all charged',d['spec'],d['charged_pads'])
  assert 30 in d['charge_ticks'] and any(0<=n<10 for n in d['charge_ticks']),('both tap and fully held passes required',d['spec'],d['charge_ticks'])
  assert d['keeper_requests']>0,('native immediate requested keeper outlet coverage',d['spec'])
 events=[e for d in results for e in d['events'] if e['charge_ticks']>=0 and e['mode']==1]
 assert {e['aim'] for e in events}==set(range(8)),('charged input must cover all eight directional aims',{e['aim'] for e in events})
 print('PASS',len(results),'cold-boot real input games, short/full hold and actual P1-P4 releases',flush=True);return dict(cases=results,observer_ABI=observer_proof)

def storage():
 specs=[dict(slot=slot,tag=t,inv=t^255) for slot in range(4) for t in range(256)]+[dict(slot=slot,tag=t,inv=t) for slot in range(4) for t in [0,0x60,0x61,255]]
 def trial(batch):
  ss=specs[batch*16:batch*16+16];body=[' move.b #1,0xa130f1']
  for j,z in enumerate(ss):
   rec=0x209a45+4*z['slot'];dest=0xffff4000+j*8
   body += [f' move.w #{int(z["slot"]>0)},0xfffff06c',f' move.w #{z["slot"]},0xfffff06a',f' move.b #{z["tag"]},0x{rec:x}',f' move.b #{z["inv"]},0x{rec+2:x}',' move.w #99,0xfffff008',' move.w #0x271f,%sr',' jsr 0x2e820c',f' move.w %sr,0x{dest:x}',f' move.w 0xfffff008,0x{dest+2:x}',f' move.l %sp,0x{dest+4:x}']
  raw=run(o/f'storage-{batch}','\n'.join(body),8*len(ss));out=[]
  for j,z in enumerate(ss):
   sr,value,sp=struct.unpack_from('>HHI',raw,8*j);want=z['tag']&1 if z['tag'] in [0x60,0x61] and z['tag']^z['inv']==255 else 0;assert value==want and sr==0x271f and sp==0xfffff900,('load/invalid fallback/ABI',z,value,sr,sp);out.append(z|dict(value=value))
  return out
 with ThreadPoolExecutor(max_workers=3) as pool:rows=sum(pool.map(trial,range((len(specs)+15)//16)),[])
 # Full byte-level storage separation: distinct sentinels across all four banks;
 # invoke actual put for each context, require only its own record/checksum move.
 checks=[]
 for slot in range(4):
  q=o/f'storage-put-{slot}';q.mkdir(exist_ok=True);raw=battery(1);b=bytearray(raw[1::2]);b[0x4d22:0x4d2a]=bytes.fromhex('609f609f609f609f');b=bytearray(pack(b)[1::2]);q.joinpath('before.srm').write_bytes(pack(b));physical=pack(b)
  body=[' move.b #1,0xa130f1',f' move.w #{int(slot>0)},0xfffff06c',f' move.w #{slot},0xfffff06a',' move.w #1,0xfffff008',' jsr 0x2e8210',' move.b #0,0xa130f1',' move.w 0xfffff008,0xffff4000']
  # run() writes its explicit battery fixture and reads back all64KB SRAM.
  q.joinpath('battery.srm').write_bytes(physical);run(q,'\n'.join(body),2);after=q.joinpath('battery.srm').read_bytes()[1::2];allowed={0x4d22+2*slot,0x4d23+2*slot,0x5ffe,0x5fff};diff=[i for i,(x,y) in enumerate(zip(b,after)) if x!=y];assert set(diff)<=allowed and after[0x4d22+2*slot:0x4d24+2*slot]==bytes.fromhex('619e'),('put overwrites another record/goalie/profile',slot,diff);assert after[0x5fff]==sum(after[0x4000:0x5ffe])%256 and after[0x5ffe]==after[0x5fff]^255,('Light checksum',slot);checks.append(dict(slot=slot,changed=diff))
 print('PASS',len(rows),'record validation/ABI contexts and four complete64KB separated writes',flush=True);return dict(loads=rows,writes=checks)

def menu_suite():
 p=o/'renderer';p.mkdir(exist_ok=True);c=asm(p,render_probe(""" cmpi.w #36,%d7
 bne.s original
 move.w %sr,-(%sp)
 addq.w #1,0xffffef90
 move.w 0xffffb028,0xffffef92
 move.l %a1,0xffffef94
 move.w (%sp)+,%sr
original: jmp 0x11ba4
""",a.rom,[36]),0x2fea00);b=bytearray(a.rom.read_bytes());assert set(b[0x2fea00:0x2fea00+len(c)])=={255} and b[0xf7bf4:0xf7bfa]==bytes.fromhex('4ef900011ba4');b[0x2fea00:0x2fea00+len(c)]=c;b[0xf7bf4:0xf7bfa]=bytes.fromhex('4ef9002fea00');observed=p/'rom.bin';observed.write_bytes(b);rows=[]
 def initial(gp=0,charges=(0,1,0,1),bad=None):
  b=bytearray(battery(gp)[1::2]);b[0xe20:0xe25]=b[0x2e20:0x2e25]=bytes.fromhex('123456789a')
  for slot,value in enumerate(charges):b[0x4d22+slot*2:0x4d24+slot*2]=bytes([0x60+value,0x9f-value])
  if bad:b[0x4d24:0x4d26]=bytes.fromhex(bad)
  return pack(b)
 def menu(name,raw,commands,checks,dev=513,render=False,finish=False):
  requested_menu_expectations(a.rom,name,checks)
  p=o/name;p.mkdir(exist_ok=True);(p/'battery.srm').write_bytes(raw);(p/'spec.json').write_text(json.dumps(dict(name=name,checks=checks,render=render,finish=finish)));game_core(p,observed,commands,dict(GPGX_DEV=str(dev),GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(h/'testcases/pass_charge_menu_hook.py')));rows.append(dict(name=name,observations=json.loads((p/'results.json').read_text())));return(p/'battery.srm').read_bytes()
 highest=int.from_bytes(a.rom.read_bytes()[0xf777a:0xf777c],'big')-2;up=(highest-row_word(a.rom,36))//2;assert up>=0
 down=[f'{900+50*i} press 1 Down 5' for i in range(down_count(a.rom,18))]
 for dev in [513,1281]:
  menu('fresh-'+str(dev),bytes(65536),down+['2250 snap','2401 exit'],{2200:dict(charge=0)},dev,True)
  raw=menu('choices-'+str(dev),initial(0,(0,0,0,0)),down+['2300 press 1 Right 5','2450 snap','2500 press 1 Left 5','2700 press 1 Right 5','2901 exit'],{2200:dict(charge=0,full_goalie='123456789a'),2400:dict(charge=1,charge_records='609f619e609f609f',record='8d72'),2600:dict(charge=0,charge_records='609f609f609f609f'),2800:dict(charge=1,record='8d72',full_goalie='123456789a')},dev,True)
  menu('reboot-'+str(dev),raw,['1301 exit'],{1200:dict(charge=1,charge_records='609f619e609f609f')},dev)
  nav=['1100 press 1 Right 5','1300 press 1 Right 5','1500 press 1 Right 5','1700 press 1 Left 5','1900 press 1 Left 5','2100 press 1 Left 5','2301 exit']
  raw=menu('slots-'+str(dev),initial(),nav,{1000:dict(charge=1,slot=1),1200:dict(charge=0,slot=2),1400:dict(charge=1,slot=3),1600:dict(charge=0,view=0),1800:dict(charge=1,slot=3),2000:dict(charge=0,slot=2),2200:dict(charge=1,slot=1)},dev)
  menu('slots-reboot-'+str(dev),raw,['1301 exit'],{1200:dict(charge=1,slot=1)},dev)
  menu('locked-'+str(dev),initial(1),down+['2300 press 1 Left 5','2500 press 1 Right 5','2701 exit'],{2200:dict(charge=1,gp=1),2400:dict(charge=1,gp=1),2600:dict(charge=1,gp=1)},dev,True)
  for bad in ['0000','619f','00ff','ffff']:
   menu('corrupt-'+bad+'-'+str(dev),initial(1,bad=bad),['1100 press 1 Right 5','1300 press 1 Left 5','1501 exit'],{1000:dict(charge=0,gp=1),1400:dict(charge=0,gp=1)},dev)
  native=[f'{1000+200*i} press 1 Right 5' for i in range(3)]+[f'{1800+35*i} press 1 Down 5' for i in range(down_count(a.rom,17))]
  raw=menu('native-'+str(dev),initial(1),native+['2900 press 1 Right 5','3101 exit'],{2850:dict(charge=0,view=0),3000:dict(charge=1,view=0,charge_records='619e619e609f619e',full_goalie='123456789a')},dev,True)
  menu('native-reboot-'+str(dev),raw,[f'{1000+200*i} press 1 Right 5' for i in range(3)]+['1701 exit'],{1600:dict(charge=1,view=0,full_goalie='123456789a')},dev)
  lines=['1100 press 1 Right 5','1300 press 1 Start 5']+[f'{1700+i*100} press 1 Down 5' for i in range(8)]+['2600 press 1 C 5','2900 press 1 Start 5','3200 press 1 C 5','3500 press 1 A 5','3500 press 1 C 5','4100 press 1 Left 5','4500 press 1 Right 5','4700 press 1 Right 5','5101 exit']
  menu('restart-'+str(dev),initial(1,(0,1,1,1)),lines,{3100:dict(charge=1,slot=2,gp=1),3900:dict(charge=0,slot=2,gp=0),4300:dict(charge=1,slot=1),4600:dict(charge=0,slot=2),4900:dict(charge=1,slot=3)},dev)
  raw=menu('first-game-'+str(dev),initial(),['1500 press 1 Start 5','1900 press 1 Start 5','16001 exit'],{1200:dict(charge=1),3300:dict(charge=1)},dev,False,dict(charge=1,gp=1))
  menu('completed-lock-'+str(dev),raw,down+['2300 press 1 Left 5','2601 exit'],{2200:dict(charge=1,gp=1),2500:dict(charge=1,gp=1)},dev,True)
 print('PASS',len(rows),'native menu/render/boot/slot/lock/restart/first-match cases',flush=True);return dict(cases=rows)

def physics():
 specs=[dict(team=team,keeper=keeper,mode=mode,ticks=t,pad=pad,height=height,speed=speed,receiver=rec) for team,keeper in [(0,False),(1,False),(0,True),(1,True)] for mode in [0,1] for t in [0,15,30] for pad in [0,3] for height,speed in [(0,0),(1,1)] for rec in [False,True]]
 old=a.baseline.read_bytes();oldcustom=branch(old,0x2e6808);oldpower=old.find(bytes.fromhex('31fcffffbee0'),oldcustom,0x2e7000);assert oldpower>=0
 def trial(item):
  batch,profile=item;ss=specs[batch*6:batch*6+6];p=o/f'physics-{profile}-{batch}';p.mkdir(exist_ok=True);body=[];patches=[(0xbdd8,bytes.fromhex('4ef90000be20'))]
  for j,z in enumerate(ss):
   idx=z['team']*6+(5 if z['keeper'] else 4);ob=0xffffb04a+128*idx;receiver=0xffffb04a+128*(6*z['team']);dest=0xffff4000+100*j;aim=0;nativepower=176 if z['keeper'] else 200;expected=nativepower*(z['ticks']+10)//40
   body+=[' lea 0xffffb04a,%a0',' move.w #383,%d0',f'clear{j}: clr.l (%a0)+',f' dbra %d0,clear{j}']
   for i in range(12):body += [f' move.w #-1,0x{0xffffb04a+128*i+0x34:x}',f' move.w #{i},0x{0xffffb04a+128*i+0x52:x}',f' move.b #{64 if i>=6 else 0},0x{0xffffb04a+128*i+0x62:x}']
   body+=setup(z['pad'],z['team'],z['keeper'])+[f' move.w #{z["ticks"]},0x{0xfffff114+8*z["pad"]:x}',f' move.w #2,0x{0xfffff116+8*z["pad"]:x}',' move.b #20,0x6e(%a3)',' clr.w (%a3)',' move.w #-100,0x14(%a3)',' move.w #800,0x28(%a3)',' move.w #-600,0x2a(%a3)',' clr.w 0xffffb74a',' move.w #-100,0xffffb75e',' move.w #0,0xffffbedc',' move.w #-1,0xffffbee0',' clr.l 0xfffff058',' clr.l 0xfffff05c',' clr.w 0xfffff736',' clr.w 0xffffd046',' clr.l 0xffffc320',' clr.l 0xffffc324',' clr.l 0xffffc328',' clr.l 0xffffc32c',f' move.w #{idx},0x{0xffffc320+2*z["pad"]:x}',f' move.w #{z["team"]+1},0x{0xffffc328+2*z["pad"]:x}',f' move.w #{z["mode"]},0xfffff0fc',f' move.w #{z["height"]},0xfffff0dc',f' move.w #{z["speed"]},0xfffff0e8',' move.l #0x12345678,0xffffd066']
   if z['receiver']:body += [f' move.w #4,0x{receiver+0x34:x}',f' move.w #30,0x{receiver:x}',f' move.w #100,0x{receiver+0x14:x}',f' move.w #200,0x{receiver+0x28:x}',f' move.w #100,0x{receiver+0x2a:x}']
   body += [f' move.w #{expected},0xffffef7c',' moveq #8,%d0',' moveq #0,%d1',' moveq #16,%d2',' moveq #0,%d3',f' moveq #{2*(z["pad"]%2)},%d4',' moveq #0,%d5',f' jsr 0x{0x2e6808 if z["mode"] else 0x2e8218 if profile=="current" else 0xbc64:x}']
   for k,addr in enumerate([0xb772,0xb774,0xb776,0xbee0,0xbeda,0xb7aa,0xc320,0xc322,0xc324,0xc326,0xbede,0xb7a8]):body += [f' move.w 0x{0xffff0000+addr:x},0x{dest+2*k:x}']
   body += [f' move.l 0xffffd066,0x{dest+24:x}',f' move.w %sr,0x{dest+28:x}',f' movem.l %d0-%d7/%a0-%a6,0x{dest+30:x}',f' move.l %sp,0x{dest+90:x}']
  if profile=='parent':
   ctrl=asm(p,' move.w 0xffffef7c,0xffffbede\n jmp 0xbcd8',0x2d9000);ctrl2=asm(p,f' move.w 0xffffef7c,0xffffbede\n jmp 0x{oldpower:x}',0x2d9040);patches += [(0x2d9000,ctrl),(0x2d9040,ctrl2),(0xbcc4,bytes.fromhex('4ef9002d9000')),(oldpower,bytes.fromhex('4ef9002d9040'))]
   # The reference stub replaces the first MOVE at power_ready; resume after it.
   ctrl2=asm(p,f' move.w 0xffffef7c,0xffffbede\n move.w #-1,0xffffbee0\n jmp 0x{oldpower+6:x}',0x2d9040);patches[-3]=(0x2d9040,ctrl2)
  raw=run(p,'\n'.join(body),100*len(ss),patches,profile);return batch,profile,[raw[100*j:100*j+94] for j in range(len(ss))]
 with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(trial,[(i,p) for i in range((len(specs)+5)//6) for p in ['current','parent']]))
 data={(i,p):v for i,p,v in results};rows=[]
 for i,z in enumerate(specs):
  b,j=divmod(i,6);new,old=data[b,'current'][j],data[b,'parent'][j];assert new==old,('charged actual solver/vector versus native lower-power input/complete ABI',z,new.hex(),old.hex());assert int.from_bytes(new[90:94],'big')==0xfffff900;rows.append(z|dict(velocity=list(struct.unpack_from('>3h',new)),target=int.from_bytes(new[6:8],'big',signed=True),power=int.from_bytes(new[20:22],'big')))
 print('PASS',len(rows),'complete charged physics/receiver/CCR/register/stack comparisons with native lower-power input',flush=True);return dict(cases=rows)

def guards():
 specs=[dict(pad=pad,mode=mode,kind=k,d0=d0,release=r) for pad in range(4) for mode in [0,1] for k in ['Off','invalid-option','CPU','inactive','shootout','penalty-shot','whistle','forced','paused','replay','fighting','lost-puck','other-actor','no-begin'] for d0,r in [(8,0),(2,0),(8,16)]]
 current=a.rom.read_bytes();parent=a.baseline.read_bytes();stub=lambda n:bytes.fromhex('40e731fc')+n.to_bytes(2,'big')+bytes.fromhex('ef7046df4e75')
 def trial(item):
  batch,profile=item;ss=specs[batch*12:batch*12+12];b=current if profile=='current' else parent;custom=branch(b,0x2e6808);patches=[(0xbc64,stub(1)),(custom,stub(2))]
  if profile=='current':patches += [(branch(b,0x2e8218),stub(1))]
  body=[]
  for j,z in enumerate(ss):
   dest=0xffff4000+72*j;body+=setup(z['pad'],on=0 if z['kind']=='Off' else 99 if z['kind']=='invalid-option' else 1)+[f' move.w #{z["mode"]},0xfffff0fc']
   modifications={'CPU':[' bclr #3,0x62(%a3)'],'inactive':[' move.w #-1,0x34(%a3)'],'shootout':[' bset #0,0xffffc2fa'],'penalty-shot':[' move.w #1,0xffffd412'],'whistle':[' bset #2,0xffffc2f2'],'forced':[' bset #3,0xffffc2f6'],'paused':[' bset #0,0xffffc2ec'],'replay':[' bset #4,0xffffc2ea'],'fighting':[' bset #0,0x63(%a3)'],'lost-puck':[' move.w #-1,0xffffb7aa'],'other-actor':[f' move.l #0xffffb04a,0x{0xfffff110+8*z["pad"]:x}'],'no-begin':[f' clr.w 0x{0xfffff116+8*z["pad"]:x}']};body+=modifications.get(z['kind'],[])
   body += [f' move.w #{z["d0"]},%d0',' moveq #0,%d1',f' moveq #{z["release"]},%d2',' moveq #16,%d3',' move.w #0x271f,%sr',' jsr 0xbc40',f' move.w %sr,0x{dest:x}',f' movem.l %d0-%d7/%a0-%a6,0x{dest+2:x}',f' move.l %sp,0x{dest+62:x}',f' move.w 0xffffef70,0x{dest+66:x}',f' move.w 0xffffbedc,0x{dest+68:x}',f' move.w 0xffffbee0,0x{dest+70:x}']
  raw=run(o/f'guards-{profile}-{batch}','\n'.join(body),72*len(ss),patches,profile);return batch,profile,[raw[72*j:72*j+72] for j in range(len(ss))]
 with ThreadPoolExecutor(max_workers=3) as pool:results=list(pool.map(trial,[(i,p) for i in range((len(specs)+11)//12) for p in ['current','parent']]))
 data={(i,p):v for i,p,v in results}
 for i,z in enumerate(specs):
  b,j=divmod(i,12);new,old=data[b,'current'][j],data[b,'parent'][j];assert new==old,('charge bypass changed native complete endpoint/ABI/aim/target',z,new.hex(),old.hex())
 print('PASS',len(specs),'paired native Off/special/stale/ownership/CPU gate ABI contexts',flush=True);return dict(contexts=len(specs),cases=specs)

sections={}
if a.section in ['all','gate']:sections['gate']=gate()
if a.section in ['all','power']:sections['power']=power()
if a.section in ['all','input']:sections['input']=input_suite()
if a.section in ['all','storage']:sections['storage']=storage()
if a.section in ['all','menu']:sections['menu']=menu_suite()
if a.section in ['all','physics']:sections['physics']=physics()
if a.section in ['all','guards']:sections['guards']=guards()
(o/'results.json').write_text(json.dumps(dict(rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),sections=sections),indent=2)+'\n')
