From dcbef721f2ecc60857bed912ad1d834b9e4a5182 Mon Sep 17 00:00:00 2001 From: funman300 Date: Mon, 17 Aug 2026 19:09:03 +0000 Subject: [PATCH] docs+tools: measure the FIFA 17 market gate bytes in the live client Adds trade_gate_probe.py (read-only: /proc//mem O_RDONLY + pread, slide proven against the on-disk FNV prologue), extending gate_byte_probe.py to vtable slot +0x270 exactly as the transfer-market analysis asked for. Measured: IS_TRADING_ENABLED=1 (was 0 in the Python era), TRADE_PILE_SIZE=100 (was 0), watchListSize=50 (was 0), with four controls reading 1. So every CardsDLL-supplied input that analysis named as a market blocker is now OPEN, which the Rust host achieves by construction -- it emits userInfo.feature as {} so the kill switch at 0x180174f19 never arms, and it already sends pileSizeClientData keys 2 and 4. This narrows the Actions-panel question to the exe-side UI script term, and rules out ownership fields, the gate bytes, the cancel route and the state vocabularies as candidates -- each on measured or PE-derived evidence rather than inference. --- docs/FIFA17_TRANSFER_MARKET_WIRE.md | 49 +++++++++++++++++ fifa17-recon/tools/trade_gate_probe.py | 76 ++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 fifa17-recon/tools/trade_gate_probe.py diff --git a/docs/FIFA17_TRANSFER_MARKET_WIRE.md b/docs/FIFA17_TRANSFER_MARKET_WIRE.md index 0c89598..9a5f45d 100644 --- a/docs/FIFA17_TRANSFER_MARKET_WIRE.md +++ b/docs/FIFA17_TRANSFER_MARKET_WIRE.md @@ -147,3 +147,52 @@ today (Core models no untradeable items) and necessary — a hardcoded `true` gr out both list buttons — but it will misrepresent SBC / promo / loan rewards once those exist. `untradeable` belongs on the owned-item instance as authoritative state, not inferred from definition, resourceId or rarity. + +--- + +## MEASURED in the live client — 2026-08-17 + +Read out of the running `FIFA17.exe` (pid-resolved, CardsDLL slide proven against +the on-disk FNV prologue) with `fifa17-recon/tools/trade_gate_probe.py`, which +extends `gate_byte_probe.py` to vtable slot `+0x270` as the transfer-market +analysis asked for. Read-only: `/proc//mem` `O_RDONLY` + `pread`. + +| Gate | Python era (2026-08-06) | Now | Owner | +|---|---|---|---| +| `IS_TRADING_ENABLED` `model+0x1fd2e` (slot `+0x270`) | **0** | **1** | settings struct `+0x28`; was zeroed by `userInfo.feature.trade` | +| `TRADE_PILE_SIZE` `model+0x1fd1c` | **0** | **100** | `userMassInfo.pileSizeClientData` key 2 | +| watch-list size `model+0x1fd20` | **0** | **50** | same member, key 4 | +| `storeEnabled` `model+0x1fd2f` | 1 | 1 | control | +| `IS_FRIENDLY_SEASON` / `IS_DRAFT_MODE` / `packOpeningAnimation` | 1 | 1 | controls | + +**CONFIRMED: every CardsDLL-supplied input the transfer-market analysis named as a +blocker is now open.** The Rust host does this by construction — it emits +`userInfo.feature` as `{}` (no `trade` member, so the kill switch at `0x180174f19` +never arms: it fires only when atom `0x330` inside `0x11c` parses as exactly 1) and +it already sends `pileSizeClientData` keys 2 and 4. Serving `tradingEnabled: 1` in +the settings `configs` array would NOT have worked, because that tail runs after +every member is parsed and would overwrite it. + +### What this rules out + +The Transfer List Actions panel not opening on an own listing is therefore **not**: + +* an ownership field — FIFA 17's auctionInfo has no `tradeOwner`/`sellerId` atom; +* `IS_TRADING_ENABLED`, `TRADE_PILE_SIZE` or the watch-list size — all measured open; +* the cancel route — `DELETE ut/delete/{ns}/trade/{tradeId}` is the PE's spelling and + is what we serve; +* `tradeState` / `bidState` / `expires` spellings — all three are the PE's own + vocabularies and values. + +Per the analysis's own falsifier ("if the byte reads 1 and the screen still refuses, +the exe-side predicate has a term we have not enumerated"), the remaining term is +**exe-side UI script**, which CardsDLL does not own and the server cannot set. +Status: **UNKNOWN**, and it is now the narrowest it has ever been. + +### Confirmed fidelity bug found on the way + +`expires` was a frozen `3600` on every poll, so the client's live countdown never +moved and an auction could never run out. Now derived from `created_at + duration` +(duration taken from the `ISStart` body), clamped at 0, with an aged-out active +listing projecting as `expired`/`none` — FIFA 17's relistable state. Verified live: +the standing listing correctly reads `expires: 0` once past its hour. diff --git a/fifa17-recon/tools/trade_gate_probe.py b/fifa17-recon/tools/trade_gate_probe.py new file mode 100644 index 0000000..592c74d --- /dev/null +++ b/fifa17-recon/tools/trade_gate_probe.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Read the FIFA 17 TRADING gate byte out of the live client. READ-ONLY. + +Extends tools/gate_byte_probe.py with vtable slot +0x270 (IS_TRADING_ENABLED, +displacement 0x1fd2e) plus the two pile-size dwords, which the transfer-market +analysis names as the market screen's CardsDLL-supplied inputs. + +Opens /proc//mem O_RDONLY and preads. Nothing here can write. +""" +import os, struct + +pid = None +for d in os.listdir('/proc'): + if d.isdigit(): + try: + if open('/proc/%s/comm' % d).read().strip() == 'FIFA17.exe': + pid = int(d) + break + except Exception: + pass +assert pid, "FIFA17.exe not running" + +base = None +for ln in open('/proc/%d/maps' % pid): + if 'CardsDLL' in ln: + base = int(ln.split('-')[0], 16) +assert base, "CardsDLL not mapped (client has not reached Ultimate Team)" +slide = base - 0x180000000 + +fd = os.open('/proc/%d/mem' % pid, os.O_RDONLY) + + +def rd(va, n): + return os.pread(fd, n, va) + + +# Control: the FNV atom-hash prologue must match the on-disk PE before any other +# address is trusted. +pe = open('/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll', 'rb').read() + + +def f(va): + return va - 0x180000000 - 0x1000 + 0x400 + + +ok = pe[f(0x180180d00):f(0x180180d00) + 32] == rd(0x180180d00 + slide, 32) +print("pid=%d slide=%#x FNV control=%s" % (pid, slide, "MATCH" if ok else "MISMATCH")) +assert ok, "slide not proven; refusing to read further" + +obj = struct.unpack('