fifa17-recon: route the draft entry purchase, and resolve the envelope ambiguity
LIVE 2026-08-04: the draft-state array fix WORKED. The screen rendered instead of
hanging and the client advanced to the entry-fee screen, then crashed on the next
call, which we had never implemented:
GET /squad/mode/draft/state?mode=ONLINE -> our array body screen RENDERED
GET /user/credits -> 7200
GET /store/purchasegroup/all -> the entry-fee screen
POST /purchase/mode/0/draft {"currency":"COINS","usePreOrder":0}
-> {} UNMAPPED, then the crash
Advancing the failure to the next unimplemented call is what a correct fix looks like.
ENVELOPE AMBIGUITY RESOLVED, and ENDPOINT_MAP's note about it is wrong. Two structures
reference RS4:FutPurchaseDraftModeServerResponse:
0x18014c260 vtable 0x180224ef8, factory 0x18014c090. 3188 chars, OBJECT root
(prologue tests != 10 = END_OBJECT), 1 skip handler, exactly the seven
scalar ints. THIS IS THE RESPONSE PARSER.
0x180150310 vtable 0x1802262f0, factory 0x180150260. 1836 chars, ARRAY root
(loops until 0xd), ZERO skip handlers -- and NOT a response root at
all. It parses ENTRANCE CRITERIA: each element's name is strcmp'd
against the literals "COINS", "POINTS", "DRAFT_TOKEN" and stored at
+0x28/+0x2c/+0x30. It shares the class-name string because it is the
fee sub-object, not an "alternate/summary envelope" as documented.
THE CRASH ITSELF DISCRIMINATED, which is worth keeping as a technique. An object-root
parser handed {} parses benignly and leaves defaults; an array-root parser handed {}
desyncs and HANGS, which is exactly what draft/state did before the fix. We observed a
CRASH, not a hang, so the object-root parser is what ran and the failure is downstream
of an empty-but-valid parse. Consistent with 0x18014c260, inconsistent with the other.
Coins are NOT deducted. The client posts the price in the URL and it sent 0, because we
omit entranceCriteria from draft/state so there is no fee to charge. Charging a guessed
amount would be inventing an economy rule.
ALSO FIXED, before it reached the game: the route table hands handlers the compiled
PATTERN, not a match object (the dispatcher calls fn(rx, self)), so calling .group() on
the first argument raised AttributeError and killed the connection outright. That is
strictly worse than the {} it was replacing. Caught by verifying the response actually
changed after the restart rather than assuming the route worked.
Default ON: the behaviour it replaces is a confirmed crash, so no working state is at
risk. FUT_DRAFT_PURCHASE=0 reverts.
392 + 61 checks green, zero tracebacks on a clean boot.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
@@ -875,6 +875,7 @@ ROUTES = [
|
||||
# is composed by appending a suffix, so it is invisible to the request-template
|
||||
# table, and the generic /squad route below was swallowing it. MUST precede it.
|
||||
(re.compile(G + r"/squad/mode/draft/state"), lambda m, h: draft_state_route(h)),
|
||||
(re.compile(G + r"/purchase/mode/\d+/draft"), lambda m, h: draft_purchase_route(h)),
|
||||
(re.compile(G + r"/squad"), lambda m, h: squad_route(h)),
|
||||
(re.compile(G + r"/match/keepalive"), lambda m, h: (204, None)),
|
||||
# PUT ut/%s/match/reset = FutResetMatch. Seen live at boot (2026-08-03) as an
|
||||
@@ -1257,6 +1258,76 @@ def draft_state_route(h):
|
||||
}]
|
||||
|
||||
|
||||
# ---- Draft entry purchase ----------------------------------------------------
|
||||
# POST ut/%s/purchase/mode/{price}/draft body {"currency":"COINS","usePreOrder":0}
|
||||
# -> FutPurchaseDraftModeServerResponse. "Buys" entry into draft mode and returns
|
||||
# the fresh draft session summary.
|
||||
#
|
||||
# LIVE 2026-08-04: this endpoint was UNMAPPED, answered {} by the catch-all, and the
|
||||
# client CRASHED immediately after. Sequence, from the log:
|
||||
# GET /squad/mode/draft/state?mode=ONLINE -> our array body, screen RENDERED
|
||||
# GET /user/credits -> 7200
|
||||
# GET /store/purchasegroup/all -> the entry-fee screen
|
||||
# POST /purchase/mode/0/draft -> {} then the crash
|
||||
# So the draft-state fix worked and simply advanced the failure to the next
|
||||
# unimplemented call, which is the outcome a correct fix is supposed to have.
|
||||
#
|
||||
# WHICH ENVELOPE. ENDPOINT_MAP flags a "response-variant ambiguity" here: two
|
||||
# structures reference the class name. Resolved this session, and the doc's note about
|
||||
# the second one is wrong:
|
||||
# 0x18014c260 (vtable 0x180224ef8, factory 0x18014c090) 3188 chars, OBJECT root
|
||||
# (prologue tests != 10 = END_OBJECT), 1 skip-handler call, and exactly
|
||||
# the seven scalar ints below. THIS IS THE RESPONSE PARSER.
|
||||
# 0x180150310 (vtable 0x1802262f0, factory 0x180150260) 1836 chars, ARRAY root
|
||||
# (loops until 0xd = END_ARRAY), ZERO skip handlers -- and it is not a
|
||||
# response root at all. It parses ENTRANCE CRITERIA: each element's
|
||||
# name is strcmp'd against the literals "COINS", "POINTS" and
|
||||
# "DRAFT_TOKEN" and stored at +0x28/+0x2c/+0x30. It shares the name
|
||||
# string because it is the fee sub-object, not an alternate envelope.
|
||||
#
|
||||
# THE CRASH ITSELF DISCRIMINATES, which is worth recording as a technique. An
|
||||
# object-root parser handed {} parses benignly and leaves defaults; an array-root
|
||||
# parser handed {} desyncs and HANGS, which is exactly what draft/state did before it
|
||||
# was fixed. We observed a CRASH, not a hang, so the object-root parser is what ran,
|
||||
# and the failure is downstream of an empty-but-valid parse. That is consistent with
|
||||
# 0x18014c260 and inconsistent with 0x180150310.
|
||||
#
|
||||
# All seven members are scalar ints and the skip handler is present, so unknown keys
|
||||
# are inert and there is no freeze surface here.
|
||||
#
|
||||
# NOT DEDUCTED FROM COINS. The client posts {"currency":"COINS"} with the price in the
|
||||
# URL, and the price it sent was 0 because we omit entranceCriteria from draft/state,
|
||||
# so there is no fee to charge yet. Charging a guessed amount would be inventing an
|
||||
# economy rule; when entranceCriteria is served the price becomes real and this is the
|
||||
# place to take it.
|
||||
#
|
||||
# DEFAULT ON for the same reason as FUT_DRAFT_STATE: the current behaviour is a
|
||||
# confirmed crash, so there is no working state being protected.
|
||||
DRAFT_PURCHASE = os.environ.get("FUT_DRAFT_PURCHASE", "1") == "1"
|
||||
|
||||
|
||||
def draft_purchase_route(h):
|
||||
if not DRAFT_PURCHASE:
|
||||
return 200, {}
|
||||
# NB: the route table hands handlers the compiled PATTERN, not a match object
|
||||
# (the dispatcher calls fn(rx, self)), so re-extract from the path rather than
|
||||
# calling .group() on the first argument. Doing that raised AttributeError,
|
||||
# which killed the connection outright -- strictly worse than the {} it replaced.
|
||||
m = re.search(r"/purchase/mode/(\d+)/draft", h.path)
|
||||
price = int(m.group(1)) if m else 0
|
||||
log(" DRAFT: purchase entry, price=%d (not deducted -- no entranceCriteria "
|
||||
"served yet, so the client posted its own price)" % price)
|
||||
return 200, {
|
||||
"championEventId": 0,
|
||||
"expectedTierLevel": 1,
|
||||
"gamesPlayed": 0,
|
||||
"gamesRemaining": 4, # a draft run is 4 rounds
|
||||
"rank": 0,
|
||||
"score": 0,
|
||||
"tierLevel": 1,
|
||||
}
|
||||
|
||||
|
||||
def champion_route(h):
|
||||
"""ut/%s/champion -- registration 0x18014980d (ack, no atoms),
|
||||
topX 0x18014a09d ({"entries":[]}), friends 0x18014b7ad ({} safe)."""
|
||||
|
||||
Reference in New Issue
Block a user