#!/usr/bin/env python3
"""Recursive-descent 68HC11 disassembler for the RIO board firmware.

The RIO board (Toshiba TMP68HC11 + AM27C512) speaks the PCSPAK serial
protocol to the game. This tool disassembles RIOv4_2.bin to help locate the
receive/reply state machine and the wedge where the reply path dies under
stress. Address == file offset for this image (EPROM occupies $C000-$FFFF,
reset vector $FFFE -> $C000 confirms).

Usage: python disasm_6811.py [RIOv4_2.bin] > RIOv4_2.disasm.asm
"""
import sys

# ---- HC11 internal register block ($1000-$103F) annotations --------------
REG = {
    0x00:"PORTA",0x02:"PIOC",0x03:"PORTC",0x04:"PORTB",0x05:"PORTCL",
    0x07:"DDRC",0x08:"PORTD",0x09:"DDRD",0x0A:"PORTE",0x0B:"CFORC",
    0x0C:"OC1M",0x0D:"OC1D",0x0E:"TCNT",0x10:"TIC1",0x12:"TIC2",
    0x14:"TIC3",0x16:"TOC1",0x18:"TOC2",0x1A:"TOC3",0x1C:"TOC4",
    0x1E:"TI4O5",0x20:"TCTL1",0x21:"TCTL2",0x22:"TMSK1",0x23:"TFLG1",
    0x24:"TMSK2",0x25:"TFLG2",0x26:"PACTL",0x27:"PACNT",0x28:"SPCR",
    0x29:"SPSR",0x2A:"SPDR",0x2B:"BAUD",0x2C:"SCCR1",0x2D:"SCCR2",
    0x2E:"SCSR",0x2F:"SCDR",0x30:"ADCTL",0x31:"ADR1",0x32:"ADR2",
    0x33:"ADR3",0x34:"ADR4",0x39:"OPTION",0x3A:"COPRST",0x3B:"PPROG",
    0x3C:"HPRIO",0x3D:"INIT",0x3E:"TEST1",0x3F:"CONFIG",
}
def reg_ann(addr):
    if 0x1000 <= addr <= 0x103F and (addr-0x1000) in REG:
        return " ; "+REG[addr-0x1000]
    return ""

# addressing modes and their extra operand byte counts
INH,IMM8,IMM16,DIR,EXT,IDX,IDY,REL = range(8)
BSET_DIR,BCLR_DIR,BRSET_DIR,BRCLR_DIR = range(8,12)
BSET_IDX,BCLR_IDX,BRSET_IDX,BRCLR_IDX = range(12,16)
MODELEN = {INH:0,IMM8:1,IMM16:2,DIR:1,EXT:2,IDX:1,IDY:1,REL:1,
           BSET_DIR:2,BCLR_DIR:2,BRSET_DIR:3,BRCLR_DIR:3,
           BSET_IDX:2,BCLR_IDX:2,BRSET_IDX:3,BRCLR_IDX:3}

