#!/usr/bin/env python3
"""Native human buffered shot contact, animation phase and pending-C regression."""
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
import argparse,json,hashlib,os,struct,subprocess
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,required=True);ap.add_argument('--contact-race-reference',type=Path);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True);sha=lambda p:hashlib.sha256(p.read_bytes()).hexdigest()
assert sha(a.baseline)=='29672052309b0bcad831716cd0edf35b498a265f0063d99173c70dff4df47c3a','exact r9 contact regression control required'

def run_core(rom,p,commands,env=None):
 p.mkdir(exist_ok=True,parents=True);(p/'commands.txt').write_text(commands);settings={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};settings.update(GPGX_DEV='513');settings.update(env or {})
 with (p/'log.txt').open('w') as f:subprocess.run(['nice','-n','19','python3',str(h/'test_core.py'),str(rom),str(p/'commands.txt'),str(p)],env=settings,stdout=f,stderr=f,check=True,timeout=180)
# A real native frame entry exercises GetHot, flip handling, accepted boundary,
# retained pending shots and contact-phase animation across all actor slots.
p=o/'abi';p.mkdir(exist_ok=True);source=''' .text
 .globl _start
_start:
 move.w #0x2700,%sr
 movea.l #0xFFF800,%sp
 clr.w 0xFF3FE0
 clr.b 0xFFFFC2EC
 lea 0xFF4000,%a4
 moveq #0,%d4
actor:
 move.w %d4,%d0
 lsl.w #7,%d0
 lea 0xFFFFB04A,%a3
 adda.w %d0,%a3
 move.w %d4,0x52(%a3)
 move.b #8,0x62(%a3)
 move.w #80,(%a3)
 move.w #100,0x14(%a3)
 move.w #11,6(%a3)
 move.w %d4,0xFFFFB7AA
 move.w %d4,0xFFFFBEE0
 moveq #0,%d5
flip:
 move.w %d5,%d0
 lsl.w #3,%d0
 move.w %d0,4(%a3)
 moveq #0,%d6
vector:
 move.w #0x50C,0x58(%a3)
 clr.w 0x5A(%a3)
 clr.b 0x64(%a3)
 move.w %d4,%d0
 addq.w #1,%d0
 move.w %d0,0xFFFFF736
 move.l %a3,-(%sp)
 jsr 0x106E0
 add.w (%a3),%d0
 add.w 0x14(%a3),%d1
 lea offsets(%pc),%a0
 move.w %d6,%d2
 lsl.w #2,%d2
 add.w (%a0,%d2.w),%d0
 add.w 2(%a0,%d2.w),%d1
 move.w %d0,0xFFFFB74A
 move.w %d1,0xFFFFB75E
 move.w 0xFF3FE0,%d7
 jsr 0x2E1004
 move.w 0xFF3FE0,%d0
 sub.w %d7,%d0
 move.w %d0,(%a4)+
 move.w 0xFFFFF736,(%a4)+
 move.w 0x58(%a3),(%a4)+
 move.w 0x5A(%a3),(%a4)+
 addq.w #1,%d6
 cmpi.w #9,%d6
 bcs.w vector
 addq.w #1,%d5
 cmpi.w #4,%d5
 bcs.w flip
 addq.w #1,%d4
 cmpi.w #12,%d4
 bcs.w actor
 move.l #0x434F4E54,0xFF3FF0
end: bra.s end
offsets: .word 0,0,3,0,4,0,3,3,5,0,-4,0,0,-4,-16,0,30,30
'''
owned=[]
for pending,owner in [(3,2),(0,2),(4,2),(3,1)]:
 owned += [' lea 0xFFFFB14A,%a3',' move.w #2,0x52(%a3)',f' move.w #{pending},0xFFFFF736',f' move.w #{owner},0xFFFFB7AA',' clr.l 0xFF3FE2',' move.l #0xABCD1234,%d0',' move.w #0x20,%d1',' jsr 0x2E100C',' move.w 0xFF3FE2,(%a4)+',' move.w 0xFF3FE4,(%a4)+',' move.l %d0,(%a4)+']
input_cases=[]
for actor,target,owner in [(2,0,-1),(0,2,-1),(0,0,-1),(0,-1,0),(0,-1,6)]:
 input_cases += [' lea 0xFFFFB04A,%a3',f' move.w #{actor},0x52(%a3)',f' move.w #{target},0xFFFFBEE0',f' move.w #{owner},0xFFFFB7AA',' move.w #1,0xFFFFF736',' clr.l 0xFF3FE4',' move.l #0xABCD1234,%d0',' move.w #0x20,%d1',' jsr 0x2E1000',' move.w 0xFFFFF736,(%a4)+',' move.w 0xFF3FE6,(%a4)+',' move.w 0xFF3FE4,(%a4)+',' move.w %d0,(%a4)+']
