#!/usr/bin/env python3
"""Actual CPU buffer entry must run native possession accounting before release.
Only the shot/animation endpoints are replaced with observers; 1013E and its
assist-chain/zone-state code execute unchanged. No native statistics are patched.
"""
from pathlib import Path
import argparse,json,os,struct,subprocess
ap=argparse.ArgumentParser(description=__doc__);ap.add_argument('rom',type=Path);ap.add_argument('output',type=Path);a=ap.parse_args();h=Path(__file__).resolve().parent;o=a.output.resolve();o.mkdir(parents=True,exist_ok=True)
cases=[dict(team=t,direction=d,previous=p) for t in (0,1) for d in (-1,1) for p in (7,15)]
cases += [dict(team=t,direction=d,previous=7,guard=g) for t in (0,1) for d in (-1,1) for g in ('normal','easy','human','unavailable','whistle','expired','missed','inflight')]
rows=[]
for n,s in enumerate(cases):
 dest=o/str(n);dest.mkdir(exist_ok=True);t=s['team'];d=s['direction'];slot=t*6+2;actor=0xffb04a+slot*128;base=0xffc6ce+t*0x364;other=0xffc6ce+(1-t)*0x364;g=s.get('guard');eligible=g is None
 body=[' .text',' .globl _start','_start:',' move.w #0x2700,%sr',' movea.l #0xfff800,%sp',' lea 0xffb04a,%a0',' move.w #1727,%d0','clear: clr.l (%a0)+',' dbra %d0,clear']
 for i in range(12):body += [f' move.w #-1,0x{0xffb04a+128*i+0x34:x}']
 body += [f' lea 0x{actor:x},%a3',' move.w #60,(%a3)',f' move.w #{210*d},0x14(%a3)',' move.w #4,0x34(%a3)',f' move.w #{slot},0x52(%a3)',f' move.b #{(128 if d==1 else 0)+(64 if t else 0)+(8 if g=="human" else 0)},0x62(%a3)',f' move.b #{4 if g=="unavailable" else 0},0x63(%a3)',' move.b #15,0x66(%a3)',f' move.w #{0 if g=="normal" else 2 if g=="easy" else 1},0xfff07a',f' move.b #{1 if g=="whistle" else 0},0xffc2ea',' clr.w 0xffc2ec',' clr.w 0xffc2ee',' clr.w 0xffc2f2',' clr.w 0xffc2fa',' clr.w 0xffbf80',f' move.w #{-1 if g=="inflight" else (1-t)*6 if g=="missed" else slot},0xffb7aa',f' move.w #{slot},0xffbee0',' move.w #0x5042,0xfff086',f' move.w #{slot+1},0xfff088',f' move.w #{0 if g=="expired" else 64},0xfff08a',f' move.w #{s["previous"]},0x{base+0x18:x}',f' move.w #9,0x{base+0x1a:x}',f' move.w #11,0x{base+0x1c:x}',f' move.w #21,0x{other+0x18:x}',' movea.l #0xff1234,%a2',' move.l #0x12345678,%d3',' jsr 0x2dd010',' move.l %a2,0xff4004',' move.l %d3,0xff4008',' move.l #0x41545452,0xff3ff0','end: bra.s end','shot:',' move.w #1,0xff4000',f' move.w 0x{base+0x18:x},0xff4002',' rts']
 (dest/'driver.s').write_text('\n'.join(body)+'\n');subprocess.run([str(h.parent/'m68kasm.sh'),str(dest/'driver.s'),'2d9000',str(dest/'driver-code.bin')],check=True,stdout=subprocess.DEVNULL)
 b=bytearray(a.rom.read_bytes());code=(dest/'driver-code.bin').read_bytes();assert b[0x2d9000:0x2d9000+len(code)]==b'\xff'*len(code);b[0x2d9000:0x2d9000+len(code)]=code;struct.pack_into('>I',b,4,0x2d9000)
 # Last eight-byte shot observer: MOVE.W abs.W destination, then RTS.
 shot=0x2d9000+code.index(bytes.fromhex("33fc000100ff4000"))
 assert b[shot:shot+2]==bytes.fromhex('33fc'),hex(shot)
 b[0xc2f2:0xc2f8]=bytes.fromhex('4ef9')+struct.pack('>I',shot);b[0x1073a:0x1073c]=bytes.fromhex('4e75')
 (dest/'driver.bin').write_bytes(b);(dest/'commands.txt').write_text(f'50 dump ff3ff0 4 done\n50 dump ff4000 12 observed\n50 dump {base+0x18:x} 6 chain\n50 dump {other+0x18:x} 2 opponent\n50 dump {base+0xb4:x} 78 counters\n51 exit\n');env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env['GPGX_AUTO_CREDITS']='0'
 with (dest/'log.txt').open('w') as log:subprocess.run(['python3',str(h/'test_core.py'),str(dest/'driver.bin'),str(dest/'commands.txt'),str(dest)],env=env,stdout=log,stderr=log,check=True,timeout=30)
 out={l.split()[1]:bytes.fromhex(' '.join(l.split()[2:])) for l in (dest/'commands.out').read_text().splitlines()};assert out['done']==b'ATTR',(s,out)
 fired,scorer,reg,reg3=struct.unpack('>HHII',out['observed']);assert fired==int(eligible),(s,fired)
 assert reg==0xff1234 and reg3==0x12345678,(s,reg,reg3)
 chain=list(struct.unpack('>3H',out['chain']));expected=[15,7,9] if eligible and s['previous']==7 else [15,9,11] if eligible else [s['previous'],9,11]
 assert chain==expected,(s,chain,expected)
 assert not eligible or scorer==15,(s,scorer)
 assert out['opponent']==bytes.fromhex('0015') and out['counters']==bytes(78),(s,'unrelated team/counter mutation')
 rows.append(dict(case=s,fired=fired,scorer_at_actual_release=scorer,native_assist_chain=chain))
(o/'results.json').write_text(json.dumps(rows,indent=2)+'\n');print('PASS',len(rows),'native CPU buffered scorer/assist attribution and guarded no-counter-write cases')
