#!/usr/bin/env python3
"""Native 68000 regressions for forum fixes. Private reset drivers; no live saves.

Calls the installed instruction paths, with tiny return stubs after the tested
native slice. --baseline reports the old results without asserting the new ones.
Full gameplay/controller and shootout regressions remain separate suites.
"""
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);ap.add_argument('--baseline',action='store_true');a=ap.parse_args()
here=Path(__file__).resolve().parent;out=a.output.resolve();out.mkdir(parents=True,exist_ok=True);reports=[]

def run(name,body,size,stubs=()):
 p=out/name;p.mkdir(exist_ok=True)
 source='''    .text
    .globl _start
_start:
    move.w #0x2700,%sr
    movea.l #0xFFF800,%sp
    lea 0xFFF300,%a3
    lea 0xFF4000,%a4
'''+body+'''
    move.l #0x464F5255,0xFF3FF0
end: bra.s end
'''
 (p/'driver.s').write_text(source);code=p/'driver-code.bin'
 subprocess.run([str(here.parent/'m68kasm.sh'),str(p/'driver.s'),'2FEA00',str(code)],check=True,stdout=subprocess.DEVNULL)
 d=bytearray(a.rom.read_bytes());b=code.read_bytes();assert len(b)<0x600 and d[0x2fea00:0x2fea00+len(b)]==b'\xff'*len(b)
 d[0x2fea00:0x2fea00+len(b)]=b;struct.pack_into('>I',d,4,0x2fea00)
 for addr in stubs:d[addr:addr+2]=bytes.fromhex('4e75')
 (p/'driver.bin').write_bytes(d);cmd=p/'commands.txt';cmd.write_text(f'1000 dump ff3ff0 4 done\n1000 dump ff4000 {size} result\n1001 exit\n')
 env={k:v for k,v in os.environ.items() if not k.startswith('GPGX_')};env['GPGX_DEV']='513'
 with (p/'log.txt').open('w') as log:subprocess.run(['nice','-n','19','python3',str(here/'test_core.py'),str(p/'driver.bin'),str(cmd),str(p)],env=env,check=True,stdout=log,stderr=log,timeout=30)
 rows={line.split()[1]:bytes.fromhex(' '.join(line.split()[2:])) for line in cmd.with_suffix('.out').read_text().splitlines()};assert rows['done']==b'FORU',(name,'driver failed to return')
 return rows['result']

b=run('friction', '''
    moveq #0,%d7
loop:
    move.b %d7,0x62(%a3)
    clr.w 0x18(%a3)
    move.w #8192,0x28(%a3)
    move.w #-8192,0x2A(%a3)
    jsr 0xAC10
    clr.w (%a4)
    move.b 0x62(%a3),1(%a4)
    addq.l #2,%a4
    move.w %d2,(%a4)+
    move.w 0x28(%a3),(%a4)+
    move.w 0x2A(%a3),(%a4)+
    addq.w #1,%d7
    cmp.w #256,%d7
    bne.s loop
''',2048,[0xac46])
rows=list(struct.iter_unpack('>4H',b));errors=[]
for flags,(got,shift,vx,vy) in enumerate(rows):
 expected=flags&~1 if flags&8 else flags;sh=9 if expected&1 else 6
 if (got,shift,vx,vy)!=(expected,sh,8192-(8192>>sh),(-8192-(-8192>>sh))&65535):errors.append(flags)
if not a.baseline:assert not errors,('friction',errors)
reports.append(dict(test='friction',cases=256,failures=len(errors),human_sliding_after_one_step=rows[9],cpu_glide_after_one_step=rows[1]))

b=run('pass-flight', '''
    moveq #0,%d6
outer:
    move.w %d6,0x34(%a3)
    moveq #0,%d7
inner:
    move.w %d7,%d2
    jsr 0xBF54
    move.w %d2,(%a4)+
    addq.w #1,%d7
    cmp.w #41,%d7
    bne.s inner
    addq.w #1,%d6
    cmp.w #2,%d6
    bne.s outer
''',164,[0xbf5e])
values=list(struct.unpack('>82H',b));expected=[max(3,min(t,24)+1) for t in range(41)]+[max(2,min(t,24)) for t in range(41)]
errors=sum(x!=y for x,y in zip(values,expected))
if not a.baseline:assert not errors,('passflight',values)
reports.append(dict(test='pass-flight',cases=82,failures=errors,goalie=values[:5],skater=values[41:46]))

