"""Extract the whole /settings flag switch, and name every case. MECHANISM (recovered 2026-08-05, q_settings.py). The body really is just `configs` (0xa2) -> array of {type(0x354), value(0x377)}, so ENDPOINT_MAP's key ladder was right. What it missed is what happens AFTER an element closes: iVar2 = FUN_180180d00(type_string) <- the atom hasher, applied to the STRING VALUE of `type` switch (iVar2) { case 0x98: ... } <- ~40 arms, one per feature flag So a flag is not a JSON key. It is a row: {"type":"coinEnabled","value":"1"}, and the client hashes "coinEnabled" itself. That is why the array being empty leaves every flag at its C++ constructor default. This prints every case with its atom name, its getter, and its destination field so the response can be built without guessing. """ import re, os # NOTE: queries are exec'd inside ghidra_env.py, so __file__ is the HARNESS, not # this file. Relative-to-__file__ paths silently resolve one directory too high, # and the resulting exception skips the harness's os._exit(0), which then hangs # the JVM until the timeout kills it. Use an absolute path. REPO = "/home/alex/Documents/OpenFUT/fifa17-recon" ATOMS = {} tsv = os.path.join(REPO, "docs", "fut_atoms.tsv") for line in open(tsv): p = line.rstrip("\n").split("\t") if len(p) >= 3: try: ATOMS[int(p[1], 16)] = p[2] except ValueError: pass GETTER = { "FUN_1800d7af0": "int/bool", "FUN_1800d7ad0": "short", "FUN_1800d7b10": "?", } src = dec(0x18013C6D0) f = func(0x18013C6D0) print("decompile %d chars / body %d bytes -- full text searched" % (len(src), f.getBody().getNumAddresses())) # The switch arms live after the element loop closes. Walk line by line and keep # the most recent case label so each arm's body can be attributed. cur = None arms = {} for line in src.splitlines(): m = re.search(r"\bcase (0x[0-9a-f]+):", line) if m: cur = int(m.group(1), 16) arms.setdefault(cur, []) continue m = re.search(r"if \(iVar2 == (0x[0-9a-f]+)\)", line) if m: cur = int(m.group(1), 16) arms.setdefault(cur, []) continue if cur is not None and line.strip(): arms[cur].append(line.strip()) print("\n%-8s %-42s %-10s %s" % ("ATOM", "NAME", "GETTER", "DESTINATION")) print("-" * 100) unknown = [] for a in sorted(arms): body = " ".join(arms[a])[:200] g = next((GETTER.get(x, x) for x in GETTER if x in body), "") dst = "" m = re.search(r"param_2\[(0x[0-9a-f]+|\d+)\]", body) if m: dst = "param_2[%s]" % m.group(1) m2 = re.search(r"\*\(undefined1 \*\)\(param_2 \+ (0x[0-9a-f]+|\d+)\)", body) if m2: dst = "byte param_2+%s" % m2.group(1) if "(**(code **)" in body: dst += " (vcall)" name = ATOMS.get(a, "?? UNKNOWN ATOM") if a not in ATOMS: unknown.append(a) print("%-8s %-42s %-10s %s" % (hex(a), name, g, dst)) print("\n%d switch arms; %d unnamed" % (len(arms), len(unknown)))