source=source.replace(' move.l #0x434F4E54,0xFF3FF0','\n'.join(owned+input_cases)+'\n move.l #0x434F4E54,0xFF3FF0')
(p/'driver.s').write_text(source);subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),'2D9000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL)
b=bytearray(a.rom.read_bytes());c=(p/'code.bin').read_bytes();assert len(c)<0x1000 and b[0x2d9000:0x2d9000+len(c)]==bytes([255])*len(c);b[0x2d9000:0x2d9000+len(c)]=c;struct.pack_into('>I',b,4,0x2d9000)
for at in [0xfd78a,0x2dd010]:b[at:at+2]=bytes.fromhex('4e75')
b[0xb6d0:0xb6d6]=bytes.fromhex('4ef9002da060');b[0x2da060:0x2da068]=bytes.fromhex('527900ff3fe64e75');b[0xfee60:0xfee66]=bytes.fromhex('4ef9002da020');b[0x2da020:0x2da028]=bytes.fromhex('527900ff3fe24e75');b[0x10750:0x10756]=bytes.fromhex('4ef9002da040');b[0x2da040:0x2da048]=bytes.fromhex('527900ff3fe44e75');b[0xb816:0xb818]=bytes.fromhex('4e75');b[0xc2f2:0xc2f8]=bytes.fromhex('4ef9002da000');b[0x2da000:0x2da008]=bytes.fromhex('527900ff3fe04e75');(p/'driver.bin').write_bytes(b)
run_core(p/'driver.bin',p,'500 dump ff3ff0 4 done\n500 dump ff4000 3528 cases\n501 exit\n');r={v.split()[1]:bytes.fromhex(' '.join(v.split()[2:])) for v in (p/'commands.out').read_text().splitlines()};assert r['done']==b'CONT';vectors=[(0,0),(3,0),(4,0),(3,3),(5,0),(-4,0),(0,-4),(-16,0),(30,30)];rows=list(struct.iter_unpack('>4H',r['cases'][:3456]));assert len(rows)==432
for i,(shots,pending,anim,span) in enumerate(rows):
 actor=i//36;dx,dy=vectors[i%9];near=dx*dx+dy*dy<=16
 assert (shots,pending,anim,span)==((1,0,0x7fc,24) if near else (0,actor+1,0x50c,0)),(i,dx,dy,shots,pending,anim,span)