# base page opcode table: opcode -> (mnemonic, mode)
OP = {
 0x00:("TEST",INH),0x01:("NOP",INH),0x02:("IDIV",INH),0x03:("FDIV",INH),
 0x04:("LSRD",INH),0x05:("LSLD",INH),0x06:("TAP",INH),0x07:("TPA",INH),
 0x08:("INX",INH),0x09:("DEX",INH),0x0A:("CLV",INH),0x0B:("SEV",INH),
 0x0C:("CLC",INH),0x0D:("SEC",INH),0x0E:("CLI",INH),0x0F:("SEI",INH),
 0x10:("SBA",INH),0x11:("CBA",INH),0x12:("BRSET",BRSET_DIR),0x13:("BRCLR",BRCLR_DIR),
 0x14:("BSET",BSET_DIR),0x15:("BCLR",BCLR_DIR),0x16:("TAB",INH),0x17:("TBA",INH),
 0x19:("DAA",INH),0x1B:("ABA",INH),
 0x1C:("BSET",BSET_IDX),0x1D:("BCLR",BCLR_IDX),0x1E:("BRSET",BRSET_IDX),0x1F:("BRCLR",BRCLR_IDX),
 0x20:("BRA",REL),0x21:("BRN",REL),0x22:("BHI",REL),0x23:("BLS",REL),
 0x24:("BCC",REL),0x25:("BCS",REL),0x26:("BNE",REL),0x27:("BEQ",REL),
 0x28:("BVC",REL),0x29:("BVS",REL),0x2A:("BPL",REL),0x2B:("BMI",REL),
 0x2C:("BGE",REL),0x2D:("BLT",REL),0x2E:("BGT",REL),0x2F:("BLE",REL),
 0x30:("TSX",INH),0x31:("INS",INH),0x32:("PULA",INH),0x33:("PULB",INH),
 0x34:("DES",INH),0x35:("TXS",INH),0x36:("PSHA",INH),0x37:("PSHB",INH),
 0x38:("PULX",INH),0x39:("RTS",INH),0x3A:("ABX",INH),0x3B:("RTI",INH),
 0x3C:("PSHX",INH),0x3D:("MUL",INH),0x3E:("WAI",INH),0x3F:("SWI",INH),
 0x40:("NEGA",INH),0x43:("COMA",INH),0x44:("LSRA",INH),0x46:("RORA",INH),
 0x47:("ASRA",INH),0x48:("LSLA",INH),0x49:("ROLA",INH),0x4A:("DECA",INH),
 0x4C:("INCA",INH),0x4D:("TSTA",INH),0x4F:("CLRA",INH),
 0x50:("NEGB",INH),0x53:("COMB",INH),0x54:("LSRB",INH),0x56:("RORB",INH),
 0x57:("ASRB",INH),0x58:("LSLB",INH),0x59:("ROLB",INH),0x5A:("DECB",INH),
 0x5C:("INCB",INH),0x5D:("TSTB",INH),0x5F:("CLRB",INH),
 0x60:("NEG",IDX),0x63:("COM",IDX),0x64:("LSR",IDX),0x66:("ROR",IDX),
 0x67:("ASR",IDX),0x68:("LSL",IDX),0x69:("ROL",IDX),0x6A:("DEC",IDX),
 0x6C:("INC",IDX),0x6D:("TST",IDX),0x6E:("JMP",IDX),0x6F:("CLR",IDX),
 0x70:("NEG",EXT),0x73:("COM",EXT),0x74:("LSR",EXT),0x76:("ROR",EXT),
 0x77:("ASR",EXT),0x78:("LSL",EXT),0x79:("ROL",EXT),0x7A:("DEC",EXT),
 0x7C:("INC",EXT),0x7D:("TST",EXT),0x7E:("JMP",EXT),0x7F:("CLR",EXT),
 0x80:("SUBA",IMM8),0x81:("CMPA",IMM8),0x82:("SBCA",IMM8),0x83:("SUBD",IMM16),
 0x84:("ANDA",IMM8),0x85:("BITA",IMM8),0x86:("LDAA",IMM8),0x88:("EORA",IMM8),
 0x89:("ADCA",IMM8),0x8A:("ORAA",IMM8),0x8B:("ADDA",IMM8),0x8C:("CPX",IMM16),
 0x8D:("BSR",REL),0x8E:("LDS",IMM16),0x8F:("XGDX",INH),
 0x90:("SUBA",DIR),0x91:("CMPA",DIR),0x92:("SBCA",DIR),0x93:("SUBD",DIR),
 0x94:("ANDA",DIR),0x95:("BITA",DIR),0x96:("LDAA",DIR),0x97:("STAA",DIR),
 0x98:("EORA",DIR),0x99:("ADCA",DIR),0x9A:("ORAA",DIR),0x9B:("ADDA",DIR),
 0x9C:("CPX",DIR),0x9D:("JSR",DIR),0x9E:("LDS",DIR),0x9F:("STS",DIR),
 0xA0:("SUBA",IDX),0xA1:("CMPA",IDX),0xA2:("SBCA",IDX),0xA3:("SUBD",IDX),
 0xA4:("ANDA",IDX),0xA5:("BITA",IDX),0xA6:("LDAA",IDX),0xA7:("STAA",IDX),
 0xA8:("EORA",IDX),0xA9:("ADCA",IDX),0xAA:("ORAA",IDX),0xAB:("ADDA",IDX),
 0xAC:("CPX",IDX),0xAD:("JSR",IDX),0xAE:("LDS",IDX),0xAF:("STS",IDX),
 0xB0:("SUBA",EXT),0xB1:("CMPA",EXT),0xB2:("SBCA",EXT),0xB3:("SUBD",EXT),
 0xB4:("ANDA",EXT),0xB5:("BITA",EXT),0xB6:("LDAA",EXT),0xB7:("STAA",EXT),
 0xB8:("EORA",EXT),0xB9:("ADCA",EXT),0xBA:("ORAA",EXT),0xBB:("ADDA",EXT),
 0xBC:("CPX",EXT),0xBD:("JSR",EXT),0xBE:("LDS",EXT),0xBF:("STS",EXT),
 0xC0:("SUBB",IMM8),0xC1:("CMPB",IMM8),0xC2:("SBCB",IMM8),0xC3:("ADDD",IMM16),
 0xC4:("ANDB",IMM8),0xC5:("BITB",IMM8),0xC6:("LDAB",IMM8),0xC8:("EORB",IMM8),
 0xC9:("ADCB",IMM8),0xCA:("ORAB",IMM8),0xCB:("ADDB",IMM8),0xCC:("LDD",IMM16),
 0xCE:("LDX",IMM16),0xCF:("STOP",INH),
 0xD0:("SUBB",DIR),0xD1:("CMPB",DIR),0xD2:("SBCB",DIR),0xD3:("ADDD",DIR),
 0xD4:("ANDB",DIR),0xD5:("BITB",DIR),0xD6:("LDAB",DIR),0xD7:("STAB",DIR),
 0xD8:("EORB",DIR),0xD9:("ADCB",DIR),0xDA:("ORAB",DIR),0xDB:("ADDB",DIR),
 0xDC:("LDD",DIR),0xDD:("STD",DIR),0xDE:("LDX",DIR),0xDF:("STX",DIR),
 0xE0:("SUBB",IDX),0xE1:("CMPB",IDX),0xE2:("SBCB",IDX),0xE3:("ADDD",IDX),
 0xE4:("ANDB",IDX),0xE5:("BITB",IDX),0xE6:("LDAB",IDX),0xE7:("STAB",IDX),
 0xE8:("EORB",IDX),0xE9:("ADCB",IDX),0xEA:("ORAB",IDX),0xEB:("ADDB",IDX),
 0xEC:("LDD",IDX),0xED:("STD",IDX),0xEE:("LDX",IDX),0xEF:("STX",IDX),
 0xF0:("SUBB",EXT),0xF1:("CMPB",EXT),0xF2:("SBCB",EXT),0xF3:("ADDD",EXT),
 0xF4:("ANDB",EXT),0xF5:("BITB",EXT),0xF6:("LDAB",EXT),0xF7:("STAB",EXT),
 0xF8:("EORB",EXT),0xF9:("ADCB",EXT),0xFA:("ORAB",EXT),0xFB:("ADDB",EXT),
 0xFC:("LDD",EXT),0xFD:("STD",EXT),0xFE:("LDX",EXT),0xFF:("STX",EXT),
}
# page 2 ($18): Y-register/Y-indexed forms
OP18 = {
 0x08:("INY",INH),0x09:("DEY",INH),0x1C:("BSET",BSET_IDX),0x1D:("BCLR",BCLR_IDX),
 0x1E:("BRSET",BRSET_IDX),0x1F:("BRCLR",BRCLR_IDX),0x30:("TSY",INH),0x35:("TYS",INH),
 0x38:("PULY",INH),0x3A:("ABY",INH),0x3C:("PSHY",INH),0x60:("NEG",IDY),
 0x63:("COM",IDY),0x64:("LSR",IDY),0x66:("ROR",IDY),0x67:("ASR",IDY),
 0x68:("LSL",IDY),0x69:("ROL",IDY),0x6A:("DEC",IDY),0x6C:("INC",IDY),
 0x6D:("TST",IDY),0x6E:("JMP",IDY),0x6F:("CLR",IDY),0x8C:("CPY",IMM16),
 0x8F:("XGDY",INH),0x9C:("CPY",DIR),0xA0:("SUBA",IDY),0xA1:("CMPA",IDY),
 0xA2:("SBCA",IDY),0xA3:("SUBD",IDY),0xA4:("ANDA",IDY),0xA5:("BITA",IDY),
 0xA6:("LDAA",IDY),0xA7:("STAA",IDY),0xA8:("EORA",IDY),0xA9:("ADCA",IDY),
 0xAA:("ORAA",IDY),0xAB:("ADDA",IDY),0xAC:("CPY",IDY),0xAD:("JSR",IDY),
 0xAE:("LDS",IDY),0xAF:("STS",IDY),0xBC:("CPY",EXT),0xCE:("LDY",IMM16),
 0xDE:("LDY",DIR),0xDF:("STY",DIR),0xE0:("SUBB",IDY),0xE1:("CMPB",IDY),
 0xE2:("SBCB",IDY),0xE3:("ADDD",IDY),0xE4:("ANDB",IDY),0xE5:("BITB",IDY),
 0xE6:("LDAB",IDY),0xE7:("STAB",IDY),0xE8:("EORB",IDY),0xE9:("ADCB",IDY),
 0xEA:("ORAB",IDY),0xEB:("ADDB",IDY),0xEC:("LDD",IDY),0xED:("STD",IDY),
 0xEE:("LDY",IDY),0xEF:("STY",IDY),0xBE:("LDY",EXT),0xBF:("STY",EXT),
}
OP1A = {0x83:("CPD",IMM16),0x93:("CPD",DIR),0xA3:("CPD",IDX),0xB3:("CPD",EXT),
        0xAC:("CPY",IDX),0xEE:("LDY",IDX),0xEF:("STY",IDX)}
