"""D3 run 9: CORRECTION RUN. There IS a reader, and I nearly missed it.

WHAT WENT WRONG IN RUNS 3-8. I triaged the disp32 scan by ADDRESS BAND, treating
everything below ~0x180100000 as "engine noise", and on that basis dismissed
FUN_18002c3c0. It is in fact a pack-record -> view-model adapter that reads all five
packContentInfo slots, plus `start` (+0xb4) and `unopened` (+0xcd). Address band is
not evidence. This run replaces the band heuristic with a FIELD FINGERPRINT.

FINGERPRINT. The pack record's distinctive, disp32-encodable field offsets are
  0xb0 state, 0xb4 start, 0xbc quantity, 0xc0, 0xc4, 0xc8 saleType, 0xcc
  useDefaultImage, 0xcd unopened, 0xce isPremium, 0xcf dealType-free,
  0xd0 dealType-promo, 0x138 visible, 0x13c bonus, 0x140, 0x144 itemQuantity,
  0x148 gold, 0x14c silver, 0x150 bronze, 0x154 rare.
Any unrelated struct may collide on one or two of these. Colliding on five or more,
especially on the tight run 0xcd/0xce/0xcf/0xd0, is not chance.

CONTROLS. The known-good members must score at the top: 0x1801340e0 (copy-assign),
  0x180133210 (uninitialised_copy), 0x18002c3c0 (the adapter just found).
  0x180133af0 (stride 0x168) and 0x180134b50 (extends to +0x163) must NOT, since
  their strides prove they are other classes.

THEN follow the view model FUN_18002c3c0 builds: its callers, FUN_18002cc90 which it
tail-calls, and every reader of the view model's copies of the quantities at
vm+0xc0/0xc4/0xc8/0xcc/0xd0, to see whether any of them counts an item list.
"""
import traceback, sys, os

OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres/"
os.makedirs(OUT, exist_ok=True)

FP = [0x0b0, 0x0b4, 0x0bc, 0x0c0, 0x0c4, 0x0c8, 0x0cc, 0x0cd, 0x0ce, 0x0cf, 0x0d0,
      0x138, 0x13c, 0x140, 0x144, 0x148, 0x14c, 0x150, 0x154]
TIGHT = [0x0cd, 0x0ce, 0x0cf, 0x0d0, 0x13c, 0x144, 0x14c, 0x154]
VM = [0x0c0, 0x0c4, 0x0c8, 0x0cc, 0x0d0, 0x084, 0x0b0, 0x0b5, 0x0b6, 0x0b7, 0x0b8]


def dump(tag, va, path, echo=True):
    src = dec(va)
    hdr = "%s  %#x  fname=%s  len(src)=%d  (FULL, NOT TRUNCATED)" % (
        tag, va, fname(va), len(src))
    if echo:
        print("=" * 78)
        print(hdr)
        print("=" * 78)
        print(src)
    with open(path, "w") as fh:
        fh.write("// " + hdr + "\n" + src)
    return src


def scan_mem(val):
    pat = bytes([val & 0xFF, (val >> 8) & 0xFF, (val >> 16) & 0xFF, (val >> 24) & 0xFF])
    tag = "+ %#x]" % val
    out = {}
    seen = set()
    for h in find_all(pat, blocks=(".text",)):
        ins = None
        for back in range(0, 12):
            i2 = listing.getInstructionContaining(addr(h - back))
            if i2 is not None:
                ins = i2
                break
        if ins is None:
            continue
        a = int(ins.getAddress().getOffset())
        if a in seen:
            continue
        seen.add(a)
        t = str(ins)
        if tag not in t:
            continue
        f = fm.getFunctionContaining(ins.getAddress())
        if f is None or f.getName().startswith("Unwind@"):
            continue
        out.setdefault(int(f.getEntryPoint().getOffset()), []).append((a, t))
    return out


try:
    print("### 1. PACK-RECORD FIELD FINGERPRINT OVER ALL OF .text")
    hits = {}
    for off in FP:
        for k in scan_mem(off):
            hits.setdefault(k, set()).add(off)
    ranked = sorted(hits.items(), key=lambda kv: -len(kv[1]))
    print("  functions scoring >=5 fingerprint offsets:")
    strong = []
    for k, offs in ranked:
        if len(offs) < 5:
            break
        t = sorted(o for o in offs if o in TIGHT)
        print("    %#x %-22s score=%2d tight=%d %s"
              % (k, fname(k), len(offs), len(t), [hex(x) for x in sorted(offs)]))
        strong.append(k)
    print("\n  CONTROLS: 0x1801340e0 in=%s  0x180133210 in=%s  0x18002c3c0 in=%s"
          "   |  must NOT be strong: 0x180133af0 in=%s  0x180134b50 in=%s"
          % (0x1801340e0 in strong, 0x180133210 in strong, 0x18002c3c0 in strong,
             0x180133af0 in strong, 0x180134b50 in strong))

    print("\n  full decompile of every strong function not already understood:")
    KNOWN = {0x1801340e0, 0x180133210, 0x1801342d0, 0x18013af30, 0x18002c3c0}
    for k in strong:
        if k in KNOWN:
            continue
        dump("STRONG FINGERPRINT", k, OUT + "d3_fp_%x.txt" % k, echo=True)

    print("\n### 2. THE ADAPTER AND ITS VIEW MODEL")
    print("  callers of adapter 0x18002c3c0: %s"
          % [(hex(a), n) for a, n in callers(0x18002c3c0)])
    for a, n in callers(0x18002c3c0):
        dump("ADAPTER CALLER", a, OUT + "d3_ad_caller_%x.txt" % a, echo=True)
    dump("FUN_18002cc90 (tail call from adapter)", 0x18002cc90,
         OUT + "d3_vm_18002cc90.txt", echo=True)
    print("  callers of 0x18002cc90: %s"
          % [(hex(a), n) for a, n in callers(0x18002cc90)])

    print("\n### 3. VIEW-MODEL QUANTITY READERS (vm+0xc0..0xd0)")
    vmhits = {}
    for off in (0x0c0, 0x0c4, 0x0c8, 0x0cc, 0x0d0):
        for k, v in scan_mem(off).items():
            vmhits.setdefault(k, {})[off] = v
    cands = [(k, o) for k, o in vmhits.items() if len(o) >= 4]
    print("  functions reading >=4 of vm+0xc0..0xd0: %d" % len(cands))
    for k, o in sorted(cands):
        print("    %#x %-22s %s" % (k, fname(k), [hex(x) for x in sorted(o)]))
    print("\n  decompiles:")
    for k, o in sorted(cands):
        if k in KNOWN:
            continue
        dump("VM QTY READER", k, OUT + "d3_vm_%x.txt" % k, echo=True)
except Exception:
    traceback.print_exc()
    sys.stdout.flush()