owned_results=list(struct.iter_unpack('>2HI',r['cases'][3456:3488]));assert owned_results==[(0,1,0xabcd1234),(1,0,0xabcd1234),(1,0,0xabcd1234),(1,0,0xabcd1234)],owned_results
input_results=list(struct.iter_unpack('>4H',r['cases'][3488:]));assert input_results==[(1,1,0,0x1234),(0,1,0,0x1234),(1,0,1,0x1234),(1,0,1,0x1234),(0,1,0,0x1234)],input_results
# Cold native co-op inputs reproduce and measure the bug. The only ROM probe
# logs at native doshot entry; it does not inject game coordinates or results.
probe=o/'shot-probe.bin';subprocess.run([str(h.parent/'m68kasm.sh'),str(h/'testcases/human_contact_probe.s'),'2D9000',str(probe)],check=True,stdout=subprocess.DEVNULL);code=probe.read_bytes()
legacy_probe=o/'original-probe.bin'
legacy_source=o/'original-probe.s';legacy_source.write_text((h/'testcases/human_contact_probe.s').read_text().replace(' move.w 16(%sp),0xFFFFEFA2           | SR replaces unused attr diagnostic at identical length/cycles',' move.w 4(%a3),0xFFFFEFA0'))
subprocess.run([str(h.parent/'m68kasm.sh'),str(legacy_source),'2D9000',str(legacy_probe)],check=True,stdout=subprocess.DEVNULL);original_code=legacy_probe.read_bytes()
# Historical bug controls retain their exact original observer order/bytes.
# Only the current paired observers publish after the complete payload; both
# have identical length/cycles and differ solely in the diagnostic SR slot.
historical_source=o/'historical-probe.s'
historical_text=legacy_source.read_text().replace(' addq.w #1,0xFFFFEF80\n','')
historical_text=historical_text.replace(' movem.l %d0-%d1/%a0-%a1,-(%sp)\n',' movem.l %d0-%d1/%a0-%a1,-(%sp)\n addq.w #1,0xFFFFEF80\n')
historical_source.write_text(historical_text)
historical_probe=o/'historical-probe.bin'
subprocess.run([str(h.parent/'m68kasm.sh'),str(historical_source),'2D9000',str(historical_probe)],check=True,stdout=subprocess.DEVNULL)
historical_code=historical_probe.read_bytes()
def native(spec):
 label,rom,style=spec;code=historical_code if label.startswith(('baseline','race-control')) else (original_code if label.startswith('current-original') else probe.read_bytes());p=o/label;p.mkdir(exist_ok=True);data=bytearray(rom.read_bytes());assert data[0xc2f2:0xc2fc]==bytes.fromhex('48e7fff008b80004c2fa');assert data[0x2d9000:0x2d9000+len(code)]==bytes([255])*len(code);data[0x2d9000:0x2d9000+len(code)]=code;data[0xc2f2:0xc2f8]=bytes.fromhex('4ef9002d9000');(p/'probe.bin').write_bytes(data)
 logical=bytearray(a.seed.read_bytes()[1:16384:2]);logical[0xe20:0x1ef6]=bytes(0x10d6);struct.pack_into('>4s6H',logical,0x1000,b'SESN',0,0,0,84,1,3);logical[0x1fff]=sum(logical[:0x1ffe])%256;logical[0x1ffe]=logical[0x1fff]^255;raw=bytearray(16384);raw[1::2]=logical;(p/'battery.srm').write_bytes(raw)
 commands='900 press 1 Down 5\n1000 press 1 Right 5\n1400 press 1 Up 5\n1500 press 1 Start 5\n1900 press 1 Start 5\n35001 snap\n35002 exit\n';run_core(p/'probe.bin',p,commands,dict(GPGX_SRM=str(p/'battery.srm'),GPGX_HOOK=str(h/'testcases/human_contact_hook.py'),CONTACT_STYLE=style))
 events=json.loads((p/'events.json').read_text());buffered=[e for e in events if e['human'] and e['one_timer'] and 0x2e1000<=e['caller']<0x2e1200];assert buffered,(label,'no actual buffered human shots')
 if label.startswith('race-control'):assert any(e['stick_distance']>16 and e['span']==24 for e in buffered),('pre-atomic native contact/race regression not reproduced',buffered)
 elif label.startswith('baseline'):assert any(e['stick_distance']>16 and e['span']==0 for e in buffered),('old bug not reproduced',buffered)
 else:
  assert all(e['owner']==e['actor'] and e['stick_distance']<=4.000001 and e['span']==24 for e in buffered),('released without stick contact/follow-through',buffered)
  if a.contact_race_reference and not label.startswith('current-original'):assert all(e['irq_level']==7 for e in buffered),('buffered native release was interruptible',buffered)
 return dict(case=label,style=style,frames=35001,owned_wait=json.loads((p/'contact-waits.json').read_text()),native_passes=len(json.loads((p/'passes.json').read_text())),buffered_shots=buffered,all_shot_attempts=len(events),max_buffered_stick_distance=max(e['stick_distance'] for e in buffered))
profiles=[('baseline-mash',a.baseline,'mash'),('current-mash',a.rom,'mash'),('current-held',a.rom,'held')]
if a.contact_race_reference:
 assert sha(a.contact_race_reference)=='4c5023ec712627941f576b7701e96ccf26f6b2e81aa095f9bd0fc11c371b5b59'
 profiles += [('race-control-mash',a.contact_race_reference,'mash'),('current-original-probe-mash',a.rom,'mash')]
with ThreadPoolExecutor(max_workers=3) as pool:reports=list(pool.map(native,profiles))
# Same-size/timing observer: the current mash and old-observer mash must produce
# exactly the same game inputs, passes and measured shot positions/callers.
if a.contact_race_reference:
 before=json.loads((o/'current-original-probe-mash/events.json').read_text());after=json.loads((o/'current-mash/events.json').read_text())
 def positional(events):return [{k:v for k,v in row.items() if k not in ['shot_sr','irq_level']} for row in events]
 assert positional(before)==positional(after),'SR measurement changed native shot events'
 for file in ['inputs.json','passes.json','contact-waits.json']:
  assert (o/'current-original-probe-mash'/file).read_bytes()==(o/'current-mash'/file).read_bytes(),('SR measurement changed game inputs/passes',file)