OPCD = {0xA3:("CPD",IDY),0xAC:("CPX",IDY),0xEE:("LDX",IDY),0xEF:("STX",IDY)}

def u16(d,a): return (d[a]<<8)|d[a+1]

class Insn:
    __slots__=("addr","end","mnem","mode","opbytes","txt","target","flow")
    def __init__(s,**k):
        for n,v in k.items(): setattr(s,n,v)

def decode(d,a):
    """Decode one instruction at address a. Returns Insn or None (illegal)."""
    start=a; op=d[a]; a+=1; pfx=None; table=OP
    if op==0x18: pfx=0x18; table=OP18; op=d[a]; a+=1
    elif op==0x1A: pfx=0x1A; table=OP1A; op=d[a]; a+=1
    elif op==0xCD: pfx=0xCD; table=OPCD; op=d[a]; a+=1
    ent=table.get(op)
    if ent is None: return None
    mnem,mode=ent; n=MODELEN[mode]; ops=d[a:a+n]; a+=n
    idxreg="Y" if (mode in (IDY,) or pfx in (0x18,) and mode in (IDX,)) else "X"
    if pfx==0xCD: idxreg="Y"
    if pfx==0x1A and mode==IDY: idxreg="Y"
    tgt=None; flow="seq"; ann=""
    if mode==INH: txt=mnem
    elif mode==IMM8: txt=f"{mnem} #${ops[0]:02X}"
    elif mode==IMM16:
        v=(ops[0]<<8)|ops[1]; txt=f"{mnem} #${v:04X}"
    elif mode==DIR:
        txt=f"{mnem} ${ops[0]:02X}"; ann=reg_ann(ops[0])
    elif mode==EXT:
        v=(ops[0]<<8)|ops[1]; txt=f"{mnem} ${v:04X}"; ann=reg_ann(v)
        if mnem=="JSR": tgt=v; flow="call"
        elif mnem=="JMP": tgt=v; flow="jump"
    elif mode==IDX or mode==IDY:
        txt=f"{mnem} ${ops[0]:02X},{idxreg}"
    elif mode==REL:
        rel=ops[0]-256 if ops[0]>127 else ops[0]; tgt=a+rel
        txt=f"{mnem} ${tgt:04X}"
        if mnem=="BRA": flow="jump"
        elif mnem=="BSR": flow="call"
        else: flow="branch"
    elif mode in (BSET_DIR,BCLR_DIR):
        txt=f"{mnem} ${ops[0]:02X},#${ops[1]:02X}"; ann=reg_ann(ops[0])
    elif mode in (BSET_IDX,BCLR_IDX):
        txt=f"{mnem} ${ops[0]:02X},{idxreg},#${ops[1]:02X}"
    elif mode in (BRSET_DIR,BRCLR_DIR):
        rel=ops[2]-256 if ops[2]>127 else ops[2]; tgt=a+rel
        txt=f"{mnem} ${ops[0]:02X},#${ops[1]:02X},${tgt:04X}"; ann=reg_ann(ops[0]); flow="branch"
    elif mode in (BRSET_IDX,BRCLR_IDX):
        rel=ops[2]-256 if ops[2]>127 else ops[2]; tgt=a+rel
        txt=f"{mnem} ${ops[0]:02X},{idxreg},#${ops[1]:02X},${tgt:04X}"; flow="branch"
    else: txt=mnem
    if mnem in ("RTS","RTI","JMP","BRA","STOP","WAI") and flow not in ("call","branch"):
        if mnem in ("RTS","RTI","STOP"): flow="end"
    ob=bytes([d[i] for i in range(start,a)])
    return Insn(addr=start,end=a,mnem=mnem,mode=mode,opbytes=ob,
                txt=txt+ann,target=tgt,flow=flow)

