fifa17-recon: store fix (v2/store gate + flags) + full FUT endpoint map
Store "not available" root cause reversed from CardsDLL:
- ut/v2/game/fifa17/store is an ELIGIBILITY gate (FutStorePackQuantities
deser 0x1801758c0), not a quantity list. It reads one key "result"
(atom 0x288); the store screen refuses to open unless SUCCESS. Was
unhandled -> catch-all {} -> "not available". Now returns {"result":"SUCCESS"}.
- Store-screen entitlement checks (0x18001749d/0x1800175a2) read IS_*/
*_PURCHASE_ENABLED Blaze flags, separate from storeEnabled. Added the full
confirmed set (14 flags) to FUT_RS4_CONFIG.
- Catalog: assetId (0x23) is the real pack identity; extPrice inner keys are
amount/currency (not mtx). (Also gated client-side by GetSystemMetrics>1024x768.)
Full FUT API reversed (clean-room, CardsDLL only) into docs/ENDPOINT_MAP.md:
~100 FutXServerResponse types across 7 feature groups (market, SBC, draft,
seasons/match, club, store, user/hub), each with deserializer VA, atom-mapped
field schema + types, freeze-risk flags, and minimal known-good JSON.
Tooling kept: tools/atomdump.py (dumps the 907-atom key table at 0x1802d2760)
-> docs/fut_atoms.tsv. Research prompt: docs/OPENCODE_ENDPOINT_PROMPT.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
# Dump the FUT atom name table (atom index -> key string) from CardsDLL.
|
||||
# Table at VA 0x1802d2760 is an array of char* pointers into .rdata.
|
||||
import struct, sys
|
||||
|
||||
DLL = "/tmp/fut/cardsdll.dll"
|
||||
data = open(DLL, "rb").read()
|
||||
|
||||
# (VA_start, size, file_off) from objdump -h
|
||||
SECTIONS = [
|
||||
(0x180001000, 0x1e3f62, 0x400), # .text
|
||||
(0x1801e5000, 0xa4094, 0x1e4400), # .rdata
|
||||
(0x18028a000, 0x54000, 0x288600), # .data
|
||||
]
|
||||
|
||||
def va_to_off(va):
|
||||
for start, size, off in SECTIONS:
|
||||
if start <= va < start + size:
|
||||
return off + (va - start)
|
||||
return None
|
||||
|
||||
def read_cstr(va, maxlen=128):
|
||||
off = va_to_off(va)
|
||||
if off is None:
|
||||
return None
|
||||
end = data.find(b"\x00", off, off + maxlen)
|
||||
if end < 0:
|
||||
return None
|
||||
try:
|
||||
return data[off:end].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
|
||||
TABLE_VA = 0x1802d2760
|
||||
off = va_to_off(TABLE_VA)
|
||||
atoms = {}
|
||||
for i in range(0, 1200):
|
||||
ptr = struct.unpack_from("<Q", data, off + i * 8)[0]
|
||||
if ptr == 0:
|
||||
s = None
|
||||
else:
|
||||
s = read_cstr(ptr)
|
||||
if s is None:
|
||||
# allow a few gaps then stop if we run off the end
|
||||
if i > 40 and all(struct.unpack_from("<Q", data, off + (i + k) * 8)[0] == 0 for k in range(4)):
|
||||
break
|
||||
continue
|
||||
if s.isprintable() and 1 <= len(s) <= 40:
|
||||
atoms[i] = s
|
||||
|
||||
for i in sorted(atoms):
|
||||
print(f"{i}\t0x{i:x}\t{atoms[i]}")
|
||||
print(f"# total {len(atoms)} atoms", file=sys.stderr)
|
||||
@@ -575,6 +575,18 @@ FUT_RS4_CONFIG = (
|
||||
[("FUT_RS4_APIURL_%s" % m, UTAS_BASE) for m in FUT_RS4_MODULES]
|
||||
+ [("FUT_RS4_URL_%s" % c, UTAS_BASE) for c in FUT_RS4_CALLS_BOOT]
|
||||
+ [("FUT_RS4_BASE_URL", UTAS_BASE)]
|
||||
# STORE gate: FIFA shows "store not available" unless these config flags are
|
||||
# true. The store-screen entitlement checks (CardsDLL 0x18001749d vtable+0x138 /
|
||||
# 0x1800175a2 vtable+0x280) read the IS_*/*_PURCHASE_ENABLED booleans -- SEPARATE
|
||||
# from storeEnabled. Full confirmed list from ENDPOINT_MAP store § (grepped in
|
||||
# cardsdll.strings). (Also gated by GetSystemMetrics > 1024x768, client-side.)
|
||||
+ [(k, "1") for k in (
|
||||
"storeEnabled", "cardPackStoreEnabled", "pointsPackStoreEnabled",
|
||||
"cardPackStoreEnabled_JP", "coinEnabled", "coinEnabled_JP",
|
||||
"IS_STORE_ENABLED", "IS_COIN_PURCHASABLE", "IS_FIFAPOINT_AVAILABLE",
|
||||
"IS_FIFAPOINT_PURCHASABLE", "IS_EASTORE_SERVICE_READY",
|
||||
"COINS_PURCHASE_ENABLED", "POINTS_PURCHASE_ENABLED", "MONEY_PURCHASE_ENABLED",
|
||||
)]
|
||||
# NOTE: do NOT advertise itemDbVersion/checkServerDbVersion here or in any
|
||||
# response -- proven inert (wf_96b6c0c5): they are JSON field names that route
|
||||
# to the value-SKIP handler 0x180135ff0, never compared. See docs/CARD_SYSTEM.md.
|
||||
|
||||
@@ -154,6 +154,11 @@ ROUTES = [
|
||||
# ---- store / packs (match regardless of /ut/game vs /ut/v2/game prefix) ----
|
||||
(re.compile(r"/store/purchasegroup"), lambda m, h: store_catalog(h)),
|
||||
(re.compile(r"/store/transaction"), lambda m, h: store_buy(h)),
|
||||
# ut/v2/game/fifa17/store = FutStorePackQuantities ELIGIBILITY GATE, not a
|
||||
# quantity list. deser 0x1801758c0 reads one key "result" (atom 0x288); the
|
||||
# store screen shows "not available" unless this is SUCCESS. (ENDPOINT_MAP
|
||||
# store §2.) Bare /store only -- purchasegroup/transaction matched above.
|
||||
(re.compile(r"/store(\?|$)"), lambda m, h: (200, {"result": "SUCCESS"})),
|
||||
(re.compile(r"/purchased"), lambda m, h: purchased_items(h)),
|
||||
(re.compile(r"^/ut/auth"), lambda m, h: (200, auth_body())),
|
||||
(re.compile(r"^/ut/delete/auth"), lambda m, h: (200, {})),
|
||||
@@ -214,10 +219,19 @@ def store_catalog(h):
|
||||
# {name,funds,finalFunds}; display name is "description". (wf a245577b —
|
||||
# {purchaseGroups:...} + packId/price were all unknown atoms => empty => "not
|
||||
# available".) quantity:0 => unlimited.
|
||||
# Parse format is verified correct ("purchase" array, per-pack 0x18013af30).
|
||||
# The store still rejected minimal packs -> a pack must be COMPLETE to count as
|
||||
# valid: content info + BOTH a coins price (currencies) and a FIFA-Points price
|
||||
# (extPrice {finalPrice,originalPrice} -> {"mtx":N}).
|
||||
packs = []
|
||||
for p in PACK_CATALOG:
|
||||
gold = p["gold"]
|
||||
mtx = max(1, p["price"] // 100)
|
||||
packs.append({
|
||||
# assetId (atom 0x23) is the REAL pack identity the deser 0x18013af30
|
||||
# reads (ENDPOINT_MAP store §). id/packType/quantity/saleType/isPremium
|
||||
# are all unknown atoms -> SKIP (harmless no-ops, kept for readability).
|
||||
"assetId": p["id"],
|
||||
"id": p["id"],
|
||||
"packType": "GOLD" if gold else "BRONZE",
|
||||
"description": p["name"],
|
||||
@@ -228,11 +242,14 @@ def store_catalog(h):
|
||||
"saleType": "PERMANENT",
|
||||
"sortPriority": p["id"] - 100,
|
||||
"currencies": [{"name": "coins", "funds": p["price"], "finalFunds": p["price"]}],
|
||||
# extPrice inner keys are amount(0x1b)/currency(0xc4), NOT mtx (skipped).
|
||||
"extPrice": {"finalPrice": {"amount": mtx, "currency": "fifapoints"},
|
||||
"originalPrice": {"amount": mtx, "currency": "fifapoints"}},
|
||||
"packContentInfo": {
|
||||
"bronzeQuantity": 0 if gold else p["count"],
|
||||
"silverQuantity": 0,
|
||||
"goldQuantity": p["count"] if gold else 0,
|
||||
"rareQuantity": p["count"] if gold else 0,
|
||||
"rareQuantity": 1 if gold else 0,
|
||||
"itemQuantity": p["count"],
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user