docs+tools: measure the FIFA 17 market gate bytes in the live client
Adds trade_gate_probe.py (read-only: /proc/<pid>/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.
This commit is contained in:
@@ -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
|
out both list buttons — but it will misrepresent SBC / promo / loan rewards once
|
||||||
those exist. `untradeable` belongs on the owned-item instance as authoritative
|
those exist. `untradeable` belongs on the owned-item instance as authoritative
|
||||||
state, not inferred from definition, resourceId or rarity.
|
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/<pid>/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.
|
||||||
|
|||||||
@@ -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/<pid>/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('<Q', rd(0x1802e6398 + slide, 8))[0]
|
||||||
|
vt = struct.unpack('<Q', rd(obj, 8))[0]
|
||||||
|
print("model=%#x vtable(static)=%#x" % (obj, vt - slide))
|
||||||
|
|
||||||
|
SLOTS = [
|
||||||
|
(0x270, 'IS_TRADING_ENABLED '),
|
||||||
|
(0x2b0, 'IS_FRIENDLY_SEASON '),
|
||||||
|
(0x2c8, 'IS_DRAFT_MODE '),
|
||||||
|
(0x2e0, 'packOpeningAnimation '),
|
||||||
|
]
|
||||||
|
print("\n-- gate bytes decoded from their accessor stubs --")
|
||||||
|
for off, name in SLOTS:
|
||||||
|
slot = struct.unpack('<Q', rd(vt + off, 8))[0]
|
||||||
|
stub = rd(slot, 8)
|
||||||
|
if stub[:3] == b'\x0f\xb6\x81':
|
||||||
|
disp = struct.unpack('<I', stub[3:7])[0]
|
||||||
|
val = rd(obj + disp, 1)[0]
|
||||||
|
print(" slot +%#05x %s disp=%#x VALUE=%d" % (off, name, disp, val))
|
||||||
|
else:
|
||||||
|
print(" slot +%#05x %s NOT a movzx stub: %s" % (off, name, stub.hex()))
|
||||||
|
|
||||||
|
print("\n-- market screen inputs --")
|
||||||
|
for disp, name in [(0x1fd1c, 'TRADE_PILE_SIZE'), (0x1fd20, 'watchListSize '),
|
||||||
|
(0x1fd2e, 'tradingEnabled '), (0x1fd2f, 'storeEnabled ')]:
|
||||||
|
print(" model+%#x %s = %d" % (disp, name, rd(obj + disp, 1)[0]))
|
||||||
|
|
||||||
|
os.close(fd)
|
||||||
Reference in New Issue
Block a user