"""Why does tradingEnabled stay 0 after we send it? Verify the arm -> index -> byte chain.

REFUTED LIVE 2026-08-06. We served {"type":"tradingEnabled","value":1} on BOTH delivery
paths (the standalone /settings route and the userMassInfo.settings member), the client
fetched settings four times, and the gate byte 0x1fd2e stayed 0 while the three control
gates stayed 1. So the claim "FUT_SETTINGS=keep opens the trading gate" is wrong. This
file finds out where the chain actually breaks.

THE CHAIN AS CURRENTLY BELIEVED, and every link is a candidate for being the wrong one:
  1. deser FUN_18013c6d0 hashes the STRING value of "type" and switches, 42 arms wide.
     tradingEnabled is atom 0x336.
  2. that arm stores into some index of a settings struct
  3. applier FUN_18011dc50 copies `param_2[10] == 1` into gate byte +0x1fd2e, and that
     line was LABELLED tradingEnabled
  4. service vtable slot +0x270 reads 0x1fd2e

The label in step 3 was inferred from ordering, never verified against step 2. If the
tradingEnabled arm writes an index other than 10, the label is simply wrong and 0x1fd2e
belongs to some other flag we are not sending.

FOUR HYPOTHESES, and the query is designed to separate them rather than confirm one:
  H1 wrong index: the 0x336 arm writes an index the applier does not copy to 0x1fd2e.
  H2 the applier never re-runs after a settings response; it runs once, early, from
     defaults. That fits this morning's observation that the other three gates read 1
     with an EMPTY configs array, i.e. from constructor defaults, and would mean NO
     /settings response can ever move any gate byte.
  H3 the applier runs but reads a different settings struct instance than the deser wrote.
  H4 the arm needs a different value shape (a string "1", a bool) than our int 1.

CONTROLS: storeEnabled (0x2f1) is labelled as writing +0x1fd2f from param_2[0xb], and the
store demonstrably works, so whatever is true of tradingEnabled must be consistent with
storeEnabled too. Do the same trace for BOTH and compare. If the store works via a path
that does NOT involve this applier (it is known to arrive through the Blaze client-config
store), then storeEnabled is NOT a valid control and say so.

COVERAGE: print both functions IN FULL with lengths. No absence claim from a truncation.
Remember the dispatch-form trap: enumerate case labels, == and != forms, and sub/dec
ladders before saying an atom is absent.
"""
import re
import traceback

DESER = 0x18013C6D0     # /settings deserializer, 42 arms
APPLIER = 0x18011DC50   # the only writer of the gate bytes
A_TRADING = 0x336
A_STORE = 0x2F1


def dump(va, title):
    f = func(va)
    src = dec(va)
    print("\n" + "=" * 78)
    print("%#x  %s   body %d bytes / decompile %d chars (IN FULL)"
          % (va, title, f.getBody().getNumAddresses() if f else -1, len(src)))
    print("=" * 78)
    print(src)
    return src


try:
    dsrc = dump(DESER, "settings deserializer")
    asrc = dump(APPLIER, "gate-byte applier")

    print("\n" + "=" * 78)
    print("DISPATCH TARGETS IN THE DESERIALIZER, all four forms")
    print("=" * 78)
    found = set()
    for m in re.finditer(r"case (0x[0-9a-f]+|\d+):", dsrc):
        found.add(int(m.group(1), 0))
    for m in re.finditer(r"[!=]= (0x[0-9a-f]+)\b", dsrc):
        found.add(int(m.group(1), 16))
    print("  %d targets" % len(found))
    for a, n in (("tradingEnabled", A_TRADING), ("storeEnabled", A_STORE)):
        print("  %-16s %#-6x %s" % (a, n, "PRESENT" if n in found else "ABSENT"))

    print("\n--- the tradingEnabled and storeEnabled arm bodies, to read the index each writes ---")
    lines = dsrc.split("\n")
    for want, label in ((A_TRADING, "tradingEnabled"), (A_STORE, "storeEnabled")):
        idx = None
        for i, l in enumerate(lines):
            if re.match(r"\s*case %s:" % hex(want), l) or ("== %s" % hex(want)) in l:
                idx = i
                break
        print("\n### %s (%#x) ###" % (label, want))
        if idx is None:
            print("   not located by either form")
            continue
        out = [lines[idx]]
        for l in lines[idx + 1:]:
            if re.match(r"\s*case (0x[0-9a-f]+|\d+):", l):
                break
            out.append(l)
            if len(out) > 14:
                break
        print("\n".join(out))

    print("\n" + "=" * 78)
    print("APPLIER: every gate-byte write, so the index->byte map can be read directly")
    print("=" * 78)
    for l in asrc.split("\n"):
        if "0x1fd" in l:
            print("  " + l.strip())

    print("\n" + "=" * 78)
    print("H2: WHO CALLS THE APPLIER, and is it reachable from the settings deserializer?")
    print("=" * 78)
    for a, n in callers(APPLIER):
        print("   caller %#x  %s" % (a, n))
    print("\n   callers of the DESERIALIZER:")
    for a, n in callers(DESER):
        print("   caller %#x  %s" % (a, n))
    print("\n   does the deserializer (or anything it calls) reach the applier?")
    seen, stack, hit = set(), [DESER], []
    while stack:
        cur = stack.pop()
        if cur in seen:
            continue
        seen.add(cur)
        for a, n in callees(cur):
            if a == APPLIER:
                hit.append(cur)
            if a not in seen and len(seen) < 400:
                stack.append(a)
    print("   reached from: %s" % (" ".join("%#x" % h for h in hit) or "NOT REACHABLE within 400 nodes"))

except Exception:
    traceback.print_exc()
