"""D3: (a) constructor defaults for the gate block, (b) the Flash property
publisher registration table.

HYPOTHESIS A: model+0x1fd2e's CONSTRUCTOR DEFAULT is 1. Live it is 0, and the
only disp32 writer is FUN_18011dc50 at 0x18011dc91. If the default is 1, then
that applier RAN and wrote 0, which means the /settings apply path executes and
merely computes the wrong value -- the opposite of "unreachable".
Method: locate the constructor by rip-relative reference to the vtable
0x18021c2a0, decompile it in full, and separately scan .text for the disp32
0x0001fd2c/2d/2e/2f/0x1fd1c so every write form (mov imm, mov reg, movzx, cmp,
lea) is caught by the DISPLACEMENT rather than by the opcode.

HYPOTHESIS B (Q2/Q3): FUN_18003e370 (per-card action flags) and FUN_1800377c0
(auction counts) are entries in a property-provider table. Enumerating that
table gives every Flash property surface the transfer UI can read, which is how
to tell whether "Place on Transfer List" and "List on Transfer Market" share one
predicate.

CONTROL for the disp32 scan: 0x1fd2e must return exactly the two already-known
sites (read 0x18011c670, write 0x18011dc91). If it returns something else the
scan is mis-tuned and its other answers are worthless.
"""
import traceback

try:
    TEXT_LO, TEXT_HI = 0x180001000, 0x1801e5000
    text = read_bytes(TEXT_LO, TEXT_HI - TEXT_LO)
    print("text bytes: %d" % len(text))

    def riprefs(target):
        out = []
        for i in range(0, len(text) - 4):
            d = int.from_bytes(text[i:i + 4], "little", signed=True)
            va = TEXT_LO + i
            for ilen in range(4, 12):
                if va + ilen + d == target:
                    out.append((va, ilen))
                    break
        return out

    def dispscan(disp):
        pat = int(disp).to_bytes(4, "little")
        out = []
        i = text.find(pat)
        while i != -1:
            va = TEXT_LO + i
            f = fm.getFunctionContaining(addr(va))
            out.append((va, f.getName() if f else "?",
                        int(f.getEntryPoint().getOffset()) if f else 0,
                        text[max(0, i - 6):i + 6].hex()))
            i = text.find(pat, i + 1)
        return out

    print("#" * 78)
    print("### A1. disp32 scan (CONTROL first)")
    for disp, nm in ((0x1FD2E, "IS_TRADING_ENABLED  CONTROL"),
                     (0x1FD1C, "TRADE_PILE_SIZE"),
                     (0x1FD20, "watchlist size"),
                     (0x1FD28, "the 0x3c field"),
                     (0x1FD2C, "byte=1 live"),
                     (0x1FD2D, "byte=1 live"),
                     (0x1FD2F, "byte=1 live"),
                     (0x54F8, "auction sub-object base")):
        hits = dispscan(disp)
        print("--- +%#x  %-28s %d sites" % (disp, nm, len(hits)))
        for va, nm2, ent, ctx in hits:
            print("      %#x  %s (%#x)  ctx=%s" % (va, nm2, ent, ctx))

    print()
    print("#" * 78)
    print("### A2. references to the model vtable 0x18021c2a0 (constructor hunt)")
    ctors = set()
    for va, ilen in riprefs(0x18021C2A0):
        f = fm.getFunctionContaining(addr(va))
        ent = int(f.getEntryPoint().getOffset()) if f else 0
        print("    disp@%#x ilen=%d in %s (%#x) raw=%s"
              % (va, ilen, f.getName() if f else "?", ent, read_bytes(va - 3, 12).hex()))
        if ent:
            ctors.add(ent)
    for a in find_all((0x18021C2A0).to_bytes(8, "little"), blocks=(".rdata", ".data")):
        print("    absolute qword ptr at %#x" % a)

    print()
    print("#" * 78)
    print("### A3. constructor decompiles")
    for ent in sorted(ctors):
        src = dec(ent)
        print("=" * 78)
        print("### %#x  len(src)=%d" % (ent, len(src)))
        print("=" * 78)
        print(src)

    print()
    print("#" * 78)
    print("### B. property-provider table around FUN_18003e370 / FUN_1800377c0")
    for nm, fa in (("FUN_18003e370 action flags", 0x18003E370),
                   ("FUN_1800377c0 auction counts", 0x1800377C0),
                   ("FUN_18003e550 listing panel", 0x18003E550)):
        hits = find_all(int(fa).to_bytes(8, "little"), blocks=(".rdata", ".data"))
        print("--- %s: %d absolute-pointer sites: %s"
              % (nm, len(hits), [hex(h) for h in hits]))
        for h in hits:
            print("    neighbourhood of %#x:" % h)
            for k in range(-6, 7):
                q = qword(h + k * 8)
                f = fm.getFunctionAt(addr(q)) if 0x180000000 <= q < 0x181000000 else None
                s = ""
                if 0x1801E5000 <= q < 0x18028A000:
                    s = repr(rd_str(q, 48))
                print("       %+4d  %#018x  %s %s"
                      % (k * 8, q, f.getName() if f else "", s))
except Exception:
    traceback.print_exc()