def main():
    path=sys.argv[1] if len(sys.argv)>1 else "RIOv4_2.bin"
    d=open(path,"rb").read()
    LO,HI=0xC000,0x10000
    # entry points: reset + all IRQ vectors
    entries=set()
    for va in range(0xFFD6,0x10000,2):
        entries.add(u16(d,va))
    # The RX/TX protocol runs as a pointer state machine: handlers are stored
    # into dispatch-pointer RAM vars and reached via `JMP $00,X`, which a
    # recursive tracer can't follow. Seed every handler by scanning for
    # `LDX #imm16 ; STX <dispatchvar>` (CE iw FF pp) and taking imm16.
    DISPATCH={0x292F,0x2927,0x2929,0x292B,0x2D3B,0x2D34,0x2D36,0x2D38}
    for a in range(LO,HI-5):
        if d[a]==0xCE and d[a+3]==0xFF:
            iw=(d[a+1]<<8)|d[a+2]; pp=(d[a+4]<<8)|d[a+5]
            if pp in DISPATCH and LO<=iw<HI:
                entries.add(iw)
    insns={}; labels=set(); calls=set()
    work=[e for e in entries if LO<=e<HI]
    for e in work: labels.add(e)
    seen=set()
    while work:
        a=work.pop()
        while LO<=a<HI and a not in insns:
            ins=decode(d,a)
            if ins is None: break
            insns[a]=ins
            if ins.target is not None and LO<=ins.target<HI:
                labels.add(ins.target)
                if ins.flow in ("call",): calls.add(ins.target)
                if ins.flow in ("call","branch","jump"):
                    if ins.target not in insns: work.append(ins.target)
            if ins.flow in ("end","jump"): break
            a=ins.end
    # emit listing
    out=[]
    a=LO
    while a<HI:
        if a in insns:
            ins=insns[a]
            lbl=f"L{a:04X}:" if a in labels else ""
            mark=" <<<CALLED" if a in calls else ""
            hexb=" ".join(f"{b:02X}" for b in ins.opbytes)
            out.append(f"{a:04X}  {hexb:<20} {lbl:<8}{ins.txt}{mark}")
            a=ins.end
        else:
            # data byte
            out.append(f"{a:04X}  {d[a]:02X}                   .byte  ${d[a]:02X}")
            a+=1
    sys.stdout.write("\n".join(out)+"\n")
    sys.stderr.write(f"decoded {len(insns)} insns, {len(labels)} labels, "
                     f"{len(calls)} call targets\n")

if __name__=="__main__":
    main()
