From 245c22161b79273dfd1a37ae2b7b0f4023deb747 Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 6 Aug 2026 14:39:38 -0700 Subject: [PATCH] fifa17-recon: correct tradePile/counts shape, and narrow the marketdata array fix Two follow-ups on the working transfer market. 1. GET /tradePile/counts now returns the FutGetAuctionCount shape ({count, maxAuctionsAllowed, offered, selling, sold}, all scalar ints, atoms 0xbc/0x1bf/0x1e5/0x2b8/0x2c9) via a dedicated route ordered before /tradePile. Previously it fell through to tradepile_route and got the auction-LIST body, which the counts deser skips, leaving every tally at its constructor default. Survivable but wrong; the doc flags the loaded byte at +0x28 as gating a completion-handler branch. selling reflects real STORE.listings(). 2. Narrowed the marketdata bare-array fix to /pricelimits only. The client sends TWO marketdata requests: /marketdata/pricelimits (GetSuggestedPricing, a bare array, the thing that froze) and plain /marketdata?defId=N (price comparison, an OBJECT). The prior commit returned the array for both, which the contract suite caught (test_market_bodies: 'list' has no attribute get) -- plain /marketdata wants {minPrice,maxPrice} and was never the freeze. Returning the array for it would be the same desync in reverse. Now: pricelimits -> array, plain marketdata -> object. The contract suite catching my over-broadened fix before it reached the game is the suite doing its job. 439 contract checks pass, market unit suite passes. Co-Authored-By: Claude Opus 5 (1M context) --- fifa17-recon/tools/utas_server.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/fifa17-recon/tools/utas_server.py b/fifa17-recon/tools/utas_server.py index 4e03c28..3ef8f17 100755 --- a/fifa17-recon/tools/utas_server.py +++ b/fifa17-recon/tools/utas_server.py @@ -673,7 +673,15 @@ def marketdata_route(h): pricing is a later refinement, not a freeze concern. """ from urllib.parse import urlparse, parse_qs - q = parse_qs(urlparse(h.path).query) + parsed = urlparse(h.path) + # ONLY /marketdata/pricelimits is the bare-array GetSuggestedPricing. Plain + # /marketdata?defId=N is a DIFFERENT endpoint (price comparison) that takes an + # OBJECT: it was served {minPrice,maxPrice} in the frozen session and did NOT + # freeze, so it wants an object, not the array. Returning the array for it would + # be the same object-vs-array desync in reverse. Keep them distinct. + if not parsed.path.endswith("/pricelimits"): + return 200, {"minPrice": 150, "maxPrice": 15000} + q = parse_qs(parsed.query) raw = q.get("defId", [""])[0] ids = [int(x) for x in raw.split(",") if x.strip().isdigit()] return 200, [{"defId": d, "minPrice": 150, "maxPrice": 15000} for d in ids] @@ -1147,6 +1155,9 @@ ROUTES = [ (re.compile(G + r"/activeMessage"), lambda m, h: (200, {})), # ---- transfer market / auction house (empty-but-valid; ENDPOINT_MAP market ยง) # tradePile MUST precede /trade ("/tradePile" contains the "/trade" prefix). + # /tradePile/counts (GetAuctionCount) MUST precede /tradePile: the latter's regex + # also matches the /counts path, and the two responses are different shapes. + (re.compile(G + r"/tradePile/counts"), lambda m, h: auction_counts_route(h)), (re.compile(G + r"/tradePile"), lambda m, h: tradepile_route(h)), (re.compile(G + r"/trade"), lambda m, h: trade_route(h)), (re.compile(G + r"/watchList"), lambda m, h: watchlist_route(h)), @@ -2959,6 +2970,22 @@ def watchlist_route(h): return 200, {"auctionInfo": [], "credits": STORE.coins(), "total": 0} +def auction_counts_route(h): + """GET tradePile/counts -- FutGetAuctionCount (deser 0x180163770). Distinct from + /tradePile: this is the auction TALLY, not the listing list. Until now it fell + through to tradepile_route and got {auctionInfo,...}, which the counts deser skips, + leaving every count at its constructor default. Survivable but wrong. + + All five fields are scalar ints (count 0xbc, maxAuctionsAllowed 0x1bf, offered + 0x1e5, selling 0x2b8, sold 0x2c9), so there is no container-type freeze risk. + They are the only inputs to IS_MAX_AUCTIONS (FUN_1800377c0 = !(max<0 || cur