#!/usr/bin/env python3 """Offline check: every /settings flag we ship is one the client actually switches on. A flag name is not validated by anything at runtime. The client hashes the string we send and switches on the result, so a typo, a renamed field or a flag that simply has no arm in the switch is INERT and looks exactly like "the fix did not work". This asserts each shipped name against two independent sources: 1. docs/fut_atoms.tsv -- the recovered atom table (the name must hash to an id) 2. the switch arms recovered from 0x18013c6d0 (the id must have an arm) Source 2 is the one that matters: enableSquadBuildingSetsFeature is a perfectly real atom with NO arm, so source 1 alone would have passed it. Run before shipping any settings change. No server needed. """ import os import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) # The 42 atoms with an arm in FUN_18013c6d0, recovered 2026-08-05 by # tools/ghidra_queries/q_settings_flags.py + q_settings_types.py (full decompile, # both halves of the switch, coverage asserted by char count). SWITCH_ARMS = { 0x18: "allowGracePeriodForSquadBuildingSets", 0x19: "allowUntradeableForSquadBuildingSets", 0x6D: "cardPackStoreEnabled", 0x6E: "cardPackStoreEnabled_JP", 0x80: "checkServerDbVersion", 0x86: "clientKeepAliveResetTimeoutSec", 0x8C: "clubCreateThreshold", 0x98: "coinEnabled", 0x99: "coinEnabled_JP", 0xA3: "constrainGracePeriod", 0xBB: "couchPlayEnabled", 0xF9: "enableDraftMode", 0xFA: "enableOfflineDraftMode", 0xFB: "enableLiveMessaging", 0xFC: "enableLoyaltyBonusForConceptPlayers", 0xFD: "enableObjectives", 0xFE: "enableObjectivesAsManagerTasks", 0xFF: "enableSinglePlayerDraftMode", 0x118: "extendGameSessionTimerSec", 0x11F: "fifaPointsEnabled", 0x120: "fifaPointsEnabled_JP", 0x133: "friendlySeasonsEnabled", 0x13D: "getOperationTimeoutSec", 0x16C: "itemDbVersion", 0x1C0: "maximumTradePileSize", 0x1CD: "mtxEnabled", 0x1CE: "mtxEnabled_JP", 0x1DE: "numEndMatchRetriesAllowed", 0x20E: "packOpeningAnimationEnabled", 0x242: "pointsPackStoreEnabled", 0x257: "processingStateEnabled", 0x28A: "returningUserRewardsScreenEnabled", 0x2D0: "squadBuildingSetsGracePeriodMinutes", 0x2F1: "storeEnabled", 0x2F2: "storeEnabled_JP", 0x2F3: "storyModeRewardEnabled", 0x2F5: "championsScheduleViewPeriodInMinutes", 0x30F: "enableFloatPointSquadRating", 0x310: "enableLegacyYearInfoInItemResourceId", 0x320: "tokenRedemptionEnabled", 0x32D: "tournamentQuitEnabled", 0x336: "tradingEnabled", } # Arms that do NOT simply store a value. Shipping these has side effects. SPECIAL = { "enableObjectives": "shared arm can only CLEAR the field; 1 is a no-op, 0 disables", "enableObjectivesAsManagerTasks": "same shared arm as enableObjectives", "clientKeepAliveResetTimeoutSec": "reprograms a client timer with value*1000", "getOperationTimeoutSec": "reprograms a client timer with value*1000", "checkServerDbVersion": "makes the client go read a server_db_version config", } def main(): fails = [] atoms = {} with open(os.path.join(HERE, "..", "docs", "fut_atoms.tsv")) as fh: for line in fh: p = line.rstrip("\n").split("\t") if len(p) >= 3: try: atoms[p[2]] = int(p[1], 16) except ValueError: pass # Cross-check the recovered table against the atom table both ways. by_name = {v: k for k, v in SWITCH_ARMS.items()} for name, aid in by_name.items(): if name not in atoms: fails.append("switch arm %s (%#x) is not in fut_atoms.tsv" % (name, aid)) elif atoms[name] != aid: fails.append("%s: switch says %#x, atom table says %#x" % (name, aid, atoms[name])) import utas_server as u body = u.SETTINGS if not isinstance(body, dict) or list(body) != ["configs"]: fails.append("body must be exactly {'configs': [...]}, got %r" % (body,)) return report(fails) rows = body["configs"] if not isinstance(rows, list): fails.append("configs must be a LIST (a scalar here desyncs the parser)") return report(fails) seen = set() for r in rows: if not isinstance(r, dict) or set(r) != {"type", "value"}: fails.append("row must be exactly {type, value}: %r" % (r,)) continue t, v = r["type"], r["value"] # value: any scalar is safe (getter 0x1801c79d0 coerces int/float/bool/str), # but the applier tests `== 1`, so a bool True would work and a string "1" # would work -- ints keep it unambiguous. An object or array FREEZES. if isinstance(v, (dict, list)): fails.append("%s: value is %s -- an object/array here FREEZES the client" % (t, type(v).__name__)) if not isinstance(t, str): fails.append("type must be a string, got %r" % (t,)) continue if t in seen: fails.append("%s sent twice; last one wins, so this is at best confusing" % t) seen.add(t) if t not in by_name: hint = " (it IS an atom, but has no arm in the switch)" if t in atoms else "" fails.append("%s has no arm in 0x18013c6d0 -- INERT%s" % (t, hint)) elif t in SPECIAL: print(" NOTE %-34s %s" % (t, SPECIAL[t])) gates = {"friendlySeasonsEnabled", "enableDraftMode", "tournamentQuitEnabled"} # Read the mode off the server module, never re-declare the default here: a # checker with its own copy of a default tests the copy, not the server. mode = u._SETTINGS_MODE if mode == "gates": for g in sorted(gates - seen): fails.append("mode 'gates' but %s is missing" % g) for t in sorted(seen & gates): row = next(r for r in rows if r["type"] == t) if row["value"] != 1: fails.append("%s = %r; the applier tests `== 1`, nothing else opens " "the gate" % (t, row["value"])) print(" mode=%s, %d rows, %d distinct flags, all with a live switch arm" % (mode, len(rows), len(seen))) return report(fails) def report(fails): if fails: print("\nFAIL (%d)" % len(fails)) for f in fails: print(" - %s" % f) return 1 print("PASS") return 0 if __name__ == "__main__": sys.exit(main())