b=run('penalty-shot', '''
    moveq #0,%d7
loop:
    move.b %d7,0x62(%a3)
    move.w #12345,0xFFB772
    move.w #-12345,0xFFB774
    move.w #200,0xFFB776
    jsr 0xED96
    move.w 0xFFB772,(%a4)+
    move.w 0xFFB774,(%a4)+
    move.w 0xFFB776,(%a4)+
    clr.w (%a4)
    move.b 0x62(%a3),1(%a4)
    addq.l #2,%a4
    bset #1,0x62(%a3)
    jsr 0xEF92
    move.w %sr,(%a4)+
    clr.w (%a4)
    move.b 0x62(%a3),1(%a4)
    addq.l #2,%a4
    jsr 0xEF92
    move.w %sr,(%a4)+
    addq.w #1,%d7
    cmp.w #256,%d7
    bne.s loop
''',3584,[0xed9e,0xef98])
rows=list(struct.iter_unpack('>7H',b));errors=[]
for flags,(vx,vy,vz,award,sr,setup,sr2) in enumerate(rows):
 if (vx,vy,vz,award,setup)!=(0,0,0,flags|4,flags&~6) or sr&4 or not sr2&4:errors.append(flags)
if not a.baseline:assert not errors,('penalty-shot',errors)
reports.append(dict(test='penalty-shot',cases=256,failures=len(errors),award_and_setup=list(rows[0]),zero_flag_preserved=True))

b=run('awareness', '''
    move.b #7,0x6A(%a3)
    move.b #29,0x6B(%a3)
    jsr 0xD0DE
    move.b 0x40(%a3),(%a4)+
    jsr 0xD2D6
    move.b 0x40(%a3),(%a4)+
''',2,[0xd0e4,0xd2dc])
if not a.baseline:assert b==bytes([7,7]),('awareness',b.hex())
reports.append(dict(test='offensive-awareness',cases=2,wing_and_center=list(b)))

# Actual native scan and our fallback: the closest candidate is boxed (-1),
# another is available (+1). Capture the selected object, not a made-up ranking.
for native in [True,False]:
 b=run('native-receiver' if native else 'fallback-receiver','''
    lea 0xFFB04A,%a0
    moveq #0,%d0
    move.w #383,%d7
clear:
    move.w %d0,(%a0)+
    dbra %d7,clear
    lea 0xFFB04A,%a1
    moveq #5,%d3
init:
    move.w #-1,0x34(%a1)
    adda.w #128,%a1
    dbra %d3,init
    lea 0xFFB2CA,%a3
    clr.w 0x34(%a3)
    move.w #5,0x52(%a3)
    move.w #10,0xFFB04A
    move.w #50,0xFFB0CA
    move.w #1,0xFFB0FE
    clr.w 0xFFB74A
    clr.w 0xFFB75E
    move.w #2,0xFFBEDC
    clr.w 0xFFC2F2
    moveq #-1,%d4
    movea.l #0x1234,%a0
'''+('''
    lea 0xFFB04A,%a1
    moveq #5,%d3
    jsr 0xBCF8
''' if native else '    jsr 0x2FF180\n')+'''
    move.l %a0,(%a4)+
''',4,[0xbd64,0xbd6c])
 target=int.from_bytes(b,'big')&0xffffff
 if not a.baseline:assert target==0xffb0ca,('boxed receiver',native,hex(target))
 reports.append(dict(test='native-receiver' if native else 'fallback-receiver',selected=hex(target),boxed='0xffb04a',available='0xffb0ca'))

# Native moving-target intercept and velocity arithmetic, not just the time clamp.
b=run('short-moving-passes', '''
    lea 0xFFB04A,%a3
    lea 0xFFB0CA,%a0
    move.w #1,0x34(%a3)
    move.w #180,0xFFBEDE
    moveq #50,%d6
outer:
    move.w #-7000,%d7
inner:
    move.w %d7,0x28(%a0)
    clr.w 0x2A(%a0)
    moveq #45,%d5
    bsr.s pass
    move.w 0xFFB772,(%a4)+
    move.w 0xFFB774,(%a4)+
    move.w 0xFFB776,(%a4)+
    subi.w #1000,%d7
    cmp.w #-10000,%d7
    bge.s inner
    addq.w #1,%d6
    cmp.w #70,%d6
    ble.s outer
    bra.s after
pass:
    clr.w -(%sp)
    move.w %d6,-(%sp)
    jmp 0xBEE8
after:
''',504)
passes=list(struct.iter_unpack('>hhH',b));peak=max(abs(x) for x,y,t in passes)
if not a.baseline:assert peak<=16000 and all(x>0 and y==0 for x,y,t in passes),('rocket-pass',peak,passes)
reports.append(dict(test='short-moving-passes',cases=len(passes),peak_velocity=peak,min_flight=min(t for x,y,t in passes),max_flight=max(t for x,y,t in passes)))

(out/'results.json').write_text(json.dumps(reports,indent=2)+'\n');print(json.dumps(reports,indent=2));print('PASS native forum instruction-path checks' if not a.baseline else 'BASELINE recorded')