atomic_rows=[]
if a.contact_race_reference:
 cases=[dict(actor=actor,near=near,irq=irq) for actor in range(12) for near in [False,True] for irq in range(8)]
 def atomic_batch(item):
  profile,batch=item;rom=a.rom if profile=='current' else a.contact_race_reference;ss=cases[batch*8:batch*8+8];p=o/f'atomic-abi-{profile}-{batch:02}';p.mkdir(exist_ok=True);body=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xfffff900,%sp']
  for j,z in enumerate(ss):
   ob=0xffffb04a+128*z['actor'];at=0xffff4000+100*j;body += [f' lea 0x{ob:x},%a3',' move.w #80,(%a3)',' move.w #100,0x14(%a3)',' move.w #0,4(%a3)',' move.w #11,6(%a3)',' move.w #4,0x34(%a3)',f' move.w #{z["actor"]},0x52(%a3)',' move.w #0x50c,0x58(%a3)',' clr.w 0x5a(%a3)',' clr.w 0x5c(%a3)',' move.w #0x0800,0x62(%a3)',' clr.w 0x64(%a3)',f' move.w #{z["actor"]},0xffffb7aa',f' move.w #{z["actor"]},0xffffbee0',f' move.w #{z["actor"]+1},0xfffff736',' clr.w 0xff3fe0',' move.l %a3,-(%sp)',' jsr 0x106e0',' add.w (%a3),%d0',' add.w 0x14(%a3),%d1',f' addi.w #{0 if z["near"] else 20},%d0',' move.w %d0,0xffffb74a',' move.w %d1,0xffffb75e']
   for k in range(8):body += [f' move.l #0x{0x11223300+k:x},%d{k}']
   for k in range(7):body += [f' movea.l #0x{0xffff1800+k*256:x},%a{k}']
   body += [f' move.w #{0x201f+256*z["irq"]},%sr',' jsr 0x2e1004',f' move.w %sr,0x{at:x}',f' movem.l %d0-%d7/%a0-%a6,0x{at+2:x}',f' move.l %sp,0x{at+62:x}',f' move.w 0xfffff736,0x{at+66:x}',f' move.w 0xff3fe0,0x{at+68:x}']
  body += [' move.l #0x41544f4d,0xff3ff0','end: bra.s end'];(p/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(p/'driver.s'),'2d9000',str(p/'code.bin')],check=True,stdout=subprocess.DEVNULL);b=bytearray(rom.read_bytes());code=(p/'code.bin').read_bytes();assert len(code)<0x1000 and set(b[0x2d9000:0x2d9000+len(code)])=={255};b[0x2d9000:0x2d9000+len(code)]=code;struct.pack_into('>I',b,4,0x2d9000)
  for site in [0xfd78a,0x2dd010]:b[site:site+2]=bytes.fromhex('4e75')
  b[0xc2f2:0xc2f8]=bytes.fromhex('4ef9002da200');b[0x2da200:0x2da208]=bytes.fromhex('527900ff3fe04e75');(p/'rom.bin').write_bytes(b);run_core(p/'rom.bin',p,f'500 dump ff3ff0 4 done\n500 dump ff4000 {len(ss)*100} data\n501 exit\n');raw={v.split()[1]:bytes.fromhex(' '.join(v.split()[2:])) for v in (p/'commands.out').read_text().splitlines()};assert raw['done']==b'ATOM';return profile,batch,[raw['data'][j*100:(j+1)*100] for j in range(len(ss))]
 with ThreadPoolExecutor(max_workers=3) as pool:found=list(pool.map(atomic_batch,[(v,b) for v in ['reference','current'] for b in range((len(cases)+7)//8)]))
 lookup={(v,b):r for v,b,r in found}
 for i,z in enumerate(cases):
  batch,j=divmod(i,8);old=lookup['reference',batch][j];new=lookup['current',batch][j];assert new==old,('atomic window changed complete frame registers/SR/stack/pending contract',z,new.hex(),old.hex());sr=int.from_bytes(new[:2],'big');assert sr&0xff00==0x2000+256*z['irq'] and int.from_bytes(new[62:66],'big')==0xfffff900 and int.from_bytes(new[68:70],'big')==int(z['near']);atomic_rows.append(dict(spec=z,full_native_frame_ABI_exact=True,returned_sr=sr))
 print('PASS',len(cases),'paired atomic human frame full-register/CCR/IRQ-level/SP/near/wait contexts',flush=True)
(o/'results.json').write_text(json.dumps(dict(rom_sha256=sha(a.rom),baseline_sha256=sha(a.baseline),contact_race_reference_sha256=sha(a.contact_race_reference) if a.contact_race_reference else None,native_contact_boundaries=432,atomic_frame_ABI_contexts=atomic_rows,owned_C_guard_cases=owned_results,pending_input_ownership_cases=input_results,contact_radius_pixels=4,release_animation_phase=24,cold_boot_coop_profiles=reports,no_game_positions_owner_clock_rng_or_results_injected=True,private_shot_measurement_probe=True),indent=2)+'\n');print('PASS432 native actor/flip/contact boundaries; cold native co-op mash/hold shots; old distant phase0 release reproduced; current buffered releases within4px at native contact phase24')
