diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index ea2c5d6..915c842 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -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)."""