# -*- coding: utf-8 -*-
"""
q_pack_inv_2 -- DIMENSION 1 (pack inventory) pass 2.

HYPOTHESES
  (a) FUN_18014cc60 really is the FutCreateUserServerResponse deserializer, so its
      atom->type mapping (bonusPacks=BOOL, login=OBJECT->userInfo deser,
      starterPack=ARRAY-of-ITEM) supersedes ENDPOINT_MAP.md's typing.
  (b) userInfo.unopenedPacks pushes (preOrderPacks+recoveredPacks) into a global
      model through singleton FUN_18011a830 -> vtbl[0x4e0]. That setter's member is
      readable by a UI surface; find the setter, the member offset, and the readers.
  (c) atom 0x20d "packList" is a real key somewhere. If any deserializer dispatches
      on it, that is the pack-inventory response we have never modelled.
  (d) ut/%s/purchased (FutGetPurchasedItemsServerResponse) is the only route that
      lists already-owned-but-unrevealed things.

CONTROLS
  * class_deser on FULL class names must return the three known-good deserializers:
      FutSquadSaveServerResponse   -> 0x180171a60
      FutSquadListServerResponse   -> 0x180172140
      FutCreateMatchServerResponse -> 0x180120380
    (pass 1 called class_deser("FutSquadSave") and got nothing -- the literal is the
    FULL name, there is no bare "FutSquadSave\\0" in the image. Not a harness bug.)
  * The atom-immediate scanner is controlled with atom 0x2cd (squad), which MUST be
    found inside FUN_18014cc60, and 0x35e (unopenedPacks) inside FUN_18013ec10.
  * Every decompile prints len(src) first and is dumped whole.
"""
import struct
import traceback

OUT = "/tmp/claude-1000/-home-alex-Documents-OpenFUT/8e521ca1-ca3e-4138-bb96-df1744dd1d30/scratchpad/packres"

ATOM_NAMES = {
    0x5d: "bonusPacks", 0xbc: "count", 0x1a5: "login", 0x20c: "packContentInfo",
    0x20d: "packList", 0x24b: "preOrderPacks", 0x262: "purchased",
    0x27b: "recoveredPacks", 0x2cd: "squad", 0x2e5: "starterPack",
    0x35d: "unopened", 0x35e: "unopenedPacks", 0x36d: "userData",
}

try:
    print("########## CONTROL BLOCK ##########")
    want = {"FutSquadSaveServerResponse": 0x180171a60,
            "FutSquadListServerResponse": 0x180172140,
            "FutCreateMatchServerResponse": 0x180120380}
    for nm, w in want.items():
        got = sorted(set(x[0] for x in class_deser(nm)))
        print("CONTROL class_deser(%-32s) -> %s expect %#x %s"
              % (nm, [hex(g) for g in got], w, "PASS" if w in got else "FAIL"))

    print("\n--- class_deser on the classes this dimension needs ---")
    for nm in ("FutCreateUserServerResponse", "FutGetPurchasedItemsServerResponse",
               "FutStoreGetPackTypesServerResponse", "FutGetUserInfoServerResponse",
               "FutStoreVoucherRefreshResponse", "FutGetUserActionServerResponse",
               "FutUpdateUserActionServerResponse"):
        got = sorted(set(x[0] for x in class_deser(nm)))
        print("  %-42s -> %s" % (nm, [hex(g) for g in got]))

    print("\n########## ATOM IMMEDIATE SCAN (.text) ##########")
    print("(a hit is a 32-bit LE immediate equal to the atom; noisy by nature, so")
    print(" only functions that ALSO call the FNV hasher 0x180180d00 are flagged HOT)")
    hashers = set()
    for (fr, ty, fn, en) in xrefs_to(0x180180d00):
        if en:
            hashers.add(en)
    print("functions calling FNV 0x180180d00: %d" % len(hashers))
    for atom in sorted(ATOM_NAMES):
        pat = struct.pack("<I", atom)
        hits = find_all(pat, blocks=(".text",))
        fns = {}
        for h in hits:
            f = fm.getFunctionContaining(addr(h))
            if f is None:
                continue
            e = int(f.getEntryPoint().getOffset())
            fns.setdefault(e, 0)
            fns[e] += 1
        hot = [(e, c) for e, c in fns.items() if e in hashers]
        print("\natom %#x %-16s : %d raw immediates, %d functions, %d HOT (parser-like)"
              % (atom, ATOM_NAMES[atom], len(hits), len(fns), len(hot)))
        for e, c in sorted(hot):
            f = fm.getFunctionContaining(addr(e))
            print("    HOT %#x %-40s x%d" % (e, f.getName(), c))

    print("\n########## unopenedPacks CONSUMER CHAIN ##########")
    src = dec(0x18011a830)
    print("--- FUN_18011a830 (model singleton getter) len=%d" % len(src))
    print(src)
    open(OUT + "/d1_singleton_18011a830.txt", "w").write(src)

    # find the vtable that singleton objects use: look at callers of 18011a830
    # that then call [vtbl+0x4e0]; the deser at 0x18013ec10 does exactly that.
    print("\n--- callers of FUN_18011a830 : %d" % len(xrefs_to(0x18011a830)))
    for (fr, ty, fn, en) in xrefs_to(0x18011a830):
        print("   %#x %-10s %s @%#x" % (fr, ty, fn, en))

    print("\n########## FUN_180142470 (CreateUser userData handler) ##########")
    s2 = dec(0x180142470)
    print("len=%d" % len(s2))
    print(s2)
    open(OUT + "/d1_userdata_180142470.txt", "w").write(s2)

    print("\n########## STRING XREFS ##########")
    STRS = {
        0x18021e650: "ut/%s/purchased", 0x18021de48: "/purchasegroup",
        0x18021e670: "ut/%s/store", 0x1801ec008: "mypacks",
        0x180231cb0: "packList", 0x18022fdc8: "unopened",
        0x18022fdd8: "unopenedPacks", 0x18022f698: "starterPack",
        0x180230490: "bonusPacks", 0x180232020: "preOrderPacks",
        0x1802322e0: "recoveredPacks", 0x1802099c8: "CentralUnclaimedPack",
        0x1802099e0: "CentralUnclaimedPack2", 0x1801eecd0: "futopenpackanimviewmodel",
        0x180231ca0: "packContentInfo", 0x180231c98: "packId",
        0x1801f0560: "{items:{pack:[...]}}", 0x180200748: "starting_pack_opened",
        0x18021f2b8: "PurchasedItems", 0x18021f308: "StorePackTypes",
        0x18021f328: "StorePackQuantities", 0x1801ef798: "GetPurchasedItems",
        0x1801f4e48: "PurchasePack", 0x18021cf50: "FUT Purchased Items",
    }
    for va in sorted(STRS):
        xr = xrefs_to(va)
        print("\n%#x %-26r : %d refs   (%r)" % (va, STRS[va], len(xr), rd_str(va, 90)))
        for (fr, ty, fn, en) in xr[:12]:
            print("     %#x %-10s %s @%#x" % (fr, ty, fn, en))

    print("\nDONE q_pack_inv_2")
except Exception:
    traceback.print_exc()
