Compare commits
305 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 750d6c2e18 | |||
| 082246c085 | |||
| 16771b0b33 | |||
| 022634704a | |||
| 96ca7c0484 | |||
| 202366611e | |||
| ea92057e53 | |||
| c71593b286 | |||
| 413ad901fb | |||
| e0e46d8a57 | |||
| fbe29da05b | |||
| 9ffbd651b1 | |||
| aa5fb2cc40 | |||
| 468bc0fba9 | |||
| 571c5f9261 | |||
| cb32fe9b84 | |||
| fbe9804d3e | |||
| f6606accb3 | |||
| 0a007f4941 | |||
| 0c4aee6164 | |||
| a57f4930f0 | |||
| f9ca901a50 | |||
| 3ce69f8951 | |||
| acd1def00d | |||
| 5ec9c7f8bf | |||
| 11c028e6eb | |||
| afadb13de4 | |||
| b6398c44e6 | |||
| a2bd048ace | |||
| 2e97ff1461 | |||
| 7f37b37be3 | |||
| 4e31fb98a2 | |||
| 6cc22e5cc5 | |||
| b1d7ed2570 | |||
| dcbef721f2 | |||
| 772f8a615a | |||
| bf9ae20367 | |||
| 58d1f9426f | |||
| 3cd31c4322 | |||
| ae5feb05b7 | |||
| f2c4927ea6 | |||
| aa2abc2772 | |||
| 1aa84afa9a | |||
| 33e9118329 | |||
| e06fd57211 | |||
| 42fd3c7e90 | |||
| 0bc71dbd74 | |||
| 2ecd830d75 | |||
| 3a51b0ebd4 | |||
| ad406f21bd | |||
| 12fb9fc38b | |||
| 7b580a0070 | |||
| 22443a3810 | |||
| 0fce1e521c | |||
| 71fcf5e251 | |||
| 979e71fbea | |||
| 45e0b0bd95 | |||
| 70eb3fc13f | |||
| b30aa352f6 | |||
| 67cc33cfee | |||
| 6eec3b9ec7 | |||
| 57773b98ec | |||
| fe9b899a0e | |||
| abe9e663c1 | |||
| f5a33eb58c | |||
| a85090c3c6 | |||
| 97d48d8371 | |||
| 5020137050 | |||
| cf05ab2a9e | |||
| a6416a3f1d | |||
| 47ced228de | |||
| b8beeba98d | |||
| df6994c957 | |||
| d74e86c065 | |||
| 76512f6048 | |||
| 93a46d4de7 | |||
| 3ba24a0faf | |||
| 6926bb9528 | |||
| b1643309f6 | |||
| 43917a0051 | |||
| 1fac71e3ef | |||
| 747cc234c1 | |||
| fe72f0def2 | |||
| 884ecbba64 | |||
| 580d80a86e | |||
| 0e2ca5a7c3 | |||
| 4d2b8b9be3 | |||
| 0b31abe1d1 | |||
| 6b652cb0a2 | |||
| 3507c5714d | |||
| 4f78b9a875 | |||
| f3aebafcc7 | |||
| 1df0bc4f00 | |||
| 7d5d0cff06 | |||
| 8d752cb0e4 | |||
| 96e80ab293 | |||
| 49c5185ae3 | |||
| 181bd94341 | |||
| 09675a9f7f | |||
| 56c364e4c9 | |||
| 3021a4e761 | |||
| d240a61157 | |||
| d7c5307045 | |||
| 838d76f95e | |||
| 9791eee67b | |||
| 5d119f5555 | |||
| 46e5f612c8 | |||
| 41494bd18f | |||
| 40ebf7c1e7 | |||
| c7609252d2 | |||
| 4cf388dd3b | |||
| 00d85aa6e4 | |||
| d9e80a774a | |||
| a82407c686 | |||
| 805d754dc8 | |||
| d4c3811665 | |||
| b25761ea31 | |||
| 1c396dd562 | |||
| fc29c2eb9b | |||
| b0d5e04bb9 | |||
| 6746c75302 | |||
| b2697b13dc | |||
| e8ee6c34e7 | |||
| f42279f869 | |||
| 54ad9e8f79 | |||
| 6f16a231fc | |||
| 626c972232 | |||
| 44fcf24d92 | |||
| e187cd49a2 | |||
| 1631d3b1a2 | |||
| c71c2a8d33 | |||
| a51947562c | |||
| 63f02c4fb1 | |||
| 4fd5ee2608 | |||
| c7d4b9f753 | |||
| 37c2e5d7ee | |||
| 7dbd878398 | |||
| c2e2e0d8f2 | |||
| afc909fd3b | |||
| b607ff28cb | |||
| cf86d4e425 | |||
| 85761390a8 | |||
| 4d30d8b3e8 | |||
| 0e30980632 | |||
| 46a81e7a07 | |||
| e09344490f | |||
| 80a8bc4520 | |||
| b50e0359f7 | |||
| 58a300c7f4 | |||
| eb8311a5ee | |||
| 550a59d12c | |||
| 8c1d1ed958 | |||
| fc00b0c6f9 | |||
| 5276dd2066 | |||
| 88da16a11e | |||
| 3ef3bc32ec | |||
| 36fe1caa3f | |||
| f55c401b6c | |||
| b8037b9b22 | |||
| c0a3f68ded | |||
| 04c5043aba | |||
| 0b66662525 | |||
| cdea85e214 | |||
| 05f6147433 | |||
| 8f3b659c33 | |||
| c9ae914910 | |||
| 84e81f2037 | |||
| 696386a9c1 | |||
| aa2679162d | |||
| 096d1c882f | |||
| ca63095786 | |||
| c65e9c54ce | |||
| b1bc7a764e | |||
| d7c0a5521d | |||
| 2ae90b1ea9 | |||
| cfb0435d96 | |||
| fc411bb6f1 | |||
| 5bc39e902d | |||
| e2c4ca6d56 | |||
| 288d990821 | |||
| c03702707b | |||
| 89f77470f3 | |||
| 0d576a14b7 | |||
| c5807c07a9 | |||
| 8f5f54833f | |||
| f451406058 | |||
| 8aab2c0d41 | |||
| ed0ccb8c2b | |||
| b40adac3fc | |||
| bfb7876ed4 | |||
| 55b4e54d5f | |||
| fafa2f1858 | |||
| c84fd14cac | |||
| 23374312bc | |||
| a84a72e0c0 | |||
| 6c102f00c0 | |||
| 48aa955212 | |||
| cf3ddde3a6 | |||
| 468b006008 | |||
| e091921b18 | |||
| a9eb54ae9c | |||
| cf961603fe | |||
| cc3ecddc06 | |||
| a9a816e0ed | |||
| 3153a93edf | |||
| f64106ed8b | |||
| 9faaf12dd7 | |||
| 83539e33ec | |||
| 695421cfd4 | |||
| 8cba70dc90 | |||
| 622a774f6a | |||
| cc694774a3 | |||
| 3d3239bab9 | |||
| a7e3e43ae9 | |||
| 31fc590b99 | |||
| 245c22161b | |||
| 43557989f5 | |||
| a3fd51692f | |||
| e578443d73 | |||
| e3092ca0f9 | |||
| 21a81ad63c | |||
| 3f3d5704a7 | |||
| 89da7b7609 | |||
| 1605e6effd | |||
| afdbb364ca | |||
| d0dbfa99c0 | |||
| 897259c8fb | |||
| 9348b83374 | |||
| ccf912c157 | |||
| f5002a0d4d | |||
| 3b58b29094 | |||
| cef1e8d1f4 | |||
| 214be11202 | |||
| c41b1514d3 | |||
| 550313362c | |||
| 122b7b94d2 | |||
| ccb736fa71 | |||
| 2f8a6512db | |||
| 0f83d73364 | |||
| 456ec24360 | |||
| 22ba361578 | |||
| 21b2263e30 | |||
| 0e4bc13db4 | |||
| 9b22435421 | |||
| 9feb577c1c | |||
| 340c31f34e | |||
| d8ef9d4c4f | |||
| 9876a6c870 | |||
| 7b9a4fde15 | |||
| d17cf684ea | |||
| 9a0a76c9f4 | |||
| 49733f79d1 | |||
| c76cf706ef | |||
| b996c0673d | |||
| 1b1c178c09 | |||
| c28cad281b | |||
| 0a1c9dc2ce | |||
| e9e6f203c2 | |||
| b0bbc2a07f | |||
| 87e5cd2e53 | |||
| 132a013b39 | |||
| a3fd9e870f | |||
| 7ad7aa0afa | |||
| 4c5cc3ab4b | |||
| ffb0e6033c | |||
| a7ac5a09fd | |||
| f65c197942 | |||
| 9ee21afb56 | |||
| d2bbb4d378 | |||
| 1e2b073e04 | |||
| b434a3efdc | |||
| 285d4f6cb7 | |||
| 1e9cfb6da9 | |||
| 9cf21202dc | |||
| 397d46f174 | |||
| 24bbc32da5 | |||
| 0968cd351b | |||
| 224874d1d3 | |||
| 38b10e5ec5 | |||
| b7943aead3 | |||
| dd8dddd7af | |||
| e5bfe58fc8 | |||
| 5b139864ee | |||
| 18d864908e | |||
| f488793b34 | |||
| a93a8bcdd7 | |||
| 5d5198f5d1 | |||
| 59934b4ef0 | |||
| 6270c37208 | |||
| f9ffcfdf20 | |||
| 69ef101efb | |||
| 0081dfc8d4 | |||
| f38231d89d | |||
| b05ccd7ce5 | |||
| f7f19aeed3 | |||
| 7a243aa795 | |||
| 629813b580 | |||
| 4e89cce37d | |||
| c7759a52c4 | |||
| 89dc9baa61 | |||
| 1b2319635d | |||
| 5c43dbe39e | |||
| bebe573d05 | |||
| 2083a8821e | |||
| 6ddd5e9d47 |
@@ -30,3 +30,9 @@ Thumbs.db
|
|||||||
|
|
||||||
# Frozen baseline archives / inspects / manifests
|
# Frozen baseline archives / inspects / manifests
|
||||||
/docker-backups/
|
/docker-backups/
|
||||||
|
gate-evidence/
|
||||||
|
|
||||||
|
# Raw Fire2 frame captures — forensic evidence, may contain session material.
|
||||||
|
# Sanitize with `blaze-sanitize` before anything leaves this machine.
|
||||||
|
*.ofcap
|
||||||
|
captures/
|
||||||
|
|||||||
Generated
+6639
File diff suppressed because it is too large
Load Diff
+31
@@ -0,0 +1,31 @@
|
|||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [
|
||||||
|
"openfut-core",
|
||||||
|
"openfut-protocol-blaze",
|
||||||
|
"openfut-adapter-fifa17",
|
||||||
|
"openfut-blaze-host",
|
||||||
|
"openfut-host-config",
|
||||||
|
"openfut-http",
|
||||||
|
"openfut-tls",
|
||||||
|
"openfut-redirector-host",
|
||||||
|
"openfut-roster-host",
|
||||||
|
"openfut-utas-host",
|
||||||
|
"openfut-identity",
|
||||||
|
"openfut-import-fifa17",
|
||||||
|
"openfut-bridge",
|
||||||
|
"openfut-launcher",
|
||||||
|
# The two companion services the launcher used to shell out to Python for.
|
||||||
|
"openfut-lsx",
|
||||||
|
"openfut-autopatch",
|
||||||
|
"fifa-blaze/crates/blaze-proto",
|
||||||
|
"fifa-blaze/crates/server",
|
||||||
|
]
|
||||||
|
# openfut-hook is a Windows-only version.dll proxy injected into the FIFA client.
|
||||||
|
# It MUST build with its own [profile.release] (panic="abort" — unwinding across
|
||||||
|
# the DllMain/FFI boundary into the game process is UB — plus strip + opt-level="s").
|
||||||
|
# Cargo ignores a non-root member's profile and forbids per-package `panic` overrides,
|
||||||
|
# so the hook is deliberately EXCLUDED from this workspace to build as its own root
|
||||||
|
# (this also lands its artifact in openfut-hook/target/, matching the launcher's
|
||||||
|
# config.rs default hook_dll_path). Build: cargo build --release --target x86_64-pc-windows-gnu.
|
||||||
|
exclude = ["openfut-launcher/openfut-hook"]
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
# OpenFUT — project handoff
|
||||||
|
|
||||||
|
Self-contained briefing for an assistant with **no access to this repo or machine**.
|
||||||
|
Everything needed to understand the project and reason about its open problems is here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What this is
|
||||||
|
|
||||||
|
**OpenFUT** is a clean-room, fully offline re-implementation of the server backend for
|
||||||
|
**FIFA 17 Ultimate Team (FUT)**. EA's servers for FIFA 17 are long dead. The goal is to
|
||||||
|
make the retail game's FUT mode fully playable again — open packs, build squads, use the
|
||||||
|
transfer market, play matches and earn rewards — by emulating every server the client
|
||||||
|
talks to, on localhost.
|
||||||
|
|
||||||
|
Nothing is decompiled *into* the project. The game's binaries are read to learn the
|
||||||
|
**wire format** (which JSON keys, of which types, each response must contain), and the
|
||||||
|
servers are written from scratch in Python against that spec.
|
||||||
|
|
||||||
|
The client is unmodified retail FIFA 17 running under Wine/Proton on Linux.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Architecture — four independent servers
|
||||||
|
|
||||||
|
FIFA 17 does not talk to one backend. It talks to four, on different protocols, and all
|
||||||
|
four must be satisfied in sequence before FUT loads.
|
||||||
|
|
||||||
|
```
|
||||||
|
FIFA 17 (Wine/Proton)
|
||||||
|
│
|
||||||
|
├─ LSX / Origin :4216 XML over TCP. Local Origin client emulation.
|
||||||
|
│ Login, entitlements, persona.
|
||||||
|
├─ Blaze :42127 redirector (TLS) → :42130 game server, :42131 nucleus
|
||||||
|
│ EA's binary "Fire2/TDF" RPC protocol. Session, auth,
|
||||||
|
│ and — critically — the CLIENT-CONFIG STORE.
|
||||||
|
├─ UTAS / RS4 :8099 The FUT REST API. JSON over HTTP. ~45 endpoints.
|
||||||
|
│ Club, squads, packs, market, matches. The bulk of it.
|
||||||
|
└─ POW / EASFC :8094 (+ :8080 content) A third HTTP API, discovered late.
|
||||||
|
Online status bar, level, EASFC credits, catalogue.
|
||||||
|
```
|
||||||
|
|
||||||
|
Plus two helpers: a **roster** server (:8081) serving a roster-update XML the FUT
|
||||||
|
loading screen blocks on, and **autopatch**, which patches the running process's
|
||||||
|
ProtoSSL certificate verification so the client accepts our self-signed TLS.
|
||||||
|
|
||||||
|
### How the client is redirected
|
||||||
|
|
||||||
|
Three mechanisms, in decreasing order of preference:
|
||||||
|
|
||||||
|
1. **Blaze client-config keys** — the cleanest. Blaze serves a key/value config store
|
||||||
|
and the client reads its own service URLs from it. `FUT_RS4_APIURL_<MODULE>` and
|
||||||
|
`FUT_RS4_URL_<CALL>` point the FUT API at `127.0.0.1:8099`; `FIFA_POW_URL` points
|
||||||
|
the EASFC layer at `127.0.0.1:8094`. **No root, no DNS games.**
|
||||||
|
2. **`/etc/hosts`** — for hosts baked into the binary (`easw.easports.com`,
|
||||||
|
`gosredirector.ea.com`).
|
||||||
|
3. **iptables DNAT** — for a hardcoded IP (`159.153.51.20` → the Blaze redirector).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. The wire format, and why it is unforgiving
|
||||||
|
|
||||||
|
FUT responses are JSON, but the client does **not** use a general JSON object model. Each
|
||||||
|
response class has a hand-written SAX-style deserializer that walks tokens and dispatches
|
||||||
|
on a **hashed key id** (an "atom"). This has three consequences that dominate the project:
|
||||||
|
|
||||||
|
**Atoms.** Every JSON key name maps to a 16-bit atom id via FNV-1a. There is a recovered
|
||||||
|
table of ~900 (id → name). A response is really "which atoms does deserializer X read,
|
||||||
|
and of what type".
|
||||||
|
|
||||||
|
**Type fidelity is fatal.** Feeding a scalar where the parser expects an object or array
|
||||||
|
does not error — it **desyncs the token reader and the game hard-freezes** in a busy loop
|
||||||
|
at `0x1801c7f1a`. This is the single most common way to break the game, and it has bitten
|
||||||
|
this project repeatedly. Arrays must be arrays; nested objects must be objects.
|
||||||
|
|
||||||
|
**Unknown keys are usually skipped safely** — most deserializers route an unrecognised
|
||||||
|
atom to a value-skip handler (`FUN_180135ff0`), so extra fields are inert. This document
|
||||||
|
previously named `FutMoveCard` (`0x180128600`) as an exception with no skip handler at
|
||||||
|
all. **That was wrong**, and the retraction is in §5a: it has two skip-handler call sites
|
||||||
|
and parses seven atoms. No deserializer in this project is currently known to lack one.
|
||||||
|
Treat any future "this class has no skip handler" claim as unproven until the search is
|
||||||
|
shown to have covered the whole function.
|
||||||
|
|
||||||
|
### Working method
|
||||||
|
|
||||||
|
For any endpoint: find the response class's deserializer, extract the atom ids it
|
||||||
|
compares against, map them to names, note the getter used for each (int / string / bool /
|
||||||
|
nested), and build the minimal body. Omit nested members unless their shape is known —
|
||||||
|
omission is skip-safe, a wrong shape freezes the game.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. What works today (live-verified)
|
||||||
|
|
||||||
|
| Area | Status |
|
||||||
|
|---|---|
|
||||||
|
| Boot to the FUT hub | ✅ |
|
||||||
|
| Club identity, coins, W/D/L record | ✅ |
|
||||||
|
| Active squad — renders 11 real players, chemistry links | ✅ |
|
||||||
|
| Squad building — `PUT /squad/<id>` fires, persists across relaunch | ✅ |
|
||||||
|
| Squad roster ("MY SQUADS") | ✅ |
|
||||||
|
| Transfer market — browse, bid, buy-now, sell, watchlist | ✅ |
|
||||||
|
| Packs — buy, reveal, Send to Club | ✅ (the workaround is retired, see §5a) |
|
||||||
|
| Quick sell — destroys cards, credits coins | ✅ |
|
||||||
|
| Match loop — create/ready/play/destroy + coin rewards | ✅ implemented, **never played in-game** |
|
||||||
|
| Online/EASFC status bar (no "servers unreachable") | ✅ when enabled |
|
||||||
|
| Seasons / tournaments / leaderboards / champions | ⚠️ routed with reversed schemas, never live-tested |
|
||||||
|
|
||||||
|
Current save state: 99 club items, 8,400 coins, 0-0-0 record.
|
||||||
|
|
||||||
|
**Design convention:** every risky change ships behind an environment flag, default set
|
||||||
|
to whatever is live-proven (`FUT_MASSINFO`, `FUT_USERINFO`, `FUT_MODES`,
|
||||||
|
`FUT_PACK_AUTOCLUB`, `FUT_POW`, `FUT_STORE_GROUPS`, …). This exists because two working
|
||||||
|
screens were broken by shipping "corrections" on by default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Open problems
|
||||||
|
|
||||||
|
### 5a. "Send to Club" kills the FUT session — SOLVED 2026-08-04
|
||||||
|
|
||||||
|
Opening a pack shows the cards correctly; choosing **Send to Club** used to produce
|
||||||
|
*"there has been an error connecting to FIFA 17 Ultimate Team"* and a logout, seven
|
||||||
|
attempts running. The cards always moved server-side; only the acknowledgement was
|
||||||
|
rejected.
|
||||||
|
|
||||||
|
The cause was the response body. `PUT ut/%s/item` returns per-item **verdict** records,
|
||||||
|
not an acknowledgement, and the completion handler raises
|
||||||
|
`EVENT_CARDS_MOVE_CARD_FAILURE` when the record vector is empty or when `success != 1`.
|
||||||
|
Every body the project returned, `{}` included, reported the move as failed. The fix is
|
||||||
|
the real shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"itemData":[{"id":100000125,"pile":"club","success":true}, ...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Live: five cards to the club, session survived, cards persisted, no `ut/delete/auth`.
|
||||||
|
The autoclub workaround is retired.
|
||||||
|
|
||||||
|
Two corrections this closed, both worth carrying forward:
|
||||||
|
|
||||||
|
- The repo's claim that this deserializer had **no skip handler** and parsed only two
|
||||||
|
keys was false. It came from searching a truncated decompile. It implied the body
|
||||||
|
could not be at fault, which is what sent seven attempts after client-side state.
|
||||||
|
Never conclude an absence from a truncated or unverified-length extraction.
|
||||||
|
- The quick-sell asymmetry was not evidence of client state. Quick sell's callbacks
|
||||||
|
read only the transport status code and never touch the body.
|
||||||
|
|
||||||
|
### 5b. "MY CLUB" counter always reads 0 — UNSOLVED
|
||||||
|
|
||||||
|
The hub tab bar shows `MY CLUB 0` despite the club holding 99 items.
|
||||||
|
|
||||||
|
**Eliminated by live test:**
|
||||||
|
- **Not the item list** — the user opened MY CLUB, the client fetched the club and
|
||||||
|
**displayed all 99 players correctly**, and the counter still read 0.
|
||||||
|
- **Not `pileSizeClientData`** — the massinfo member that carries pile sizes as
|
||||||
|
`{"entries":[{"key":int,"value":int}]}`. A probe sent 16 entries with uniquely
|
||||||
|
identifiable values; the counter stayed 0.
|
||||||
|
- **Not lazy loading** — the club endpoint was fetched 6 times that session.
|
||||||
|
|
||||||
|
**Unexplored contrast:** the `ACTIVE SQUAD` tab in the same bar correctly shows `11/23`.
|
||||||
|
So some counters work. Whatever differs between that one and the club one is likely the
|
||||||
|
answer.
|
||||||
|
|
||||||
|
### 5c. Store tiles render "unknown" — DIAGNOSED, NOT FIXED
|
||||||
|
|
||||||
|
Pack tiles show `unknown` with zero item counts. The `"unknown"` string is an
|
||||||
|
unconditional **default** in a string constructor — the field simply never gets written.
|
||||||
|
The store renders *display groups*, and `displayGroup` is parsed **recursively by the same
|
||||||
|
element parser**. Sending it populated **froze the store** (the type-desync busy loop), so
|
||||||
|
it is behind a flag, default off. Doing it properly needs the group's own field set worked
|
||||||
|
out rather than a self-referential copy of the pack.
|
||||||
|
|
||||||
|
### 5d. Seasons and Draft refuse — UNSOLVED, and not obviously server-side
|
||||||
|
|
||||||
|
Selecting **single-player Seasons** raises *"There was a problem communicating with the
|
||||||
|
FIFA Ultimate Team servers"* while making **zero requests to any layer**. UTAS, Blaze and
|
||||||
|
POW logs show only pings and one census subscription across the whole failure window. No
|
||||||
|
response can be wrong because no request was made. POW is eliminated (same failure with it
|
||||||
|
enabled and disabled).
|
||||||
|
|
||||||
|
**Online Draft** hangs the client rather than crashing it (process alive, no dump). The
|
||||||
|
one suspicious thing on the wire is `GET ut/%s/squad/mode/draft/state`, which our generic
|
||||||
|
`/squad` route answers with a full active-squad object: 23 slots, nested `itemData`, a
|
||||||
|
33-integer formation string. The real class wants `roundsInfo` plus a state enum, so this
|
||||||
|
is a textbook type-desync candidate and the timing matches. **Nothing has isolated it**;
|
||||||
|
it is a suspect, not a cause.
|
||||||
|
|
||||||
|
Both matter beyond themselves, because they are the only two routes into a match, and the
|
||||||
|
`/match` request shape has therefore never been captured.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Notable reverse-engineering findings
|
||||||
|
|
||||||
|
- **Class → deserializer resolution.** A response class's name literal is preceded by a
|
||||||
|
**4-byte header**, and the constructing factory's `lea` points at *the header*, not the
|
||||||
|
text. Lookups must use `name_address - 4`. Six attempts failed on this off-by-four;
|
||||||
|
four of them returned zero results and nearly got recorded as "this class has no
|
||||||
|
deserializer".
|
||||||
|
- **The request-template table is a floor, not a ceiling.** Several real endpoints are
|
||||||
|
built by the caller appending a suffix and therefore never appear in the binary's URL
|
||||||
|
table: `squad/list`, `user/club`, `club/stats/*`, `clientdata/<key>`. Only live traffic
|
||||||
|
reveals them. This has caught the project three separate times.
|
||||||
|
- **The documentation lies.** The project's own `ENDPOINT_MAP.md` (~1,360 lines, ~100
|
||||||
|
reversed structs) has been wrong repeatedly: it claimed `FutMoveCard` parses
|
||||||
|
`chemistry` (it does not); it called seven store pack fields "skipped no-ops" (all are
|
||||||
|
parsed); it gave price-object keys as `amount`/`currency` (the parsers read
|
||||||
|
`externalPriceId`). **Verify against the decompiler before relying on any row.**
|
||||||
|
- **The online layer was hiding in an unpacked DLL.** "EA FC servers are unreachable"
|
||||||
|
comes from a third HTTP API implemented in a *loose, unpacked, string-rich* library —
|
||||||
|
not the packed executable, and not any layer previously emulated. It is redirectable
|
||||||
|
purely through the Blaze config store.
|
||||||
|
- **The main executable is Denuvo-packed.** Its code exists only in a live process. Live
|
||||||
|
memory is readable via `/proc/PID/mem` (the PE is mapped flat), which is how a crash
|
||||||
|
site was disassembled. Any logic living there cannot be reversed statically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Tooling built
|
||||||
|
|
||||||
|
- A **PyGhidra harness** with helpers for decompiling, xrefs, vtables, byte scanning and
|
||||||
|
class→deserializer resolution. (Ghidra's Java/OSGi scripting is broken on this machine;
|
||||||
|
PyGhidra bypasses it entirely.)
|
||||||
|
- A **minidump reader** — exception record, fault-time registers, module map, and a stack
|
||||||
|
walk that recovers a usable backtrace.
|
||||||
|
- A **live code grabber** that reads and disassembles unpacked code out of a running
|
||||||
|
process.
|
||||||
|
- A **read-only live probe** pattern for polling client model state while playing.
|
||||||
|
- **Two test suites**: 380 live contract checks (freeze-safety: asserts every response
|
||||||
|
field's type against the reversed schema) and 51 pure unit checks for match rewards.
|
||||||
|
- A **traffic-replay audit** that diffs current responses against a known-good session —
|
||||||
|
this is what proves a change did not alter what the client sees.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Documentation in-repo
|
||||||
|
|
||||||
|
| File | Contents |
|
||||||
|
|---|---|
|
||||||
|
| `ENDPOINT_MAP.md` | ~100 reversed response structs, atoms, types, freeze risks |
|
||||||
|
| `FUT_RESPONSE_REBUILD_PLAN.md` | Squad family, massinfo, the service-layer call graph |
|
||||||
|
| `REBUILD_RESEARCH.md` | Complete API surface, gap analysis, POW layer, and every eliminated hypothesis with its evidence |
|
||||||
|
| `CARD_SYSTEM.md` | How card identity resolves locally from the game's own database |
|
||||||
|
| `REPACK_INTEL.md` | Origin/LSX emulation notes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Where help would be most valuable
|
||||||
|
|
||||||
|
1. **The MY CLUB counter (§5b).** Given the client demonstrably *has* the items and
|
||||||
|
*renders* them, what else could a tab counter read from? Note that a sibling counter in
|
||||||
|
the same bar works correctly.
|
||||||
|
2. **Seasons refusing with zero requests to any server (§5d).** The client raises a
|
||||||
|
"problem communicating with the FIFA Ultimate Team servers" without contacting
|
||||||
|
anything. Nothing on the wire can be wrong because nothing went on the wire.
|
||||||
|
3. **Whether the remaining problems are fixable server-side at all**, or whether the
|
||||||
|
deciding logic lives in the Denuvo-packed executable and only live instrumentation can
|
||||||
|
settle it. Note that this question was asked about `Send to Club` too, and there the
|
||||||
|
answer turned out to be a plain wire fix, so treat "it must be client-side" as a
|
||||||
|
hypothesis needing evidence rather than a fallback explanation.
|
||||||
|
|
||||||
|
Useful framing: this project's failures have almost always come from proposing a fix
|
||||||
|
before testing the assumption under it. Hypotheses that come with a cheap way to
|
||||||
|
disconfirm them are worth far more than plausible ones.
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
# OpenFUT — Project Report
|
||||||
|
|
||||||
|
**Goal:** make FIFA 17 Ultimate Team fully playable offline, forever, by re-implementing
|
||||||
|
every server the game talks to.
|
||||||
|
|
||||||
|
**Status:** FUT boots, loads, and is playable. Packs, squads, the transfer market, coins
|
||||||
|
and progression all work. Two cosmetic/flow problems remain open.
|
||||||
|
|
||||||
|
**Timeline:** 2026-06-25 → 2026-08-04 · 44 commits · ~12,900 lines of Python across 39
|
||||||
|
tools · ~3,600 lines of reverse-engineering documentation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What the project is
|
||||||
|
|
||||||
|
EA shut down FIFA 17's servers years ago, which kills Ultimate Team — the mode is entirely
|
||||||
|
server-driven. Your club, squads, packs, market and progression all live server-side, so
|
||||||
|
without a backend the mode is dead even though the game still installs and runs.
|
||||||
|
|
||||||
|
OpenFUT replaces that backend with local servers. The game is **unmodified retail
|
||||||
|
FIFA 17** running under Wine/Proton on Linux; nothing is patched into the game except a
|
||||||
|
single runtime tweak so it accepts our TLS certificate.
|
||||||
|
|
||||||
|
This is **clean-room work**. No EA code is copied or redistributed. The game's own
|
||||||
|
binaries are read to learn the *wire format* — which JSON keys, of which types, each
|
||||||
|
response must carry — and the servers are written from scratch against that specification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Project history
|
||||||
|
|
||||||
|
The project changed target twice before finding its footing. That arc matters, because
|
||||||
|
each pivot was driven by hitting a hard wall.
|
||||||
|
|
||||||
|
**Phase 1 — FIFA 23 (June 2026).** Began as an offline FUT backend for FIFA 23: a Rust
|
||||||
|
core (`openfut-core`, Axum + SQLite), a protocol bridge (`openfut-bridge`), and a GUI
|
||||||
|
launcher. All three built and passed tests. The architecture was sound but the client
|
||||||
|
never got far enough to exercise it.
|
||||||
|
|
||||||
|
**Phase 2 — the FIFA 23 wall.** FIFA 23 refused to go online at all. Extensive reverse
|
||||||
|
engineering of the connection state machine, live-memory probing, and forcing the
|
||||||
|
"go online" gate directly all failed — the client's internal coherence checks were the
|
||||||
|
wall, not any single flag. Documented as a dead end rather than fought.
|
||||||
|
|
||||||
|
**Phase 3 — the FIFA 17 pivot (late July).** FIFA 17 turned out to be a far better target:
|
||||||
|
its network library is **unprotected and fully symboled**, exposing 979 RPC names. The
|
||||||
|
insight was to crack FIFA 17 first and port the understanding back.
|
||||||
|
|
||||||
|
That worked, quickly:
|
||||||
|
- **TLS pinning defeated** — the client's certificate verification is patched at runtime
|
||||||
|
in memory, so a self-signed cert is accepted.
|
||||||
|
- **Origin/LSX emulation** — the local Origin client protocol was reverse engineered,
|
||||||
|
including the repack's own crypto layer, beating "log in to Origin" and
|
||||||
|
"title version outdated".
|
||||||
|
- **Blaze cracked end to end** — EA's binary RPC protocol: redirector, second-hop
|
||||||
|
handshake, and the encoded pre-auth exchange. This was the big one.
|
||||||
|
- **Full FUT API mapped** — ~100 response structures reverse engineered to field level.
|
||||||
|
|
||||||
|
**Phase 4 — building the FUT backend (August).** With the protocol understood, the work
|
||||||
|
became making FUT actually *play*: card rendering, packs, the store, the transfer market,
|
||||||
|
squads, and match rewards. This is where the project stands.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Architecture
|
||||||
|
|
||||||
|
FIFA 17 does not talk to one backend. It talks to **four**, on different protocols, and
|
||||||
|
all four must be satisfied in sequence before FUT loads.
|
||||||
|
|
||||||
|
```
|
||||||
|
FIFA 17 (Wine/Proton)
|
||||||
|
│
|
||||||
|
├─ LSX / Origin :4216 XML/TCP — local Origin client emulation
|
||||||
|
│ login, entitlements, persona
|
||||||
|
├─ Blaze :42127 redirector (TLS) → :42130 game, :42131 nucleus
|
||||||
|
│ EA's binary Fire2/TDF RPC
|
||||||
|
│ session, auth, and the CLIENT-CONFIG STORE
|
||||||
|
├─ UTAS / RS4 :8099 the FUT REST API — JSON/HTTP, ~45 endpoints
|
||||||
|
│ club, squads, packs, market, matches
|
||||||
|
└─ POW / EASFC :8094 a third HTTP API (+ :8080 content)
|
||||||
|
online status, level, credits, catalogue
|
||||||
|
```
|
||||||
|
|
||||||
|
Plus **roster** (:8081), serving an XML file the FUT loading screen blocks on, and
|
||||||
|
**autopatch**, which patches certificate verification in the running process.
|
||||||
|
|
||||||
|
### Redirection, in order of preference
|
||||||
|
|
||||||
|
1. **Blaze client-config keys** — the client reads its own service URLs from a key/value
|
||||||
|
store that Blaze serves. Pointing FUT and EASFC at localhost needs **no root and no
|
||||||
|
DNS manipulation**. This is the clean mechanism and most redirection uses it.
|
||||||
|
2. **`/etc/hosts`** — for hostnames baked into the binary.
|
||||||
|
3. **iptables DNAT** — for one hardcoded IP address.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. The wire format — and why it is unforgiving
|
||||||
|
|
||||||
|
FUT responses are JSON, but the client does not use a general JSON object model. Each
|
||||||
|
response class has a hand-written SAX-style deserializer that walks tokens and dispatches
|
||||||
|
on a **hashed key id** ("atom"). Three consequences dominate the project:
|
||||||
|
|
||||||
|
**Atoms.** Every JSON key maps to a 16-bit id via FNV-1a. A recovered table of ~900
|
||||||
|
id→name pairs is the Rosetta stone. A response spec is really "which atoms does this
|
||||||
|
deserializer read, of what type".
|
||||||
|
|
||||||
|
**Type fidelity is fatal.** A scalar where an object or array is expected does not error —
|
||||||
|
it **desyncs the reader and hard-freezes the game** in a busy loop. This is the primary
|
||||||
|
failure mode of the entire project.
|
||||||
|
|
||||||
|
**Unknown keys are usually skipped — but not always.** Most deserializers route
|
||||||
|
unrecognised atoms to a skip handler, making extra fields inert. At least one does not,
|
||||||
|
so any unexpected key desyncs it.
|
||||||
|
|
||||||
|
**Working method:** locate the deserializer, extract its atom set and per-atom getter
|
||||||
|
types, build the minimal body, and omit nested members whose shape isn't known — omission
|
||||||
|
is safe, a wrong shape freezes the game.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. What works
|
||||||
|
|
||||||
|
| Capability | State |
|
||||||
|
|---|---|
|
||||||
|
| Boot: Origin → Blaze → FUT hub | ✅ |
|
||||||
|
| Club identity, coins, W/D/L record | ✅ |
|
||||||
|
| Active squad — 11 real players, ratings, chemistry | ✅ |
|
||||||
|
| Squad building — saves and survives relaunch | ✅ |
|
||||||
|
| Squad roster ("MY SQUADS") | ✅ |
|
||||||
|
| Transfer market — browse, bid, buy-now, list, watchlist | ✅ |
|
||||||
|
| Store — buy packs | ✅ |
|
||||||
|
| Packs — cards land in the club | ✅ via workaround (§6a) |
|
||||||
|
| Quick sell — credits coins | ✅ |
|
||||||
|
| Match loop — create/ready/play/destroy + rewards | ⚠️ built, **never requested by the client** (see below) |
|
||||||
|
| Online/EASFC status bar | ✅ when enabled |
|
||||||
|
| Seasons, tournaments, leaderboards, champions | ⚠️ routed, **never requested by the client** (see below) |
|
||||||
|
| Card identity (names, faces, ratings) | ✅ resolves from the game's own local database |
|
||||||
|
|
||||||
|
### "Untested" is two different things, and the difference matters
|
||||||
|
|
||||||
|
The server log records the User-Agent of every request. The real client identifies as
|
||||||
|
`ProtoHttp`; this project's own curl and Python probes do not. Separating them shows that
|
||||||
|
several endpoints previously filed as "built but untested" have in fact **never been
|
||||||
|
requested by the game at all**, and everything recorded against them was self-inflicted
|
||||||
|
traffic:
|
||||||
|
|
||||||
|
| endpoint | client requests | project probes |
|
||||||
|
|---|---|---|
|
||||||
|
| `/leaderboards/options` | 5 | 1 |
|
||||||
|
| `/clientdata/userHubData` | 13 | 2 |
|
||||||
|
| `/user/accountinfo` | 23 | 49 |
|
||||||
|
| `/season`, `/season/user` | **0** | 2 |
|
||||||
|
| `/tournament`, `/tournament/user` | **0** | 3 |
|
||||||
|
| `/leaderboards` (bare) | **0** | 2 |
|
||||||
|
| `/champion` | **0** | 2 |
|
||||||
|
| `/match` | **0** | 2 |
|
||||||
|
| `/clubUser` | **0** | 93 |
|
||||||
|
| `/user/list` | **0** | 180 |
|
||||||
|
| `/sbs` (SBC), `/draft/mode` | **0** | 0 |
|
||||||
|
|
||||||
|
`/clubUser` and `/user/list` are the starkest: 273 requests between them, none from the
|
||||||
|
game. Work was done on both on the assumption the client wanted them.
|
||||||
|
|
||||||
|
A zero in the client column does **not** mean the client never wants that endpoint. In
|
||||||
|
most cases it means **nobody has navigated to that part of the game yet**. It does mean no
|
||||||
|
claim about those endpoints has been tested against the client, and any analysis that does
|
||||||
|
not apply this filter is misleading by default.
|
||||||
|
|
||||||
|
**Requirement:** every capture and analysis tool in this project should apply the
|
||||||
|
User-Agent split by default rather than as an afterthought.
|
||||||
|
|
||||||
|
**Design convention.** Every risky change ships behind an environment flag whose default
|
||||||
|
is whatever is live-proven. This exists because shipping "corrections" on by default broke
|
||||||
|
two working screens — once freezing the store outright.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Open problems
|
||||||
|
|
||||||
|
### 6a. "Send to Club" ends the FUT session — SOLVED 2026-08-04
|
||||||
|
|
||||||
|
Opening a pack displayed the cards correctly, but choosing **Send to Club** produced
|
||||||
|
*"there has been an error connecting to FIFA 17 Ultimate Team"* and a logout, seven
|
||||||
|
attempts running. The cards always moved correctly server-side; only the
|
||||||
|
acknowledgement was rejected.
|
||||||
|
|
||||||
|
**It was the response body all along.** `PUT ut/%s/item` does not parse an
|
||||||
|
acknowledgement, it builds per-item **verdict** records, and the completion handler
|
||||||
|
raises `EVENT_CARDS_MOVE_CARD_FAILURE` when the record vector is empty or when
|
||||||
|
`success != 1`. Every body this project returned, `{}` included, therefore told the
|
||||||
|
client the move had failed, and the client ended the FUT session because that is what
|
||||||
|
that event does. Serving the real shape fixed it in one launch:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"itemData":[{"id":100000125,"pile":"club","success":true}, ...]}
|
||||||
|
```
|
||||||
|
|
||||||
|
Live result: five cards sent to the club, session survived, cards persisted, no
|
||||||
|
`ut/delete/auth` logout. Both flags are now defaults and the autoclub workaround is
|
||||||
|
retired.
|
||||||
|
|
||||||
|
**Why it took seven attempts,** which is the part worth keeping: the project had
|
||||||
|
recorded that this deserializer had *no skip handler* and parsed only two keys. That
|
||||||
|
was false, produced by searching a **truncated** decompile (the first 4,000 characters
|
||||||
|
of a 6,193-character function). It implied "the body cannot be the problem", which is
|
||||||
|
what redirected the investigation to client-side state. The quick-sell asymmetry that
|
||||||
|
seemed to confirm it has a mundane explanation: quick sell's callbacks read only the
|
||||||
|
transport status code and never touch the body, so its tolerance of `{}` said nothing
|
||||||
|
about this endpoint.
|
||||||
|
|
||||||
|
The general lesson, now a standing rule: **never conclude an absence from a truncated
|
||||||
|
or unverified-length extraction**, and treat every negative claim in the endpoint docs
|
||||||
|
as weaker than the corresponding positive one.
|
||||||
|
|
||||||
|
### 6b. "MY CLUB" counter reads 0
|
||||||
|
|
||||||
|
The hub shows `MY CLUB 0` despite 99 items. Not the item list (the client *displays* all
|
||||||
|
99), not the pile-size data (a 16-entry probe changed nothing), not lazy loading (fetched
|
||||||
|
6 times). Unexplored: the neighbouring `ACTIVE SQUAD` counter works correctly — the
|
||||||
|
difference between them is likely the answer.
|
||||||
|
|
||||||
|
### 6c. Store tiles read "unknown"
|
||||||
|
|
||||||
|
`"unknown"` is an unconditional default in a string constructor — the field is never
|
||||||
|
written. The store renders *display groups*, and that member is parsed recursively by the
|
||||||
|
same parser; sending it populated **froze the store**, so it is flagged off pending a
|
||||||
|
correct group schema.
|
||||||
|
|
||||||
|
### 6d. Large parts of the game have never been opened
|
||||||
|
|
||||||
|
Distinct from 6a to 6c, which are things that misbehave. Per the User-Agent table in
|
||||||
|
section 5, entire modes have never issued a single client request: Seasons, Tournaments,
|
||||||
|
FUT Champions, Draft, SBC, and the match loop itself. Their endpoints are routed and their
|
||||||
|
schemas are reversed, but no claim about any of them has been tested against the game.
|
||||||
|
|
||||||
|
This is not a bug list. It is unmeasured surface, and it is the cheapest information
|
||||||
|
available to the project because most of it costs nothing but navigating menus. It is
|
||||||
|
recorded here because "routed from a reversed schema" reads like a stronger claim than it
|
||||||
|
is, and section 7 warns that this project's notes have described things differently from
|
||||||
|
what is true.
|
||||||
|
|
||||||
|
*A multi-agent investigation into 6a and 6b is currently running.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Notable findings
|
||||||
|
|
||||||
|
- **Class → deserializer resolution.** A response class's name literal is preceded by a
|
||||||
|
4-byte header and the factory points at *the header*. Six attempts failed on that
|
||||||
|
off-by-four; four returned nothing and were nearly recorded as "no deserializer exists".
|
||||||
|
- **The URL table is a floor, not a ceiling.** Several real endpoints are built by
|
||||||
|
appending a suffix at the call site and never appear in the binary's template table.
|
||||||
|
Only live traffic reveals them — this caught the project three separate times.
|
||||||
|
- **The project's own documentation has been wrong repeatedly** — fields described as
|
||||||
|
inert turned out to be parsed, and documented key names didn't match the parsers.
|
||||||
|
Verify against the decompiler, not the notes.
|
||||||
|
- **The online layer was hiding in plain sight.** "EA FC servers unreachable" comes from a
|
||||||
|
third HTTP API in a *loose, unpacked, string-rich* library — not the protected
|
||||||
|
executable, and not any previously emulated layer. Redirectable purely by config.
|
||||||
|
- **The main executable is Denuvo-packed**, so its code exists only in a live process.
|
||||||
|
Live memory is readable, which is how a crash site was disassembled — but logic living
|
||||||
|
there cannot be reverse engineered statically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Tooling and quality
|
||||||
|
|
||||||
|
- **PyGhidra harness** with decompile / xref / vtable / byte-scan / class-resolution
|
||||||
|
helpers (Ghidra's own Java scripting is broken on this machine).
|
||||||
|
- **Minidump reader** — exception record, fault-time registers, module map, stack walk.
|
||||||
|
- **Live code grabber** — reads and disassembles unpacked code from a running process.
|
||||||
|
- **Read-only live model probes** for watching client state while playing.
|
||||||
|
- **Test suites:** 380 live contract checks (type/freeze safety per reversed schema) plus
|
||||||
|
51 pure unit checks. Both green.
|
||||||
|
- **Traffic-replay audit** — diffs current responses against a known-good session to prove
|
||||||
|
a change didn't alter what the client sees.
|
||||||
|
- **~3,600 lines of RE documentation** across six files, including every eliminated
|
||||||
|
hypothesis with its supporting evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Roadmap
|
||||||
|
|
||||||
|
**Immediate (free, no code):** play a match — the reward loop is built and unit-tested but
|
||||||
|
has never run in-game. Enable the game-mode endpoints and see whether four more modes
|
||||||
|
light up.
|
||||||
|
|
||||||
|
**Near term:** close the two open problems, most likely via live instrumentation rather
|
||||||
|
than more static analysis. Implement SBC and Draft (schemas already recovered). Fix the
|
||||||
|
store display groups properly.
|
||||||
|
|
||||||
|
**Longer term:** the stated destination is porting this into the Rust `openfut-core`
|
||||||
|
behind a FIFA-17 bridge. Everything currently lives in Python prototypes; the
|
||||||
|
documentation is now good enough to write the port against.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Honest assessment
|
||||||
|
|
||||||
|
**What went well.** The FIFA 17 pivot was the decisive call — recognising that an
|
||||||
|
unprotected binary was worth more than persisting against a hardened one. Blaze, Origin
|
||||||
|
and the FUT API were all cracked end to end. The freeze-safety test suite has repeatedly
|
||||||
|
caught regressions before they reached the game.
|
||||||
|
|
||||||
|
**What went badly.** Progress has been slowest where fixes were proposed before the
|
||||||
|
assumption under them was tested. Both open problems absorbed many attempts built on
|
||||||
|
plausible but unverified theories; several were disproved in a single measurement that
|
||||||
|
could have been taken first. Two working screens were broken by shipping unverified
|
||||||
|
"corrections" on by default — which is precisely why the flag convention exists now.
|
||||||
|
|
||||||
|
**The most reliable technique** has been comparing a working case against a failing one:
|
||||||
|
diffing live traffic against a known-good session, and contrasting a succeeding endpoint
|
||||||
|
with its failing sibling. That has produced more answers than any amount of decompilation.
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"reportDate": "2026-07-28",
|
||||||
|
"codebaseName": "OpenFUT",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"submodulesCovered": [
|
||||||
|
"openfut-core",
|
||||||
|
"openfut-bridge",
|
||||||
|
"openfut-launcher"
|
||||||
|
],
|
||||||
|
"language": "Rust",
|
||||||
|
"framework": "Axum + SQLite"
|
||||||
|
},
|
||||||
|
"vulnerabilities": [
|
||||||
|
{
|
||||||
|
"severity": "critical",
|
||||||
|
"category": "authentication",
|
||||||
|
"file": "openfut-core/src/services/profile.rs",
|
||||||
|
"line": 8,
|
||||||
|
"cwe": "CWE-287",
|
||||||
|
"title": "Missing Authentication on All Endpoints",
|
||||||
|
"description": "No authentication or authorization checks on any API endpoint. The system uses single-profile design with get_active_profile() returning the first row (LIMIT 1) without any token validation, session management, or per-user isolation. In a networked context, any HTTP client can access all endpoints without credentials.",
|
||||||
|
"impact": "Complete compromise of data confidentiality and integrity. Any attacker can view, modify, or delete all user data without authentication.",
|
||||||
|
"exploitPath": "curl http://127.0.0.1:8080/clubs - accesses club data without any auth headers or tokens",
|
||||||
|
"recommendation": "Implement stateless JWT tokens or session-based authentication. Add middleware to validate tokens on all endpoints. Implement per-user authorization checks in services."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "critical",
|
||||||
|
"category": "injection",
|
||||||
|
"file": "openfut-core/src/routes/auth.rs",
|
||||||
|
"line": 87,
|
||||||
|
"cwe": "CWE-89",
|
||||||
|
"title": "SQL Injection via String Interpolation",
|
||||||
|
"description": "SQL table names are interpolated using string formatting: sqlx::query(&format!(\"DELETE FROM {table}\")). Although currently hardcoded in a loop, this violates parameterized query principles and creates a risk if the table list ever becomes user-controlled or the pattern is copied elsewhere.",
|
||||||
|
"impact": "Potential remote code execution via database manipulation. If extended to user input, attackers could modify arbitrary tables or drop the database.",
|
||||||
|
"exploitPath": "Currently mitigated by hardcoded table names, but the pattern is dangerous and violates secure coding practices.",
|
||||||
|
"recommendation": "Use SQLx's dynamic query builders or identifier types that properly escape table/column names. Replace format! string interpolation with sqlx::query_builder for dynamic identifiers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "high",
|
||||||
|
"category": "configuration",
|
||||||
|
"file": "openfut-bridge/src/proxy.rs",
|
||||||
|
"line": 44,
|
||||||
|
"cwe": "CWE-295",
|
||||||
|
"title": "TLS Certificate Validation Disabled",
|
||||||
|
"description": "HTTP client explicitly disables TLS certificate validation: .danger_accept_invalid_certs(true). This bypasses all certificate pinning, expiration, and hostname verification, making the bridge vulnerable to man-in-the-middle attacks.",
|
||||||
|
"impact": "Attacker positioned between bridge and upstream can intercept, modify, or read all traffic. Compromises confidentiality and integrity of requests to Core and external services.",
|
||||||
|
"exploitPath": "MITM attack between openfut-bridge and openfut-core or upstream services. ARP spoofing on localhost subnet would redirect traffic.",
|
||||||
|
"recommendation": "Remove .danger_accept_invalid_certs(true) in production. If testing requires it, gate behind a development-only environment variable with strong warning. Use proper certificate management (CA bundles, cert pinning)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "high",
|
||||||
|
"category": "dos",
|
||||||
|
"file": "openfut-core/src/services/season.rs",
|
||||||
|
"line": 23,
|
||||||
|
"cwe": "CWE-248",
|
||||||
|
"title": "Unguarded expect() Causes Denial of Service",
|
||||||
|
"description": "Multiple unchecked expect() calls that will panic and crash the server if database queries fail or return unexpected results: Ok(fetch(pool, profile_id).await?.expect(\"just inserted\"))",
|
||||||
|
"impact": "Denial of service. A single database inconsistency or race condition crashes the entire server, making the application unavailable.",
|
||||||
|
"exploitPath": "Trigger race conditions during concurrent requests (e.g., rapid profile deletion + season fetch). Database corruption or migration failure crashes the service immediately.",
|
||||||
|
"recommendation": "Replace expect() with proper error handling (Result types, error logging, graceful degradation). Handle database query failures without panicking. Add integration tests for race conditions."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "high",
|
||||||
|
"category": "dos",
|
||||||
|
"file": "openfut-core/src/services/season.rs",
|
||||||
|
"line": 69,
|
||||||
|
"cwe": "CWE-248",
|
||||||
|
"title": "Unguarded expect() in season fetch",
|
||||||
|
"description": "let season = fetch(pool, profile_id).await?.expect(\"season must exist\"); Panics if season is not found.",
|
||||||
|
"impact": "Server crash on missing or deleted season records.",
|
||||||
|
"exploitPath": "Delete a season via concurrent requests, then call /seasons endpoint. Server panics.",
|
||||||
|
"recommendation": "Return proper error (AppError::NotFound) instead of panicking."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "high",
|
||||||
|
"category": "dos",
|
||||||
|
"file": "openfut-core/src/services/season.rs",
|
||||||
|
"line": 144,
|
||||||
|
"cwe": "CWE-248",
|
||||||
|
"title": "Unguarded expect() in season update",
|
||||||
|
"description": "let updated = fetch(pool, profile_id).await?.expect(\"season must exist\");",
|
||||||
|
"impact": "Server crash on concurrent season modifications.",
|
||||||
|
"exploitPath": "Rapid concurrent season updates that fail race conditions.",
|
||||||
|
"recommendation": "Handle missing records gracefully."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "high",
|
||||||
|
"category": "cors",
|
||||||
|
"file": "openfut-core/src/app.rs",
|
||||||
|
"line": 257,
|
||||||
|
"cwe": "CWE-346",
|
||||||
|
"title": "Permissive CORS Configuration Allows All Origins",
|
||||||
|
"description": ".layer(CorsLayer::permissive()) enables CORS for all origins (*), methods, and headers. Any website can make cross-origin requests to the API and access/modify data.",
|
||||||
|
"impact": "Cross-site request forgery (CSRF) attacks. Malicious websites can issue API requests on behalf of users. Data exfiltration via JavaScript from any origin.",
|
||||||
|
"exploitPath": "Attacker website:\n <img src=\"http://127.0.0.1:8080/clubs\" />\n Fetch API calls to delete profiles, modify squads, etc.",
|
||||||
|
"recommendation": "Restrict CORS to specific origins (e.g., localhost:3000 for web UI, or the game process if exposed). Use CorsLayer::very_restrictive() as default and explicitly allowlist origins."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "high",
|
||||||
|
"category": "dos",
|
||||||
|
"file": "openfut-bridge/src/proxy.rs",
|
||||||
|
"line": 47,
|
||||||
|
"cwe": "CWE-248",
|
||||||
|
"title": "HTTP Client Construction Panic",
|
||||||
|
"description": ".expect(\"failed to build HTTP client\") will panic if the HTTP client fails to initialize, crashing the entire proxy service on startup.",
|
||||||
|
"impact": "Service unavailability. Bridge cannot start if HTTP client configuration is invalid.",
|
||||||
|
"exploitPath": "Invalid system configuration or missing TLS libraries causes HTTP client build to fail, crashing bridge during startup.",
|
||||||
|
"recommendation": "Return Result<ProxyState, Error> from new() and handle construction errors. Use anyhow::Context for better error messages."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "medium",
|
||||||
|
"category": "information-disclosure",
|
||||||
|
"file": "openfut-core/src/error.rs",
|
||||||
|
"line": 54,
|
||||||
|
"cwe": "CWE-209",
|
||||||
|
"title": "Error Messages Leak Implementation Details",
|
||||||
|
"description": "JSON parsing errors are returned directly to clients: format!(\"json parse error: {e}\"). Exposes serde_json parser internals and syntax details useful for crafting attacks.",
|
||||||
|
"impact": "Information disclosure. Attackers learn the JSON parser implementation and can tailor payloads to bypass validation or find parser-specific quirks.",
|
||||||
|
"exploitPath": "Send malformed JSON to any endpoint. Response includes parser error details (e.g., 'expected `,` at line 2 col 5') that aid in crafting exploits.",
|
||||||
|
"recommendation": "Return generic error message to clients: 'invalid request format'. Log detailed errors internally with tracing for debugging."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "medium",
|
||||||
|
"category": "information-disclosure",
|
||||||
|
"file": "openfut-core/src/error.rs",
|
||||||
|
"line": 40,
|
||||||
|
"cwe": "CWE-215",
|
||||||
|
"title": "Database Errors Logged with Full Details",
|
||||||
|
"description": "Database errors are logged with full SQL/query details: tracing::error!(\"Database error: {e}\"). If logs are exposed or compromised, schema, query patterns, and data structure are revealed.",
|
||||||
|
"impact": "Information disclosure in logs. Compromised log files expose database schema and query logic useful for SQL injection or data exfiltration planning.",
|
||||||
|
"exploitPath": "Access server logs (via log aggregation service, file access, etc.) and extract database schema and query patterns.",
|
||||||
|
"recommendation": "Log only error type and ID to clients. Sanitize logs before exporting. Use structured logging with field masking for queries."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "medium",
|
||||||
|
"category": "input-validation",
|
||||||
|
"file": "openfut-core/src/routes/auth.rs",
|
||||||
|
"line": 17,
|
||||||
|
"cwe": "CWE-1025",
|
||||||
|
"title": "Hardcoded Default Credentials",
|
||||||
|
"description": "Default username 'Player 1' is hardcoded with no unique identifier enforcement. Multiple profiles can be created with identical usernames, and weak defaults are used.",
|
||||||
|
"impact": "Weak account creation, potential for account confusion or conflicts. No strong identity guarantees.",
|
||||||
|
"exploitPath": "Multiple users create profiles with default 'Player 1' username. No way to distinguish profiles programmatically.",
|
||||||
|
"recommendation": "Require explicit username on profile creation. Use UUIDs as primary identifiers. Validate username uniqueness and minimum length."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "medium",
|
||||||
|
"category": "input-validation",
|
||||||
|
"file": "openfut-core/src/services/",
|
||||||
|
"line": 0,
|
||||||
|
"cwe": "CWE-400",
|
||||||
|
"title": "Missing Input Length Validation",
|
||||||
|
"description": "No maximum length checks on string fields (usernames, club names, squad names, etc.). Large inputs can cause database bloat, memory exhaustion, or DoS.",
|
||||||
|
"impact": "Denial of service via large payloads. Database bloat. Memory exhaustion. While DefaultBodyLimit::max(256KB) provides some protection, field-level validation is missing.",
|
||||||
|
"exploitPath": "POST /auth/local with username = 256KB string. Database receives bloated data. Repeated calls exhaust storage.",
|
||||||
|
"recommendation": "Add input validation for all user-submitted strings. Set maximum lengths (e.g., username: 50 chars, club name: 100 chars). Validate at route handler level."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "medium",
|
||||||
|
"category": "configuration",
|
||||||
|
"file": "openfut-core/src/db.rs",
|
||||||
|
"line": 13,
|
||||||
|
"cwe": "CWE-315",
|
||||||
|
"title": "Unencrypted SQLite Database on Disk",
|
||||||
|
"description": "SQLite database file (openfut.db) is stored unencrypted on disk. All user data, profiles, squads, cards, etc., are readable by anyone with filesystem access.",
|
||||||
|
"impact": "Data breach if server filesystem is compromised. No protection against:local file access, stolen backups, forensic recovery.",
|
||||||
|
"exploitPath": "Attacker gains filesystem access (compromised server, stolen disk). Reads openfut.db directly. All game data is readable without authentication.",
|
||||||
|
"recommendation": "Use SQLite encryption (e.g., sqlcipher crate) or migrate to PostgreSQL with TLS. Implement file-level encryption. Use restrictive filesystem permissions (0600)."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "medium",
|
||||||
|
"category": "rate-limiting",
|
||||||
|
"file": "openfut-core/src/app.rs",
|
||||||
|
"line": 0,
|
||||||
|
"cwe": "CWE-770",
|
||||||
|
"title": "No Rate Limiting on Endpoints",
|
||||||
|
"description": "No per-IP or per-user rate limiting. Endpoints like POST /auth/reset can be called repeatedly without restriction, allowing attackers to repeatedly wipe all data.",
|
||||||
|
"impact": "Denial of service and data destruction. Attacker can spam /auth/reset to destroy user data or exhaust server resources.",
|
||||||
|
"exploitPath": "for i in 1..1000: POST /auth/reset with confirm='reset'. All data wiped repeatedly.",
|
||||||
|
"recommendation": "Implement rate limiting middleware using tower_governor or similar. Add per-IP limits (e.g., 10 requests/min) and per-endpoint limits. Use exponential backoff."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "low",
|
||||||
|
"category": "audit-logging",
|
||||||
|
"file": "openfut-core/src/services/",
|
||||||
|
"line": 0,
|
||||||
|
"cwe": "CWE-778",
|
||||||
|
"title": "Missing Audit Logging",
|
||||||
|
"description": "No audit trail of user actions (profile creation, data deletion, squad modifications). Cannot detect unauthorized access, data tampering, or compliance violations.",
|
||||||
|
"impact": "Incident response and forensics are impossible. Cannot determine who did what and when. Compliance risks (GDPR, etc.).",
|
||||||
|
"exploitPath": "Attacker deletes all profiles, modifies squads. No audit log shows what happened or who did it.",
|
||||||
|
"recommendation": "Add audit logging for all data mutations. Log: timestamp, user (profile) ID, action, resource affected, before/after state. Store in separate immutable table."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "low",
|
||||||
|
"category": "dependencies",
|
||||||
|
"file": "openfut-bridge/Cargo.toml",
|
||||||
|
"line": 0,
|
||||||
|
"cwe": "CWE-1035",
|
||||||
|
"title": "Older Dependency Versions (reqwest, rustls)",
|
||||||
|
"description": "openfut-bridge uses reqwest 0.11 (latest is 0.12) and rustls 0.21 (latest is 0.23). Intentional for version matching, but creates a larger surface area for known CVEs.",
|
||||||
|
"impact": "Potential vulnerabilities in older dependencies. Delayed access to security patches.",
|
||||||
|
"exploitPath": "Known CVE in reqwest 0.11 or rustls 0.21 could be exploited. Combined with danger_accept_invalid_certs, TLS bypass becomes easier.",
|
||||||
|
"recommendation": "Upgrade dependencies to latest versions when possible. Monitor CVE databases (CVE, RustSec) for the versions in use. Pin versions and set up automated dependency updates."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"severity": "low",
|
||||||
|
"category": "error-handling",
|
||||||
|
"file": "openfut-core/src/app.rs",
|
||||||
|
"line": 256,
|
||||||
|
"cwe": "CWE-248",
|
||||||
|
"title": "Body Size Limit Without Per-Field Validation",
|
||||||
|
"description": "DefaultBodyLimit::max(256KB) limits the entire request body, but individual fields are not validated. A single large field can consume most of the limit.",
|
||||||
|
"impact": "Mild DoS. Large field values cause database bloat. Not a critical issue due to body limit, but field-level validation would be better.",
|
||||||
|
"exploitPath": "POST /auth/local with 250KB club_name field. Database receives bloated data.",
|
||||||
|
"recommendation": "Add per-field validation in addition to body limits. Validate and sanitize fields before database insertion."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"riskScore": 82,
|
||||||
|
"riskCategory": "CRITICAL",
|
||||||
|
"riskSummary": "OpenFUT has critical security issues that would make it unsafe for production or networked deployment. The most severe are the complete absence of authentication/authorization and the SQL injection pattern in the auth.rs module. The system is designed as single-player (single-profile) with no multi-tenant isolation, which is dangerous if exposed to the network.",
|
||||||
|
"recommendations": [
|
||||||
|
{
|
||||||
|
"priority": "CRITICAL",
|
||||||
|
"area": "Authentication & Authorization",
|
||||||
|
"recommendation": "Implement JWT-based or session-based authentication on all endpoints. Add middleware to validate auth tokens on every request. Implement per-profile authorization checks. Currently any HTTP client can access all endpoints.",
|
||||||
|
"effort": "High",
|
||||||
|
"impact": "Blocks all data breaches from unauthenticated access"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "CRITICAL",
|
||||||
|
"area": "SQL Injection Prevention",
|
||||||
|
"recommendation": "Replace sqlx::query(&format!(...)) in auth.rs:87 with proper parameterized identifiers. Use sqlx::query_builder for dynamic table/column names instead of string interpolation.",
|
||||||
|
"effort": "Low",
|
||||||
|
"impact": "Prevents SQL injection even if pattern is copied to user input"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "HIGH",
|
||||||
|
"area": "TLS & Transport Security",
|
||||||
|
"recommendation": "Remove .danger_accept_invalid_certs(true) from proxy.rs:44. If development requires it, gate behind an environment variable (e.g., DEV_SKIP_TLS_VERIFICATION) with strong warnings in logs.",
|
||||||
|
"effort": "Low",
|
||||||
|
"impact": "Prevents MITM attacks on bridge-to-core communication"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "HIGH",
|
||||||
|
"area": "Error Handling",
|
||||||
|
"recommendation": "Replace all expect() calls with proper Result handling. Use anyhow::Context or custom error types. Add logging for debugging but return generic errors to clients.",
|
||||||
|
"effort": "Medium",
|
||||||
|
"impact": "Prevents DoS via server panics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "HIGH",
|
||||||
|
"area": "CORS",
|
||||||
|
"recommendation": "Replace CorsLayer::permissive() with CorsLayer::very_restrictive() or explicit allowlist. For single-player use, restrict to localhost and the game process only.",
|
||||||
|
"effort": "Low",
|
||||||
|
"impact": "Prevents CSRF and cross-origin attacks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "HIGH",
|
||||||
|
"area": "Rate Limiting",
|
||||||
|
"recommendation": "Add per-IP rate limiting using tower_governor or similar. Implement limits on destructive endpoints (e.g., POST /auth/reset: 1 request per hour per IP).",
|
||||||
|
"effort": "Medium",
|
||||||
|
"impact": "Prevents DoS and repeated data destruction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "MEDIUM",
|
||||||
|
"area": "Input Validation",
|
||||||
|
"recommendation": "Add maximum length validation for all string fields (username, club_name, squad_name, etc.). Enforce at route handler level. Example: username max 50 chars, club_name max 100 chars.",
|
||||||
|
"effort": "Medium",
|
||||||
|
"impact": "Prevents database bloat and data validation failures"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "MEDIUM",
|
||||||
|
"area": "Data Encryption",
|
||||||
|
"recommendation": "Use SQLite encryption (sqlcipher) or migrate to PostgreSQL with TLS. Set restrictive filesystem permissions (0600) on openfut.db.",
|
||||||
|
"effort": "High",
|
||||||
|
"impact": "Protects data at rest from filesystem access"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "MEDIUM",
|
||||||
|
"area": "Error Message Handling",
|
||||||
|
"recommendation": "Return generic error messages to clients. Log detailed errors internally. Example: client sees 'invalid request', server logs 'JSON parse error: expected `,` at line 2'.",
|
||||||
|
"effort": "Low",
|
||||||
|
"impact": "Reduces information disclosure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "MEDIUM",
|
||||||
|
"area": "Audit Logging",
|
||||||
|
"recommendation": "Add audit trail for all data mutations (create, update, delete). Log timestamp, profile ID, action, resource, and before/after state. Store in immutable audit_log table.",
|
||||||
|
"effort": "Medium",
|
||||||
|
"impact": "Enables incident response and forensics"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "LOW",
|
||||||
|
"area": "Dependency Management",
|
||||||
|
"recommendation": "Upgrade reqwest to 0.12 and rustls to 0.23 when possible. Set up Dependabot or RustSec monitoring for CVEs. Regularly audit dependencies.",
|
||||||
|
"effort": "Low",
|
||||||
|
"impact": "Reduces attack surface from known CVEs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"priority": "LOW",
|
||||||
|
"area": "Default Values",
|
||||||
|
"recommendation": "Remove hardcoded default username 'Player 1'. Require explicit username on profile creation. Use UUIDs for profile identification.",
|
||||||
|
"effort": "Low",
|
||||||
|
"impact": "Improves account identity and prevents confusion"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"securityDesignNotes": {
|
||||||
|
"intendedUse": "OpenFUT is designed for single-player offline use. Single-profile design is intentional for local FIFA 23 emulation.",
|
||||||
|
"deploymentContext": "Localhost only (127.0.0.1:8080). Not intended for networked or multi-user deployment.",
|
||||||
|
"implicationForSecurity": "Many security issues (no auth, permissive CORS) are acceptable for localhost-only use. However, the code structure lacks security boundaries, so if ever exposed to the network, it would be completely unsecured. Recommend adding security gates now rather than retrofitting later.",
|
||||||
|
"suggestedDefensiveApproach": "Even for single-player use, add security layers (basic auth, CORS restrictions, rate limiting) to prevent accidental misuse if deployed in an unsafe context."
|
||||||
|
},
|
||||||
|
"positiveFindingsAndStrengths": [
|
||||||
|
"✓ SQLx is used throughout with parameterized queries (except auth.rs:87)",
|
||||||
|
"✓ Foreign key constraints are enforced in SQLite",
|
||||||
|
"✓ UUIDs are used for entity IDs instead of sequential IDs (reduces enumeration attacks)",
|
||||||
|
"✓ Request body size is limited to 256KB (prevents large payload DoS)",
|
||||||
|
"✓ Concurrency is limited to 256 concurrent requests",
|
||||||
|
"✓ Sensitive tokens (X-UT-SID, X-UT-PHISHING-TOKEN) are stripped from captures",
|
||||||
|
"✓ Logging is structured using tracing crate (good for audit trails)",
|
||||||
|
"✓ Services layer properly encapsulates database access"
|
||||||
|
],
|
||||||
|
"testingRecommendations": [
|
||||||
|
"Add integration tests for authentication bypass (attempt to access endpoints without tokens)",
|
||||||
|
"Test SQL injection payloads in auth.rs:87 pattern (if table names become dynamic)",
|
||||||
|
"Test CORS with cross-origin requests from external origins",
|
||||||
|
"Test rate limiting with rapid concurrent requests to /auth/reset",
|
||||||
|
"Test input validation with oversized strings (100MB+ usernames)",
|
||||||
|
"Test panic handling with corrupted database state",
|
||||||
|
"Test TLS MITM scenarios (certificate pinning validation)",
|
||||||
|
"Add fuzz testing for JSON parsing to find edge cases"
|
||||||
|
],
|
||||||
|
"complianceNotes": {
|
||||||
|
"gdpr": "No explicit data handling policy. If user data is processed, GDPR requires consent, data retention limits, and audit trails. Not currently implemented.",
|
||||||
|
"dataProtection": "Unencrypted database at rest violates most data protection frameworks.",
|
||||||
|
"logging": "Audit logging is missing, violating compliance requirements."
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# FIFA 17 FUT — Card Taxonomy (single source of truth)
|
||||||
|
|
||||||
|
Status: verified 2026-08-12. This document supersedes every earlier scattered
|
||||||
|
taxonomy claim in the repo (see "Superseded taxonomy" at the bottom).
|
||||||
|
|
||||||
|
Evidence labels: **OBSERVED** = read directly from a shipped table or decompiled
|
||||||
|
function; **INFERRED** = derived from OBSERVED facts; **HYPOTHESIS** = plausible,
|
||||||
|
not yet proven. `UNKNOWN` is a valid answer and is stated as such.
|
||||||
|
|
||||||
|
## Provenance
|
||||||
|
|
||||||
|
- Authoritative tables: `10.10.0.105:/home/alex/Documents/OpenFUT/fifa17-recon/data/tables/`,
|
||||||
|
copied byte-identical into this repo at `fifa17-recon/data/tables/`.
|
||||||
|
SHA-256 manifest + verification: `docs/evidence/fifa17-recon/table-hashes.sha256`
|
||||||
|
(36 files: 31 `fcc_*.json` + 5 staff tables; combined hash-of-hashes
|
||||||
|
`10f239add919089354d8dbff873fc9737b0a0f80f6ac41b1aa2a096c0ec8d331`).
|
||||||
|
- Each table file is a DECODED dump `{table, source, rowcount, schema[], rows[]}`;
|
||||||
|
card instances are in `rows[]`, one object per card. Row counts and id ranges
|
||||||
|
below were re-extracted from the in-repo `.120` copies.
|
||||||
|
- Semantic (subtype→kind) labels are decompiler-derived, carried in
|
||||||
|
`fifa17-recon/tools/fut_consumables.py` (`BY_SUBTYPE`, `_DOC`), generated by
|
||||||
|
`tools/build_consumables.py` from `FUN_1800d8330`, `FUN_18013f4d0`,
|
||||||
|
`FUN_1801bfac0`, `FUN_1801aa230`, `FUN_180048780` + the fcc tables.
|
||||||
|
- Staff family selection: `fifa17-recon/docs/CARD_SYSTEM.md` (2026-08-04/05 blocks)
|
||||||
|
and `fifa17-recon/tools/fut_staff.py` / `fut_coaches.py`.
|
||||||
|
|
||||||
|
## Family selector (OBSERVED, binary)
|
||||||
|
|
||||||
|
Before registering an item, `FUN_180141660` merges the client's local DB. It
|
||||||
|
switches on record `+0x4c` (`cardtype`), which `FUN_1800d8330` derives from JSON
|
||||||
|
atom `0x6c cardsubtypeid` alone:
|
||||||
|
|
||||||
|
```
|
||||||
|
cardsubtypeid 0..3 -> cardtype 1 players
|
||||||
|
4 -> cardtype 2 managercards
|
||||||
|
5 -> cardtype 3 headcoachcards
|
||||||
|
6 -> cardtype 10 gkcoachcards
|
||||||
|
7 -> cardtype 5 physiocards
|
||||||
|
8 -> cardtype 4 fitnesscoachcards
|
||||||
|
0x1e,0x1f,0x91..0x96,0xe7..0xe9,0xec -> cardtype 9 club items + misc
|
||||||
|
absent (default 342) -> cardtype 0 no merge
|
||||||
|
```
|
||||||
|
|
||||||
|
Key: `carddbid == resourceId` for non-player families (queried RAW, no mask);
|
||||||
|
players are queried by `playerid = resourceId & 0xffffff` (atom `0x287`), and
|
||||||
|
`assetId` (atom `0x23`) is never read by the merge.
|
||||||
|
|
||||||
|
## Consumable & staff families — evidence table (OBSERVED, `.120` tables)
|
||||||
|
|
||||||
|
| Family | Table | Rows | carddbid range | cardsubtype set | cardassetid (ART) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Contracts | `fcc_contractcards.json` | 13 | 5001001–5001013 | {201, 202} | {7, 8} |
|
||||||
|
| Fitness + Healing | `fcc_healingcards.json` | 27 | 5002001–5002030 | {211,212,213,215,216,217,218,219,220} | {9, 10} |
|
||||||
|
| Training (6 sub-families) | `fcc_trainingcards.json` | 143 | 5003001–5003159 | 51-57, 61-67, 91-110, 121-136, 250-273, 300-340 | {1,3,32,34,35,50,51} |
|
||||||
|
| Misc | `fcc_misccards.json` | 42 | 5004001–5004042 | {231, 232, 233, 236} | {43, 44, 45, 46} |
|
||||||
|
| Badges | `fcc_badgecards.json` | 656 | 6000000–6000656 | (none in row) | {39} |
|
||||||
|
| Stadiums | `fcc_stadium.json` | 78 | 6200000–6200077 | (none in row) | {36} |
|
||||||
|
| Kits | `fcc_kitcards.json` | 1482 | 6300000–6400654 | (none in row) | {35} |
|
||||||
|
| League logos | `fcc_leaguelogos.json` | 44 | 8010000–8010044 | (none in row) | {40} |
|
||||||
|
| League logo stickers | `fcc_leaguelogostickers.json` | 39 | 8010000–8010039 | (none in row) | {40} |
|
||||||
|
| Balls | `fcc_balls.json` | 42 | 8120194–8120236 | (none in row) | {37} |
|
||||||
|
| Managers | `managercards.json` | 417 | 1000001–1001552 | (staff; by cardsubtypeid 4) | (assetid col) |
|
||||||
|
| Head coaches | `headcoachcards.json` | 124 | 2000004–2000328 | (staff; subtype 5) | — |
|
||||||
|
| GK coaches | `gkcoachcards.json` | 121 | 9000001–9000324 | (staff; subtype 6) | — |
|
||||||
|
| Physios | `physiocards.json` | 51 | 4000002–4000259 | (staff; subtype 7) | — |
|
||||||
|
| Fitness coaches | `fitnesscoachcards.json` | 115 | 3000019–3000328 | (staff; subtype 8) | — |
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- **Contracts (5001xxx)** — 201 = player_contract, 202 = manager_contract
|
||||||
|
(OBSERVED, `BY_SUBTYPE`).
|
||||||
|
- **Fitness + Healing (5002xxx)** — subtype **214 is a valid enum value but ships
|
||||||
|
ZERO rows** (OBSERVED: 214 absent from `rows[]`). Enum split (OBSERVED,
|
||||||
|
`BY_SUBTYPE`): healing = 211-218, player_fitness = 219, squad_fitness = 220.
|
||||||
|
The `+0x58 rareflag == 1` trap flips subtype 219 to squad-fitness.
|
||||||
|
- **Training (5003xxx)** — SIX sub-families, all OBSERVED in `rows[]` and labelled
|
||||||
|
in `BY_SUBTYPE`:
|
||||||
|
- `gk_training` 51-57 (7)
|
||||||
|
- `player_training` 61-67 (7)
|
||||||
|
- `position_mod` 91-110 (20)
|
||||||
|
- `formation_mod` 121-136 (16)
|
||||||
|
- chem/play styles 250-273 (24): `player_playstyle` 250-268 (19) +
|
||||||
|
`gk_playstyle` 269-273 (5)
|
||||||
|
- `manager_league` 300-**341** in the client enum (42), but the shipped table
|
||||||
|
contains only 300-340 (41 rows) — 341 is defined, not shipped.
|
||||||
|
- Client enum also defines vestigial/unshipped ranges NOT in the table:
|
||||||
|
`manager_formation_mod` 71-86, and `DEAD_ZONE`
|
||||||
|
58-60,68-70,87-90,111-120,203-210 (28). These have no rows.
|
||||||
|
- **Misc (5004xxx)** — subtypes {231,232,233,236}; cardassetid ART 43-46.
|
||||||
|
cardsubtype 231 = 0xe7 anchors the 0xe7..0xe9 block to misc via `FUN_1800d8330`.
|
||||||
|
- **Club items (badges/stadiums/kits/logos/balls)** carry no `cardsubtype` in the
|
||||||
|
row; they are keyed by `carddbid` range and distinguished on the wire by ART
|
||||||
|
`cardassetid` (badges 39, stadium 36, kits 35, logos 40, balls 37). **Kits live
|
||||||
|
at 6300000–6400654 and are NOT badges** (badges are 6000xxx). The client-side
|
||||||
|
subtype→family map (which of 0x1e,0x1f,0x91..0x96 is ball vs stadium vs badge vs
|
||||||
|
kit) is **UNKNOWN** — it is in none of the dumped tables and cardtype 9 has no
|
||||||
|
miss-fill arm (see `CARD_SYSTEM.md`, "Club items", 2026-08-05).
|
||||||
|
|
||||||
|
## Staff — subtype/cardtype selectors (OBSERVED, binary)
|
||||||
|
|
||||||
|
| cardsubtypeid | cardtype | Family | Table | Rows | carddbid range |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 4 | 2 | Manager | `managercards.json` | 417 | 1000001–1001552 |
|
||||||
|
| 5 | 3 | Head coach | `headcoachcards.json` | 124 | 2000004–2000328 |
|
||||||
|
| 6 | 10 | GK coach | `gkcoachcards.json` | 121 | 9000001–9000324 |
|
||||||
|
| 7 | 5 | Physio | `physiocards.json` | 51 | 4000002–4000259 |
|
||||||
|
| 8 | 4 | Fitness coach | `fitnesscoachcards.json` | 115 | 3000019–3000328 |
|
||||||
|
|
||||||
|
`cardsubtypeid -> rec+0x50` is the only family selector; `FUN_1800d8330` maps it to
|
||||||
|
`rec+0x4c cardtype`. Manager merge = `FUN_1801356c0` (queries `managercards` by
|
||||||
|
`carddbid` raw); coach merges live in the respective tables (`fut_coaches.py`).
|
||||||
|
All five render live with real photos/bonuses, zero "DB Error" (CARD_SYSTEM.md,
|
||||||
|
2026-08-05).
|
||||||
|
|
||||||
|
## Players (context, out of taxonomy scope here)
|
||||||
|
|
||||||
|
Players use cardsubtypeid 0..3 -> cardtype 1; merged by `playerid = resourceId &
|
||||||
|
0xffffff` against the local `players` table. Identity/name/nation/team come from
|
||||||
|
the client DB; rating/position/attributes come from our item JSON. See
|
||||||
|
`CARD_SYSTEM.md` and the migration work in `openfut-adapter-fifa17`.
|
||||||
|
|
||||||
|
## Superseded taxonomy (report only — no other files edited)
|
||||||
|
|
||||||
|
The earlier working taxonomy (in prior agent-session notes / long-term memory, and
|
||||||
|
partially in the original `TablesTaxonomy` scout output) got four things wrong.
|
||||||
|
Corrected here:
|
||||||
|
|
||||||
|
1. **Chemistry / play styles are 250-273, NOT 91-136.** 91-110 = `position_mod`,
|
||||||
|
121-136 = `formation_mod`. (OBSERVED in `fcc_trainingcards` + `BY_SUBTYPE`.)
|
||||||
|
2. **6300xxx/6400xxx are KITS, not badges.** Badges are 6000xxx. (OBSERVED.)
|
||||||
|
3. **5004xxx misc cards exist** (subtypes 231/232/233/236, ART 43-46). Previously
|
||||||
|
omitted. (OBSERVED, `fcc_misccards`.)
|
||||||
|
4. **8010xxx league logos exist** (+ stickers), ART 40. Previously omitted.
|
||||||
|
(OBSERVED, `fcc_leaguelogos`.)
|
||||||
|
|
||||||
|
**Repo blast-radius of the superseded values: NONE.** A repo-wide grep
|
||||||
|
(`docs/`, `openfut-adapter-fifa17/`, `openfut-core/`, `fifa17-recon/`) for the wrong
|
||||||
|
claims (`91-136` chem styles; `6300/6400xxx` as badges) returns zero hits.
|
||||||
|
`fifa17-recon/docs/CARD_SYSTEM.md` and `plan-2026-08-06-card-subsystem.md` already
|
||||||
|
use the correct `250..273` for chem styles and the correct staff subtypes 4-8;
|
||||||
|
`CARD_SYSTEM.md` is a mechanism log, not a taxonomy, and asserts none of the wrong
|
||||||
|
ranges. The superseded values therefore live only in non-repo agent memory, which
|
||||||
|
should be corrected to point at this document.
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
# openfut-core — Correctness Issues (audit 2026-08-17)
|
||||||
|
|
||||||
|
Read-only audit of `openfut-core` (game-agnostic axum + SQLite/sqlx economy authority).
|
||||||
|
Four reported issue classes confirmed with exact `file:line` evidence, **plus a bonus
|
||||||
|
HIGH-severity SBC duplicate-card economy exploit**. Nothing here is fixed yet —
|
||||||
|
fixing bumps Core off the frozen P1 reference (`fbb54ea`, the current known-good
|
||||||
|
production Core) and one item needs a DB migration + prod backfill, so this needs a
|
||||||
|
**go/no-go** before rebuild+redeploy.
|
||||||
|
|
||||||
|
> **Zero live users right now**, so the HIGH-severity economy exploits are not
|
||||||
|
> currently exploitable — but they are the exact class (coin overspend + card
|
||||||
|
> duplication) that crashed live clients earlier (project memory), so they should be
|
||||||
|
> fixed before any real play.
|
||||||
|
|
||||||
|
## Architecture context (why the bugs cluster)
|
||||||
|
|
||||||
|
Two generations of economy code coexist:
|
||||||
|
- **NEW** `services/economy.rs` + `routes/economy.rs` — **fully atomic + validated**:
|
||||||
|
every compound op acquires a connection, `BEGIN IMMEDIATE`, composes
|
||||||
|
transaction-scoped primitives (`debit`/`credit`/`add_item`/`remove_item`/
|
||||||
|
`consume_entitlement`), commits/rolls back via `finish()`, rejects negative amounts,
|
||||||
|
and has an extensive in-module test suite. **This is the reference fix pattern.**
|
||||||
|
- **OLD** per-feature services (`club`, `pack`, `market`, `sbc`, `checkin`, `upgrades`,
|
||||||
|
`match_service`, `season`) — predate it, still do read/check/write directly against
|
||||||
|
the `&Pool` with each statement on its own connection: no transaction. `economy.rs`'s
|
||||||
|
own module doc explicitly warns these "cannot offer that guarantee".
|
||||||
|
|
||||||
|
Issues 2 and 3 live entirely in the OLD generation; the fix is to route them through
|
||||||
|
`economy.rs`'s proven atomic ops (or give the pool helpers `&mut SqliteConnection`
|
||||||
|
transactional variants). Request flow is `X-OpenFUT-Game` header → active profile →
|
||||||
|
club → service; clients never pass a `club_id`, so cross-club access is **not** a
|
||||||
|
vector — the risks are intra-club concurrency + unvalidated payloads.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 1 — `sbc_submissions` missing `club_id` (milestone always 0) · **LOW**
|
||||||
|
|
||||||
|
- **Root cause:** `migrations/0001_initial.sql:96-102` creates `sbc_submissions(id,
|
||||||
|
profile_id, sbc_id, submitted_card_ids, passed, submitted_at)` — no `club_id`, and no
|
||||||
|
later migration adds one. But `src/routes/club.rs:94-99` (`get_milestones`) runs
|
||||||
|
`SELECT COUNT(*) FROM sbc_submissions WHERE club_id = ? AND passed = 1`. SQLite errors
|
||||||
|
`no such column: club_id`; the error is swallowed by `.unwrap_or(0)` → the
|
||||||
|
`sbcs_completed` milestone is **always 0**. Writer `services/sbc.rs:74-83` inserts
|
||||||
|
`profile_id`, not `club_id`.
|
||||||
|
- **Impact:** wrong milestone stat only. No crash, no economy corruption.
|
||||||
|
- **Fix (matches the ticket — needs migration + backfill):** new migration `0019`:
|
||||||
|
`ALTER TABLE sbc_submissions ADD COLUMN club_id TEXT;` then backfill
|
||||||
|
`UPDATE sbc_submissions SET club_id = (SELECT c.id FROM clubs c WHERE c.profile_id =
|
||||||
|
sbc_submissions.profile_id);` and bind `club_id` in `sbc.rs:submit_sbc`'s INSERT
|
||||||
|
(`club_id` is already a param at `sbc.rs:41`).
|
||||||
|
- **Simpler alternative (no migration):** change the `club.rs:95` query to
|
||||||
|
`WHERE profile_id = ?` (column already exists). Ticket asks for the column, so both
|
||||||
|
are recorded.
|
||||||
|
- **Migration required:** YES (for the ticket's fix); NO (for the alternative).
|
||||||
|
|
||||||
|
## Issue 2 — Non-atomic check-then-act economy mutations (TOCTOU) · **HIGH**
|
||||||
|
|
||||||
|
Each does read → check → write across multiple pool round-trips with no
|
||||||
|
`BEGIN IMMEDIATE`, so concurrent requests race → overspend / duplication / double reward:
|
||||||
|
|
||||||
|
| Site | Race |
|
||||||
|
|---|---|
|
||||||
|
| `services/club.rs:88-115` `spend_coins` | SELECT coins → `if balance<amount` → UPDATE; two concurrent spends both pass → **overspend / negative balance**. Shared primitive used by all callers below. |
|
||||||
|
| `services/pack.rs:58-141` `open_pack` | read `pack.opened` → INSERT cards loop → UPDATE opened=1; concurrent double-open → **card duplication** |
|
||||||
|
| `services/pack.rs:144-160` `buy_pack` | `spend_coins` then `grant_pack`, separate ops → crash between = coins gone, no pack |
|
||||||
|
| `services/market.rs:129-186` `buy_listing` | SELECT sold=0 → spend → UPDATE sold=1 → INSERT; concurrent double-buy → **two cards minted** |
|
||||||
|
| `services/market.rs:188+` `sell_card` | SELECT owned → DELETE → add_coins; concurrent double-sell → **double credit** |
|
||||||
|
| `services/sbc.rs:35-107` `submit_sbc` | validate → DELETE cards → INSERT submission → add_coins/grant_pack; same cards to two SBCs → **double reward** |
|
||||||
|
| `services/checkin.rs:60-120` `claim` | SELECT last → same-day check → reward → INSERT; concurrent → **double claim** |
|
||||||
|
| `services/upgrades.rs:64-95` `change_position` | fetch → spend_coins → UPDATE |
|
||||||
|
| `services/match_service.rs:97-235` + `season.rs` reward | many sequential writes; partial failure leaves partial rewards |
|
||||||
|
|
||||||
|
- **Impact:** corrupts economy state (coin overspend + card duplication).
|
||||||
|
- **Fix:** wrap each compound op in one `BEGIN IMMEDIATE`…`finish()` transaction
|
||||||
|
exactly as `services/economy.rs:214-236` already does; route pack/market/sbc/checkin/
|
||||||
|
upgrades through `economy.rs`'s composed atomic ops (or add `&mut SqliteConnection`
|
||||||
|
variants of `spend_coins`/`add_coins`/`grant_pack`/`add_item`).
|
||||||
|
- **Migration required:** NO (code-only).
|
||||||
|
|
||||||
|
## Issue 3 — Missing input validation: SBC duplicate-card exploit · **HIGH**
|
||||||
|
|
||||||
|
- **Root cause:** `services/sbc.rs:53-70` iterates `req.owned_card_ids` with **no dedup
|
||||||
|
and no length bound**. The same `owned_card_id` repeated N times resolves the same card
|
||||||
|
N times (each `fetch_optional` succeeds); `validate_sbc` (`sbc.rs:110-116`) counts it
|
||||||
|
toward `squad_size` and passes; the DELETE loop deletes it once → **a user satisfies
|
||||||
|
any SBC with ONE card duplicated → free rewards**. Entry point `routes/sbc.rs:30-49`
|
||||||
|
forwards `req` unvalidated. Unbounded Vec length is also a DoS.
|
||||||
|
- **Fix:** in `submit_sbc`, reject duplicate ids (collect into a `HashSet`, compare
|
||||||
|
`len`) and bound the list (e.g. ≤ 30) before resolving → `AppError::BadRequest`.
|
||||||
|
- **Already-good validation (no change):** `economy.rs:64-67,84-87` reject negative
|
||||||
|
amounts; `match_service.rs:100-104` clamps goals 0..99; `upgrades.rs` validates
|
||||||
|
boost 1..3 / positions. Minor: `routes/economy.rs post_grant_reward` forwards an
|
||||||
|
unbounded `amount` (trusted host caller; add an upper-bound sanity guard).
|
||||||
|
- **Migration required:** NO.
|
||||||
|
|
||||||
|
## Issue 4 — `season.rs` panics + checkin index panic · **LOW**
|
||||||
|
|
||||||
|
- **Root cause:** `services/season.rs` `.expect()` on `fetch_optional` Options at
|
||||||
|
`:23` (`get_or_create`), `:69` and `:144` (`record_match`) — panic if the `seasons`
|
||||||
|
row is absent when expected. `seasons.profile_id` is PRIMARY KEY
|
||||||
|
(`migrations/0006_seasons_loans_packs.sql:2`), so the concurrent-insert case surfaces
|
||||||
|
as a UNIQUE error via `?` (not the panic), lowering probability — but it still
|
||||||
|
panics-on-invariant, aborting that request (axum → 500 for the request; not a full
|
||||||
|
server crash).
|
||||||
|
- **Secondary:** `services/checkin.rs:~52,~88` index `STREAK_COINS[idx]` with
|
||||||
|
`idx = ((streak-1)%7) as usize`; Rust `%` can be negative → a corrupt/negative
|
||||||
|
persisted `streak_day` yields a negative index → **panic**.
|
||||||
|
- **Fix:** replace each `.expect(...)` with
|
||||||
|
`.ok_or_else(|| AppError::Internal("season row missing".into()))?`; guard the checkin
|
||||||
|
index with `.rem_euclid(7)` (or clamp `streak_day >= 1` on read).
|
||||||
|
- **Migration required:** NO.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fix plan summary
|
||||||
|
|
||||||
|
| Issue | Severity | Migration | Files |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 sbc_submissions club_id | LOW | YES (or none via alt) | `migrations/0001` (+new 0019), `routes/club.rs`, `services/sbc.rs` |
|
||||||
|
| 2 non-atomic mutations | HIGH | NO | `services/{club,pack,market,sbc,checkin,upgrades,match_service,season}.rs` → route through `services/economy.rs` |
|
||||||
|
| 3 SBC duplicate-card exploit | HIGH | NO | `services/sbc.rs`, `routes/sbc.rs` |
|
||||||
|
| 4 season/checkin panics | LOW | NO | `services/season.rs`, `services/checkin.rs` |
|
||||||
|
|
||||||
|
**Recommended order:** 3 (smallest, highest-value: kills the free-reward exploit) → 2
|
||||||
|
(the transactional refactor, largest) → 4 (defensive hygiene) → 1 (cosmetic; do with
|
||||||
|
the alt query unless the column is wanted).
|
||||||
|
|
||||||
|
**Deployment note:** all of these change `openfut-core`, which is currently frozen at
|
||||||
|
the P1 reference (`fbb54ea`) in production. Fixing + rebuilding + redeploying prod-core
|
||||||
|
is a deliberate step off that reference — get a go/no-go first. Only Issue 1's
|
||||||
|
column-add needs a migration + prod backfill; 2/3/4 are code-only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Resolution (2026-08-17, on `openfut-core` @ `fbb54ea` + these edits)
|
||||||
|
|
||||||
|
All four issue classes fixed in the canonical superproject submodule
|
||||||
|
`openfut-core`; full test suite green (179 tests) + clippy clean + a new
|
||||||
|
regression test `tests/integration_test.rs::test_sbc_rejects_duplicate_cards`.
|
||||||
|
|
||||||
|
| Issue | Fix | Files |
|
||||||
|
|---|---|---|
|
||||||
|
| 3 SBC dup-card exploit | dedup (`HashSet`) + `MAX_SBC_CARDS`=30 bound in `submit_sbc`, before card resolution → `BadRequest` | `services/sbc.rs` |
|
||||||
|
| 1 sbc_submissions club_id | migration `0019` adds `club_id` + backfills from `clubs`; `submit_sbc` INSERT now binds `club_id` | `migrations/0019_*.sql`, `services/sbc.rs` |
|
||||||
|
| 4 season/checkin panics | `.expect()` → `.ok_or_else(AppError::Internal)?` (3 sites); checkin index `% 7` → `.rem_euclid(7)` (2 sites) | `services/season.rs`, `services/checkin.rs` |
|
||||||
|
| 2 non-atomic mutations | statement-level compare-and-swap (see below) | `services/{club,pack,market,checkin}.rs` |
|
||||||
|
|
||||||
|
### Issue 2 — how it was fixed, and the residual
|
||||||
|
|
||||||
|
Rather than the full transaction refactor (threading `&mut SqliteConnection`
|
||||||
|
through every service), the concurrency-exploitable races were closed with
|
||||||
|
single-statement **compare-and-swap** — the atomic unit SQLite already gives us,
|
||||||
|
no transaction plumbing, minimal blast radius on the working prod economy path:
|
||||||
|
|
||||||
|
- `club::spend_coins` — `UPDATE … SET coins = coins - ? WHERE id = ? AND coins >= ?`
|
||||||
|
+ `rows_affected` guard; also rejects negative amounts. Kills **overspend** for
|
||||||
|
every caller (the shared root primitive).
|
||||||
|
- `pack::open_pack` — claims the pack (`UPDATE … opened = 1 WHERE … AND opened = 0`)
|
||||||
|
**before** minting cards; loser aborts. Kills **card duplication** via double-open.
|
||||||
|
- `market::buy_listing` — claims the listing (`sold 0→1`) before charging; releases
|
||||||
|
the claim if the debit fails. Kills **double-mint**.
|
||||||
|
- `market::sell_card` — `DELETE … WHERE id = ? AND club_id = ?` + `rows_affected`
|
||||||
|
guard before crediting. Kills **double-credit** via double-sell.
|
||||||
|
- `checkin::claim` — conditional `INSERT … SELECT … WHERE NOT EXISTS (today's row)`
|
||||||
|
+ `rows_affected` guard; pays out only if the claim landed. Kills **double-claim**.
|
||||||
|
|
||||||
|
**Residual (accepted, documented):** the *multi-statement all-or-nothing* edges that
|
||||||
|
need a real transaction to close — `pack::buy_pack` (spend then grant: a crash between
|
||||||
|
loses coins with no pack), `sbc::submit_sbc` (concurrent submits sharing cards could
|
||||||
|
double-consume mid-loop), and `match_service`/`season` reward chains (partial writes on
|
||||||
|
crash). These are **partial-failure durability edges, not statement-level races**, and
|
||||||
|
require concurrency that a single-player FIFA17 client does not generate. Closing them
|
||||||
|
is the `&mut SqliteConnection` transaction refactor originally proposed; deferred as
|
||||||
|
low-value for single-player. Overspend + duplication + double-credit — the vectors that
|
||||||
|
corrupt economy state — are all closed.
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# FIFA17 Content Completeness (33-record gap)
|
||||||
|
|
||||||
|
Evidence: ContentGapMap scout vs import-input.json (persona 33068179, 1995 items),
|
||||||
|
fifa17-recon/data/tables, manifest/fifa17-import-manifest.json (OBSERVED).
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
| Category | Expected | Resolvable | Unrecoverable |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Legend players | 13 instances (10 defs) | 0 | 13 (need .105 Legends locale names) |
|
||||||
|
| Consumables | 17 | 17 | 0 |
|
||||||
|
| Staff | 3 | 3 | 0 |
|
||||||
|
|
||||||
|
## Consumables (17/17 RESOLVABLE — carddbid present in fcc tables; semantics from cardsubtypeid)
|
||||||
|
| wire_id | resourceId | subtype | kind | table |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 100000239 | 5003012 | 54 | gk_training | fcc_trainingcards |
|
||||||
|
| 100000249 | 5003011 | 54 | gk_training | fcc_trainingcards |
|
||||||
|
| 100000260 | 5003004 | 52 | gk_training | fcc_trainingcards |
|
||||||
|
| 100000272 | 5003059 | 91 | position/playstyle mod | fcc_trainingcards |
|
||||||
|
| 100000250 | 5003060 | 92 | position/playstyle mod | fcc_trainingcards |
|
||||||
|
| 100000327 | 5003065 | 97 | position/playstyle mod | fcc_trainingcards |
|
||||||
|
| 100000238 | 5003066 | 98 | position/playstyle mod | fcc_trainingcards |
|
||||||
|
| 100000261 | 5003068 | 100 | position/playstyle mod | fcc_trainingcards |
|
||||||
|
| 100000316 | 5003068 | 100 | position/playstyle mod | fcc_trainingcards |
|
||||||
|
| 100000293 | 5003103 | 258 | player_playstyle | fcc_trainingcards |
|
||||||
|
| 100000326 | 5003112 | 267 | player_playstyle | fcc_trainingcards |
|
||||||
|
| 100000283 | 5003116 | 271 | gk_playstyle | fcc_trainingcards |
|
||||||
|
| 100000294 | 5001004 | 201 | player_contract | fcc_contractcards |
|
||||||
|
| 100000304 | 5001008 | 202 | manager_contract | fcc_contractcards |
|
||||||
|
| 100000305 | 5001009 | 202 | manager_contract | fcc_contractcards |
|
||||||
|
| 100000315 | 5002027 | 217 | healing | fcc_healingcards |
|
||||||
|
| 100000426 | 5002013 | 213 | healing | fcc_healingcards |
|
||||||
|
|
||||||
|
## Staff (3/3 RESOLVABLE)
|
||||||
|
| wire_id | resourceId | subtype | role | table |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 100000271 | 3000083 | 8 | fitnesscoach | fitnesscoachcards |
|
||||||
|
| 100000427 | 3000083 | 8 | fitnesscoach | fitnesscoachcards |
|
||||||
|
| 100000282 | 9000081 | 6 | gkcoach | gkcoachcards |
|
||||||
|
|
||||||
|
## Legends (13 instances / 10 defs — UNRECOVERABLE from .120)
|
||||||
|
All defer via NoName gate (openfut-import-fifa17/src/lib.rs:476-478). 6 assets absent
|
||||||
|
from players.json; 236250/236253/236257 resolve only to placeholder nameid 24313='171918'.
|
||||||
|
dcplayernames.json/editedplayernames.json EMPTY in .120 dump. Version formula holds for all
|
||||||
|
(resourceId == (version<<24)|assetId) — so they auto-promote the instant a .105-derived
|
||||||
|
name row exists; NO code change needed, only name data.
|
||||||
|
|
||||||
|
| asset | resourceId(ver) | rareflag/rating/pos | wire copies |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 169193 | 169193(v0) | 1/r87/CDM | 100000057,100000083,100000146,100000157 |
|
||||||
|
| 211029 | 211029(v0) | 1/r73/CB | 100000165 |
|
||||||
|
| 224512 | 224512(v0) | 1/r67/LB | 100000170 |
|
||||||
|
| 227403 | 227403(v0) | 1/r64/ST | 100000160 |
|
||||||
|
| 236743 | 236743(v0) | 1/r60/LB | 100000163 |
|
||||||
|
| 237510 | 237510(v0) | 1/r59/CB | 100000162 |
|
||||||
|
| 236250 | 17013466(v1) | 3/r78/ST | 100001102 |
|
||||||
|
| 236253 | 17013469(v1) | 3/r77/CAM | 100001932 |
|
||||||
|
| 236253 | 100899549(v6) | 21/r77/CAM | 100001472 |
|
||||||
|
| 236257 | 117676769(v7) | 22/r78/LM | 100001043 |
|
||||||
|
|
||||||
|
### .105 read-only verdict (PROVEN 2026-08-14) — UNRECOVERABLE, confirmed
|
||||||
|
SSH read-only to the FIFA machine (10.10.0.105). Install `/mnt/games/FIFA 17`
|
||||||
|
is retail PC (FIFA17.exe, build 2017-06-09, changelist 3175939, sku FFA17PCC).
|
||||||
|
The resident FIFA17.exe database (recon `data/tables`) shows the DLC/Legend name
|
||||||
|
tables ship ZERO rows in the retail PC build:
|
||||||
|
- `dcplayernames.json`: rowcount:0, rows_emitted:0 (nameid range 30000-35000 = DLC names)
|
||||||
|
- `editedplayernames.json`: rowcount:0, rows_emitted:0
|
||||||
|
- `playernames.json`: 24314 rows (standard players only)
|
||||||
|
FUT Legends were Xbox-One-exclusive in FIFA 17; the PC client contains no Legend
|
||||||
|
names at all. The 13 Legend instances are therefore GENUINELY UNRECOVERABLE from
|
||||||
|
any PC install (.105 == .120, same retail build) — NOT a filtering artifact and
|
||||||
|
NOT fabricated. Architecture is already correct: they auto-promote the instant a
|
||||||
|
name row is supplied (e.g. from an Xbox FIFA17 DB), with no importer code change.
|
||||||
|
## Implementation
|
||||||
|
- Consumables+staff: NEW emit path in openfut-import-fifa17 (classify already buckets;
|
||||||
|
plan_definitions only ingests PlayerCard). Add definition builders + Core CardDefinition
|
||||||
|
rows to fifa17-production-cards.json + owned instances in apply.rs. Data 100% present.
|
||||||
|
- Legends: attempt read-only .105 Legends locale research; if names recoverable add roster
|
||||||
|
rows (auto-promote). Otherwise document as unrecoverable (do NOT fabricate names).
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# FIFA 17 "FUT Squad Update" download failure — root cause
|
||||||
|
|
||||||
|
**Status:** root-caused, not yet fixed. Blocks any FIFA session pointed at a stack whose
|
||||||
|
roster server presents the current self-signed certificate.
|
||||||
|
|
||||||
|
**Symptom (client):** entering the FUT hub shows
|
||||||
|
|
||||||
|
> An error occurred downloading the FUT Squad Update. Please try again.
|
||||||
|
|
||||||
|
Recovered from the live client's memory with `scripts/client-error-string.py`, not from a
|
||||||
|
log. Nothing server-side reports an error: every UTAS route answers 200 and the Blaze
|
||||||
|
session stays healthy, answering PINGs while the client sits on the dialog.
|
||||||
|
|
||||||
|
## What it is
|
||||||
|
|
||||||
|
"Squad Update" here is the **roster update**, not the player's lineup. The Blaze responder
|
||||||
|
advertises it as the FUT loading gate (`blaze_responder_v3b.py`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
ROSTER_HOST = "%s:8081" % _ADVERTISE
|
||||||
|
("ROSTERUPDATE_URL", "https://%s/fifa17/fut/rosterupdate.xml" % ROSTER_HOST)
|
||||||
|
("ROSTER_URL", "https://%s/fifa17/roster/" % ROSTER_HOST)
|
||||||
|
```
|
||||||
|
|
||||||
|
The client fetches that URL over TLS, **by IP**, from `roster_server.py`.
|
||||||
|
|
||||||
|
## Root cause
|
||||||
|
|
||||||
|
The client completes TCP, sends a ClientHello, receives the certificate, and then
|
||||||
|
**aborts the handshake itself**. Captured and decoded from the wire:
|
||||||
|
|
||||||
|
```
|
||||||
|
CLIENT: ClientHello client_version = TLS1.2
|
||||||
|
offers 8 suites: RSA_AES_256_GCM_SHA384, RSA_AES_128_GCM_SHA256,
|
||||||
|
RSA_AES_256_CBC_SHA256, RSA_AES_128_CBC_SHA256, RSA_AES_256_CBC_SHA,
|
||||||
|
RSA_AES_128_CBC_SHA, RSA_RC4_128_SHA, RSA_RC4_128_MD5
|
||||||
|
SERVER: ServerHello TLS1.2 -> Certificate (909 B) -> ServerHelloDone
|
||||||
|
CLIENT: Alert level=fatal desc=certificate_unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
Protocol and cipher selection are **compatible** — TLS1.2 with an RSA suite is offered and
|
||||||
|
chosen. The client rejects the **certificate**:
|
||||||
|
|
||||||
|
```
|
||||||
|
subject/issuer = CN = winter15.gosredirector.ea.com (self-signed, CA:TRUE)
|
||||||
|
SAN = DNS:winter15.gosredirector.ea.com, DNS:*.gosredirector.ea.com, DNS:*.ea.com
|
||||||
|
notBefore = Aug 13 01:12:30 2026
|
||||||
|
sha256 = FF:66:69:48:E2:02:AB:F4:A4:40:78:B1:8C:E1:F9:65:96:32:BB:FF:1F:3D:CC:BC:07:54:02:11:8F:4D:12:3F
|
||||||
|
```
|
||||||
|
|
||||||
|
Two facts matter:
|
||||||
|
|
||||||
|
1. **The SAN carries DNS names only, no `iPAddress` entry**, while the advertised URL is an
|
||||||
|
IP literal (`https://10.10.0.120:8081/...`). A DNS-only SAN cannot match an IP dial.
|
||||||
|
2. **The certificate was regenerated on Aug 13.** `roster_server.py` loads the same
|
||||||
|
`redir_cert.pem`/`redir_key.pem` as the Blaze redirector, so the redirector rejects
|
||||||
|
identically — visible as `REDIR REJECTED ... TLS/SSL connection has been closed (EOF)`
|
||||||
|
in the Blaze log. That is tolerated only because the client then reaches Blaze main over
|
||||||
|
plain TCP; the roster fetch has no such fallback, so it is where the failure surfaces.
|
||||||
|
|
||||||
|
`roster_server.py` still carries the comment *"ProtoSSL cert-verify is patched (autopatch),
|
||||||
|
so our self-signed cert is accepted."* That assumption no longer holds for this path.
|
||||||
|
autopatch demonstrably patched both gates in the failing process — the log shows
|
||||||
|
`pid 56298: PATCHED cert gates` and the live bytes read back as the patched patterns
|
||||||
|
(`909090909090` at `0x146132548`, `31c0c3` at `0x1461361b0`) — and the client still sent
|
||||||
|
`certificate_unknown`. So **those two gates do not govern this validation**; either another
|
||||||
|
check exists on the roster path, or it validates before reaching them.
|
||||||
|
|
||||||
|
## Ruled out, with evidence
|
||||||
|
|
||||||
|
| Hypothesis | Evidence against |
|
||||||
|
| --- | --- |
|
||||||
|
| Missing/mistyped UTAS field | 0 structural differences across 14 hub routes vs production (`scripts/hub-diff-prod-staging.py`) |
|
||||||
|
| Player squad shape | matches production field-for-field, including `squadType`, `custom`, `chemistry`, `manager` |
|
||||||
|
| Squad save | PUT/GET round-trips exactly, every field and slot |
|
||||||
|
| Cert gates unpatched | autopatch log + live byte read both confirm patched |
|
||||||
|
| Roster server broken | serves `200`, `application/xml` over TLS1.2 `AES256-GCM-SHA384` |
|
||||||
|
| Advertised host wrong | production and staging Blaze envs are identical for `OPENFUT_ADVERTISE`, `POW_*`; the scripts are byte-identical |
|
||||||
|
| TLS version mismatch | client offers TLS1.2 and the server selects it |
|
||||||
|
| Blaze session | healthy, PING/PING_REPLY throughout |
|
||||||
|
|
||||||
|
## Probing gotcha
|
||||||
|
|
||||||
|
A default modern Python TLS context gets `SSLV3_ALERT_HANDSHAKE_FAILURE` against this
|
||||||
|
server and looks like a server fault. It is not — the server requires legacy-compatible
|
||||||
|
cipher selection. Probe with `ALL:@SECLEVEL=0`, and note it rejects TLS1.0/1.1 outright
|
||||||
|
(`TLSV1_ALERT_PROTOCOL_VERSION`), accepting only TLS1.2.
|
||||||
|
|
||||||
|
## Why production appears unaffected
|
||||||
|
|
||||||
|
Unresolved, and worth stating plainly rather than guessing. Production advertises the same
|
||||||
|
URL, backed by the same server and certificate, so the same rejection should occur. The
|
||||||
|
operator reports production is clean. The likely explanation is that a production client
|
||||||
|
does not re-fetch the roster (already satisfied), while a session against a different Blaze
|
||||||
|
forces the fetch — but that has not been measured. The known-good hook log used as a
|
||||||
|
control (Aug 12 19:13) **predates the Aug 13 certificate regeneration**, so it is not
|
||||||
|
evidence about the current certificate.
|
||||||
|
|
||||||
|
## Fix options (none applied)
|
||||||
|
|
||||||
|
1. **Reissue the certificate with an `iPAddress` SAN** for the advertised address. Smallest
|
||||||
|
change, addresses the concrete mismatch, and benefits the redirector too. Note
|
||||||
|
`openfut-tls` exists precisely so the redirector and roster hosts cannot configure TLS
|
||||||
|
separately — the 2026-08-11 mismatch it was written for is the same class of bug.
|
||||||
|
2. **Serve the roster over plain HTTP** for an isolated stack, by pointing
|
||||||
|
`ROSTERUPDATE_URL`/`ROSTER_URL` at a staging-owned HTTP server. Sidesteps TLS entirely;
|
||||||
|
only acceptable off production.
|
||||||
|
3. **Find the real validation site** on the roster path and extend the patch set. Most
|
||||||
|
faithful, most work, and the only option that restores the original assumption.
|
||||||
|
|
||||||
|
Option 1 is the recommended starting point because it is testable without the client:
|
||||||
|
a probe that validates hostname/IP against the presented certificate will fail before the
|
||||||
|
fix and pass after.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,241 @@
|
|||||||
|
# FIFA 17 seller-facing SOLD state — static recovery from CardsDLL
|
||||||
|
|
||||||
|
Reverse engineering of `CardsDLL_Win64_retail.dll`
|
||||||
|
(md5 `4de3493131d7d2ff7f8b360c5ac9b655`), Ghidra 12.1.2 headless via pyghidra,
|
||||||
|
13,382 functions, image base `0x180000000`. Queries and raw output:
|
||||||
|
`docs/evidence/market-sold-re-2026-08-17/`.
|
||||||
|
|
||||||
|
Confidence tags: **[PE]** read from the binary · **[PE-PROOF]** a proven *negative*
|
||||||
|
(an exhaustive absence) · **[PLAN]** corpus prose, not a decompile · **[UNDECIDABLE]**
|
||||||
|
proven not answerable from this binary.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. There is no `sold` token, and now it is exhaustive
|
||||||
|
|
||||||
|
Both vocabularies dumped in full to their sentinels, not sampled. **[PE]**
|
||||||
|
|
||||||
|
`tradeState` table `0x180229e40` — exactly 4 rows, then a `{0,0}` terminator:
|
||||||
|
|
||||||
|
```text
|
||||||
|
active=1 inactive=2 expired=3 closed=4
|
||||||
|
```
|
||||||
|
|
||||||
|
`itemState` table `0x180229cc0` — exactly 12 rows, then a `{0,-1}` terminator:
|
||||||
|
|
||||||
|
```text
|
||||||
|
invalid=0 free=1 WAITING_FOR_GAME=2 inGame=2 forSale=5 offered=6
|
||||||
|
activeBadge=100 activeHomeKit=101 activeAwayKit=102 activeBall=103
|
||||||
|
activeStadium=104 active=255
|
||||||
|
```
|
||||||
|
|
||||||
|
Neither contains `sold`. So the seller's sold state **must** be a combination of
|
||||||
|
existing atoms. This closes the question that previously rested on a partial dump.
|
||||||
|
|
||||||
|
## 2. What `closed` actually does — the complete flag computation
|
||||||
|
|
||||||
|
Decompiled from the `auctionInfo` deserializer `0x18013e410`. `local_70` is
|
||||||
|
`tradeState`, `local_40` is `bidState`. **[PE]**
|
||||||
|
|
||||||
|
```c
|
||||||
|
if (local_70 == 4) { /* tradeState == closed */
|
||||||
|
local_3b = local_40 != 0; /* bidState != none */
|
||||||
|
} else {
|
||||||
|
local_3b = (local_40 - 1U & 0xfffffffd) == 0; /* bidState in {1,3} */
|
||||||
|
}
|
||||||
|
local_3a = local_40 - 2U < 2; /* bidState in {2,3} */
|
||||||
|
```
|
||||||
|
|
||||||
|
Evaluated:
|
||||||
|
|
||||||
|
| | `none`(0) | `outbid`(1) | `highest`(2) | `buyNow`(3) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `local_3b`, tradeState==closed | 0 | **1** | **1** | **1** |
|
||||||
|
| `local_3b`, otherwise | 0 | 1 | 0 | 1 |
|
||||||
|
| `local_3a` (any tradeState) | 0 | 0 | **1** | **1** |
|
||||||
|
|
||||||
|
## 3. The complete record → Flash mapping
|
||||||
|
|
||||||
|
From the publisher `0x1801bf030`, every property it sets, with its record offset.
|
||||||
|
This supersedes the previous partial list. **[PE]**
|
||||||
|
|
||||||
|
| Flash property | record | meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `TRADEID_LOWER` / `TRADEID_UPPER` | +0x38 | tradeId, split into two 32-bit halves |
|
||||||
|
| `DURATION` | +0x90 | formatted; **`FUT_AUCTION_EXPIRED`** when the value underflows (i.e. `expires == 0`) |
|
||||||
|
| `TIME_REMAINING` | +0x90 | expires, seconds |
|
||||||
|
| `MIN_CREDITS` | +0x78 | currentBid |
|
||||||
|
| `MAX_CREDITS` | +0x70 | buyNowPrice |
|
||||||
|
| `RESERVEDPRICE` | +0x74 | startingBid |
|
||||||
|
| `YOURBID` | +0xb8 | **bidState, passed through verbatim** |
|
||||||
|
| `STATE` | +0x88 | **tradeState, passed through verbatim** |
|
||||||
|
| **`COINS_AWARDED`** | **+0xbf** | **the `coinsProcessed` atom (0x2f4), u8** |
|
||||||
|
| `UUID_UPPER` / `UUID_LOWER` | itemData+8 | |
|
||||||
|
| `CARD_ID` / `FIFA_ID` | itemData+0x18 | `FIFA_ID` masks `& 0xffffff` |
|
||||||
|
| `CARD_TYPE` | itemData+0x4c | |
|
||||||
|
| `CARD_OFFERSTATE` | itemData+0x5c | itemState |
|
||||||
|
| `IS_WATCHED` | +0xbc | `watched` atom |
|
||||||
|
| `INBOX` | +0xbe | `local_3a` — bidState ∈ {highest, buyNow} |
|
||||||
|
| `IS_GLOW` | +0xbd | `local_3b` — the table in §2 |
|
||||||
|
| `TRADE_DATA_AVAILABLE` | — | constant 1 |
|
||||||
|
|
||||||
|
**`coinsProcessed`'s consumer is now traced.** The corpus recorded its type and
|
||||||
|
noted that no consumer had ever been found; it is published to the movie as
|
||||||
|
**`COINS_AWARDED`**. That is a settlement/"you have been paid" signal, exactly as
|
||||||
|
the corpus guessed but never demonstrated.
|
||||||
|
|
||||||
|
## 4. `highest` vs `buyNow` on a closed row is UNDECIDABLE from CardsDLL
|
||||||
|
|
||||||
|
**[UNDECIDABLE]**, and this is a proof, not a failed search.
|
||||||
|
|
||||||
|
For `tradeState == closed`, §2 gives `IS_GLOW = (bidState != none)` and
|
||||||
|
`INBOX = (bidState ∈ {highest, buyNow})`. Both `highest`(2) and `buyNow`(3)
|
||||||
|
therefore produce **`IS_GLOW=1, INBOX=1`** — bit-identical. No native consumer can
|
||||||
|
tell them apart.
|
||||||
|
|
||||||
|
But §3 sharpens *why* it is undecidable: `bidState` is **not** consumed only through
|
||||||
|
those flags. It is published verbatim as `YOURBID`, alongside `STATE` and
|
||||||
|
`COINS_AWARDED`. The movie receives the raw values. So the discrimination exists —
|
||||||
|
it just lives entirely in the APT/ActionScript front end, which is unread.
|
||||||
|
|
||||||
|
Consequence: **no amount of further CardsDLL work can answer "which `bidState` does
|
||||||
|
a seller see".** Only an AVM1 read of `external/ion_fut/screens/trading/tradepile`,
|
||||||
|
or a live behavioural A/B, can. This retires the question as a static target.
|
||||||
|
|
||||||
|
The corpus's own lifecycle table (`fifa17-recon/docs/plan-2026-08-06-transfer-market.md:720-729`)
|
||||||
|
asserts the seller sees `closed` / **`highest`** with `coinsProcessed 1`, and assigns
|
||||||
|
`closed` / `buyNow` to the *buyer*. That is **[PLAN]**, and it **contradicts** the
|
||||||
|
common third-party lore that a sold seller row is `closed` + `buyNow`. Given §4 the
|
||||||
|
contradiction cannot be resolved statically — but note the corpus reading is the one
|
||||||
|
that leaves `buyNow` meaning "*I* bought it now", which is self-consistent with
|
||||||
|
`YOURBID` being a property about the viewer's own bid.
|
||||||
|
|
||||||
|
## 5. The clear-sold verb EXISTS — PE-proven
|
||||||
|
|
||||||
|
The request builder `0x1801647c0`: **[PE]**
|
||||||
|
|
||||||
|
```c
|
||||||
|
if (*(longlong *)(param_1 + 0x10) == 0) {
|
||||||
|
FUN_180007f80(&local_38, 0x20, "/sold"); /* no tradeId -> bulk */
|
||||||
|
} else {
|
||||||
|
FUN_180007f80(&local_38, 0x20, "/%lld"); /* one specific tradeId */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
One builder, two forms, on route base `ut/delete/%s/trade` (`DELETETRADE`), response
|
||||||
|
class `RS4:FutISRemoveTradeServerResponse` (`0x180228bc8`, with the `/sold` literal
|
||||||
|
at `0x180228bec` immediately after it):
|
||||||
|
|
||||||
|
```text
|
||||||
|
DELETE ut/delete/{ns}/trade/{tradeId} remove one trade
|
||||||
|
DELETE ut/delete/{ns}/trade/sold remove ALL sold trades
|
||||||
|
```
|
||||||
|
|
||||||
|
Corroborated by the client's own request-name table, where
|
||||||
|
**`RemoveAllSoldFromTradePile`** (`0x1801efae8`) sits beside `RemoveFromTradePile`,
|
||||||
|
`AddToWatchList` and `RemoveFromWatchList`.
|
||||||
|
|
||||||
|
**Architectural consequence.** A bulk "remove all sold" verb only makes sense if
|
||||||
|
sold rows **persist in the seller's pile until explicitly cleared**. That is
|
||||||
|
incompatible with our current host, where a sold/cancelled listing leaves
|
||||||
|
`/tradePile` the instant the CAS lands (the Fix A invariant). Implementing the sold
|
||||||
|
path will require revisiting that invariant — and doing so needs live validation,
|
||||||
|
because Fix A itself was a live-confirmed correction.
|
||||||
|
|
||||||
|
## 6. The seller-facing SOLD counter is real, and we hardcode it to 0
|
||||||
|
|
||||||
|
A complete chain, wire atom → struct → Flash → localised caption, with no inference
|
||||||
|
at any step. **[PE]**
|
||||||
|
|
||||||
|
Hub `tradePile` sub-deserializer `0x18013ead0`:
|
||||||
|
|
||||||
|
| atom | id | writes | Flash slot (publisher `0x1800b1dc0`, tile `0x1c0`) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `count` | 0xbc | +0x1d4 | `TEXT0` with caption `FUT_UC_ITEMS` |
|
||||||
|
| `notification` | 0x1da | +0x1d6 | `NOTIFICATION`, capped at 99 |
|
||||||
|
| `selling` | 0x2b8 | +0x1d2 (**and** counts-struct +0x36) | `TEXT2` with caption `FUT_TF_SELLING` |
|
||||||
|
| **`sold`** | **0x2c9** | **+0x1d8** | **`TEXT3` with caption `FUT_TF_SOLD`** |
|
||||||
|
|
||||||
|
The sibling tile `0x1d0` (Transfer Targets) uses `FUT_TF_WINNING` (+0x1c8) and
|
||||||
|
`FUT_TF_OUTBID`. `selling` writing two structs independently re-confirms the
|
||||||
|
corpus's counts-struct offset for `selling`.
|
||||||
|
|
||||||
|
So FIFA 17 renders a **SOLD** count to the seller, sourced from atom `sold` (0x2c9).
|
||||||
|
Our host and the Python oracle both hardcode `sold: 0`, so that bucket can never
|
||||||
|
populate. This is direct client evidence about `sold` — the thing
|
||||||
|
`MARKET_SOLD_SETTLEMENT.md` required before touching `/tradePile/counts`. It
|
||||||
|
establishes that `sold` is *displayed*; it does **not** yet establish what should be
|
||||||
|
counted in it (rows awaiting clear? rows sold this session?).
|
||||||
|
|
||||||
|
## 7. Atom-name decoder (method note, reusable)
|
||||||
|
|
||||||
|
Atom IDs are **indices into an alphabetically sorted pointer table** of atom-name
|
||||||
|
strings, base `0x1802d2760`. Validated against all twelve known `auctionInfo`
|
||||||
|
atoms — 12/12 agree — and cross-checked against `fifa17-recon/docs/fut_atoms.tsv`.
|
||||||
|
**[PE]**
|
||||||
|
|
||||||
|
```text
|
||||||
|
atom_id = (pointer_slot_address - 0x1802d2760) / 8
|
||||||
|
```
|
||||||
|
|
||||||
|
Newly resolved: `sold`=0x2c9, `count`=0xbc, `offered`=0x1e5, `selling`=0x2b8,
|
||||||
|
`maxAuctionsAllowed`=0x1bf, `credits`=0xc0, `auctionInfo`=0x35, `total`=0x325,
|
||||||
|
`duplicateItemIdList`=0xec, `itemState`=0x172, `offers`=0x1e6, `coins`=0x95.
|
||||||
|
|
||||||
|
Caution when using it: look up a name by finding the pointer slot **inside the table
|
||||||
|
range**, not by taking the first matching string in the binary. Common words such as
|
||||||
|
`sold`, `offered` and `count` appear in several unrelated tables, and taking the
|
||||||
|
first hit produces confident nonsense (it initially reported `sold` as ABSENT and
|
||||||
|
`offered` as a negative index).
|
||||||
|
|
||||||
|
## 8. An auction-outcome vocabulary exists but this client ignores it
|
||||||
|
|
||||||
|
The atom table contains a 7-value outcome vocabulary: **[PE]**
|
||||||
|
|
||||||
|
```text
|
||||||
|
0x36 auctionLostBidRejected 0x39 auctionSoldBid 0x3b auctionWonBid
|
||||||
|
0x37 auctionLostOutbid 0x3a auctionSoldBuyNow 0x3c auctionWonBuyNow
|
||||||
|
0x38 auctionLostOutbidSelf
|
||||||
|
```
|
||||||
|
|
||||||
|
These would distinguish seller-sold-by-bid from seller-sold-by-buy-now explicitly.
|
||||||
|
**No CardsDLL deserializer consumes them.** Every candidate function that compares
|
||||||
|
against three or more of `0x36..0x3c` was checked and none is an atom dispatcher
|
||||||
|
(no value-SKIP `0x180135ff0`, no atom loop `0x1801c7f10`) — they are small-immediate
|
||||||
|
coincidences. **[PE-PROOF]** So the vocabulary is server-side or telemetry, and it
|
||||||
|
does not carry the sold state to this client.
|
||||||
|
|
||||||
|
## 9. The 5% fee is NOT in the client — Task B is undecidable from here
|
||||||
|
|
||||||
|
**[PE-PROOF]**, three independent absences:
|
||||||
|
|
||||||
|
* no `0.95` or `0.05` constant, `double` or `float`, anywhere in the binary;
|
||||||
|
* no localisation key for tax/fee/net/proceeds/commission/"you will receive" — the
|
||||||
|
only `FUT_TF_*` keys in the binary are `SELLING`, `SOLD`, `WINNING`, `OUTBID`;
|
||||||
|
* the 17 functions using both `5`/`95` and `100` as immediates are all unrelated
|
||||||
|
(they include the item deserializer) — no fee arithmetic exists.
|
||||||
|
|
||||||
|
The client therefore never computes or displays a net. It learns the seller's
|
||||||
|
balance only from `credits`.
|
||||||
|
|
||||||
|
**This makes the rounding rule unmeasurable by experiment against our own server.**
|
||||||
|
Whatever we credit is what the client displays; there is no client-side expectation
|
||||||
|
to compare against, so there is no oracle. The only evidence that could settle
|
||||||
|
floor-the-fee (150 → 143) versus floor-the-proceeds (150 → 142) is an original
|
||||||
|
EA-era capture of a seller's balance across a known-price sale, which we do not
|
||||||
|
have.
|
||||||
|
|
||||||
|
Accordingly the rule stays a **documented choice**: fee = `floor(gross × 5 / 100)`,
|
||||||
|
proceeds = `gross − fee`, chosen because `fee + proceeds == gross` holds exactly at
|
||||||
|
every input. It is pinned by regression tests at 0, 1, 19, 20, 21, 39, 40, 100, 101,
|
||||||
|
119, 120, 149, 150, 151, 199, 200, 1 000, 15 000, 15 000 000 and `i64::MAX`, so it
|
||||||
|
cannot drift silently.
|
||||||
|
|
||||||
|
## 10. What would close the remaining gaps
|
||||||
|
|
||||||
|
| Gap | Only remaining route |
|
||||||
|
|---|---|
|
||||||
|
| `highest` vs `buyNow` on a seller's sold row | AVM1 disassembly of `tradepile.isInActiveAuction` / `PreCheckCardOptions`, or a live behavioural A/B (the movie gets `YOURBID` verbatim, so the two ARE separable by the client's behaviour) |
|
||||||
|
| What `sold` should count | live observation with a real sold row |
|
||||||
|
| Whether sold rows persist until cleared | live: does the client issue `DELETE .../trade/sold`? |
|
||||||
|
| The 5% rounding rule | an original EA-era seller-balance capture; nothing in reach |
|
||||||
@@ -0,0 +1,887 @@
|
|||||||
|
# FIFA 17 Transfer Market — wire findings
|
||||||
|
|
||||||
|
Reverse-engineering record for the FIFA 17 UTAS transfer-market surface, kept so
|
||||||
|
future agents do not reopen settled questions or re-guess enum spellings.
|
||||||
|
|
||||||
|
Every claim carries a confidence tag:
|
||||||
|
|
||||||
|
| Tag | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| **CONFIRMED** | Observed from our own FIFA17.exe client or live host capture |
|
||||||
|
| **FIFA17-HISTORICAL** | Supported by contemporaneous FIFA 17 implementations (`lorenzh/fut-api`, `futapi/fut` v0.2.18 — the last pre-FIFA-18 release) |
|
||||||
|
| **INFERRED** | Best explanation, not directly captured |
|
||||||
|
| **UNKNOWN** | Requires instrumentation; do NOT implement from guesswork |
|
||||||
|
|
||||||
|
Authority reminder: FIFA 17 field names, enum spellings, sentinel ids and
|
||||||
|
empty-state shapes come from captures or the Python oracle — never from a modern
|
||||||
|
FUT toolkit. Later-FIFA API drift is a known hazard, and reversing a container
|
||||||
|
type or inventing an enum is the documented client-freeze class.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The auction record (`auctionInfo[]`)
|
||||||
|
|
||||||
|
What we emit today, on `/tradePile`, `/trade/status` and market browse:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tradeId": 1000000097,
|
||||||
|
"itemData": { "...full shaped card...": "", "itemState": "listFS" },
|
||||||
|
"tradeState": "active",
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"startingBid": 150,
|
||||||
|
"currentBid": 0,
|
||||||
|
"offers": 0,
|
||||||
|
"bidState": "none",
|
||||||
|
"expires": 3600,
|
||||||
|
"tradeOwner": true,
|
||||||
|
"sellerId": 33068179,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"watched": false,
|
||||||
|
"coinsProcessed": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Confidence | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| `tradeOwner` (bool) | **FIFA17-HISTORICAL** | Exists in FIFA 17 auctionInfo. That it is *the* Actions-panel gate is **UNKNOWN** pending live confirmation. |
|
||||||
|
| `sellerId` | **FIFA17-HISTORICAL** exists; type numeric is **INFERRED** | Set to the configured persona so it agrees with `tradeOwner`. Never baked in. |
|
||||||
|
| `sellerName` | **CONFIRMED** it must be the player | `fut_account.py` annotates the persona property as "Blaze PDTL.DSNM / LSX GetProfileResponse Persona / **UTAS sellerName**". EA's `"EASFC"` here is wrong for an own listing. |
|
||||||
|
| `offers` | **FIFA17-HISTORICAL** | `0` valid for active/unbid. |
|
||||||
|
| `bidState: "none"` | **FIFA17-HISTORICAL** | Valid for active/unbid. Other observed concepts: `highest`, `buyNow`. Do NOT "fix" this. |
|
||||||
|
| `expires` | **FIFA17-HISTORICAL** | **SECONDS REMAINING, not an epoch.** Historical durations: 3600, 10800, 21600, 43200, 86400, 259200. |
|
||||||
|
| `itemData.itemState: "listFS"` | **UNKNOWN** | Plausible and unchanged. Public FIFA 17 material gives no trustworthy enumeration. Do not guess replacements — capture. |
|
||||||
|
| `itemData.untradeable` | **FIFA17-HISTORICAL** field; our blanket `false` is **INFERRED** | See "Known debt" below. |
|
||||||
|
| `marketDataMinPrice` / `marketDataMaxPrice` | **do NOT add** | These entered the public parser only after its FIFA 18 migration. |
|
||||||
|
|
||||||
|
### Why the differential could not catch the missing fields
|
||||||
|
|
||||||
|
Our record's key set was **identical to the Python oracle's**, so field-for-field
|
||||||
|
parity was green. The oracle omits `tradeOwner` / `sellerId` / `offers` as well,
|
||||||
|
because *its* remove flow was never driven by a real client either — the only
|
||||||
|
historical live datapoint is a counts-tile bug. Oracle parity is therefore
|
||||||
|
**necessary but not sufficient** for any flow the oracle never actually served.
|
||||||
|
|
||||||
|
The differential now asserts we cover every oracle key AND that our extra keys are
|
||||||
|
exactly `{offers, sellerId, tradeOwner}`, so the deliberate superset is pinned
|
||||||
|
while a new unexplained divergence still fails.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Routes
|
||||||
|
|
||||||
|
| Route | Confidence | Behaviour |
|
||||||
|
|---|---|---|
|
||||||
|
| `GET …/trade/status` | **CONFIRMED** the client polls it continuously | Live auction-state refresh. It previously fell through `starts_with("trade")` into the buy/view arm, where the tail has no numeric id, so **every poll returned `{"auctionInfo": []}`**. Now a real handler: optional `tradeIds` filter, else the whole active pile. Unknown ids are absent, never an error. |
|
||||||
|
| `DELETE /ut/game/<sku>/trade/<id>` | **FIFA17-HISTORICAL** | The spelling contemporaneous FIFA 17 clients use, no body, no meaningful response body. Previously landed in the buy/view arm and **silently cancelled nothing while returning 200.** Now maps to MarketCancel. |
|
||||||
|
| `DELETE /ut/delete/game/<sku>/trade/<id>` | **CONFIRMED** (oracle) | The oracle's spelling; retained because the differential exercises it. Whether FIFA17.exe ever uses it is **UNKNOWN**. |
|
||||||
|
| `POST …/auctionhouse` | **CONFIRMED** | List for sale. The client sends only `itemData.id`; the server resolves wire id → Core instance → `card_id`/`resourceId` and enforces ownership. |
|
||||||
|
| `GET …/tradePile/counts` | **INFERRED** | Five scalar ints (`count`, `maxAuctionsAllowed`, `offered`, `selling`, `sold`); a DISTINCT deserializer from `/tradePile`. Exact FIFA 17 semantics of `count` (active auctions vs whole pile) is **UNKNOWN** — we report active auctions and deliberately did NOT speculate. |
|
||||||
|
| `PUT …/item` (move) | **FIFA17-HISTORICAL** | `{"itemData":[{"pile":"trade"|"club","id":ID}]}` → `{"itemData":[{id,pile,success}]}`. Transfer-List membership is a **separate operation from creating an auction**. |
|
||||||
|
|
||||||
|
### Pile encoding
|
||||||
|
|
||||||
|
* MOVE commands take a **string** pile (`"trade"`, `"club"`) — **FIFA17-HISTORICAL**.
|
||||||
|
* Returned `itemData.pile` is documented **numeric** in FIFA 17 auction data — **FIFA17-HISTORICAL**.
|
||||||
|
* The numeric mapping is **UNKNOWN**. Do not unify the two representations, and do
|
||||||
|
not derive a mapping from unrelated `pileSize` keys.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q2 — Transfer List item that is not currently auctioned
|
||||||
|
|
||||||
|
A real FUT state: an item in the Transfer List with no active auction (freshly
|
||||||
|
moved, or expired unsold). **CONFIRMED** to exist as a concept (the external hub
|
||||||
|
spec §27, and move-vs-list being separate operations).
|
||||||
|
|
||||||
|
Its wire representation is **UNKNOWN**: `tradeId` 0 / omitted / null, `tradeState`
|
||||||
|
value or omission, and `itemData.itemState` are all unestablished.
|
||||||
|
|
||||||
|
Consequence, and the reason this matters: our `/tradePile` renders only `active`
|
||||||
|
listings, so keying the `/club` exclusion on the `trade` **pile** stranded 4 cards
|
||||||
|
in no screen at all (hidden from the club, absent from the Transfer List).
|
||||||
|
Commit `f2c4927` keys exclusion on the **active listing** instead, which is
|
||||||
|
self-healing. That is a workaround, not fidelity — the faithful model needs the
|
||||||
|
unlisted state represented.
|
||||||
|
|
||||||
|
**Required capture** (four states, full structural diff, not just a shortlist):
|
||||||
|
|
||||||
|
```
|
||||||
|
A. moved Club -> Transfer List, NEVER listed
|
||||||
|
B. actively listed
|
||||||
|
C. listing expired unsold
|
||||||
|
D. listing sold
|
||||||
|
```
|
||||||
|
|
||||||
|
Diff at least: `tradeId`, `tradeOwner`, `tradeState`, `bidState`, `expires`,
|
||||||
|
`offers`, `currentBid`, `startingBid`, `buyNowPrice`, `sellerId`, `sellerName`,
|
||||||
|
`itemData.id`, `itemData.itemState`, `itemData.pile`, `itemData.untradeable`.
|
||||||
|
|
||||||
|
Do NOT drop the unlisted state from the model just because its encoding is unknown.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deferred, with reasons
|
||||||
|
|
||||||
|
* **5% transfer tax** — **INFERRED** architecture only: auction closes → Core
|
||||||
|
settles → seller credited gross × 0.95, with `auctionInfo` continuing to carry
|
||||||
|
gross. No trustworthy FIFA 17 field named `tax`/`netPrice`/`sellerProceeds` was
|
||||||
|
recovered, and no separate settle operation. Not blocking; do not couple
|
||||||
|
settlement to clearing the sold auction without a capture.
|
||||||
|
* **Bid / Transfer Targets** — not implemented. Watched / active bid / winning /
|
||||||
|
outbid / won / expired are distinct states and must not collapse to a flat list.
|
||||||
|
* **Unassigned** — FIFA 17 had a dedicated Unassigned service; the exact FIFA 17
|
||||||
|
URL is **UNKNOWN**. Our 29-item `purchased` pile is this state and is currently
|
||||||
|
rendered inside `/club`. Do not manufacture a route from a modern toolkit.
|
||||||
|
* **Match CREATE / READY / PLAY** — **UNKNOWN** and explicitly not portable from
|
||||||
|
public FUT web-app work (the web app could not start matches). Instrument the
|
||||||
|
real client from Play Match to kickoff before implementing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known debt
|
||||||
|
|
||||||
|
`shape_item` reports `untradeable: false` for **every** owned instance. Correct
|
||||||
|
today (Core models no untradeable items) and necessary — a hardcoded `true` greyed
|
||||||
|
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/<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.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## RESOLVED against the live client — 2026-08-17 (supersedes the tags above)
|
||||||
|
|
||||||
|
Driven by the real FIFA 17 client end to end: list → expire → **relist** → active,
|
||||||
|
with the operator confirming each UI state. Live-client behaviour OUTRANKS both the
|
||||||
|
Python oracle and contemporaneous Web App implementations wherever they disagree.
|
||||||
|
|
||||||
|
### `tradeOwner` — do not re-litigate this
|
||||||
|
|
||||||
|
```text
|
||||||
|
tradeOwner exists in the FIFA 17-era FUT API:
|
||||||
|
FIFA17-HISTORICAL
|
||||||
|
|
||||||
|
tradeOwner required by FIFA17.exe Transfer List Actions:
|
||||||
|
DISPROVEN for the current client path
|
||||||
|
```
|
||||||
|
|
||||||
|
It is absent from the twelve atoms FIFA 17's `auctionInfo` deserializer
|
||||||
|
(`0x18013e410`) reads, so the client value-SKIPs it at `0x180135ff0`. It was
|
||||||
|
implemented, deployed, observed to change nothing, and REMOVED. A future agent
|
||||||
|
rediscovering the old Web App sources will find this note before spending
|
||||||
|
deployments on it again.
|
||||||
|
|
||||||
|
### What actually gated the Actions panel — all lifecycle/state bugs
|
||||||
|
|
||||||
|
```text
|
||||||
|
CONFIRMED — FIFA17.exe live client
|
||||||
|
|
||||||
|
Own Transfer List interactivity does NOT depend on tradeOwner in this path.
|
||||||
|
|
||||||
|
trade/status polling is LOAD-BEARING for Transfer List state. The tail carries no
|
||||||
|
numeric id, so it fell through the `trade…` buy/view arm and answered every poll
|
||||||
|
with an empty auctionInfo. Route is ISVIEWTRADE:
|
||||||
|
GET ut/{ns}/trade/status?tradeIds=a,b,c
|
||||||
|
|
||||||
|
auctionInfo.expires is SECONDS REMAINING *and must evolve with wall-clock time*.
|
||||||
|
A structurally valid but FROZEN value breaks lifecycle behaviour: the auction never
|
||||||
|
ages into expired/relistable, which is the state where Re-list appears.
|
||||||
|
|
||||||
|
Relisting must PERSIST. FIFA 17 relists by re-sending ISStart (POST /auctionhouse)
|
||||||
|
for an item that already has a listing row, so the primary-key conflict IS the
|
||||||
|
relist. Swallowing it as success left the stale expired row intact and produced a
|
||||||
|
client-visible lifecycle failure behind an HTTP 200.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Known-good own active auction (frozen fixture)
|
||||||
|
|
||||||
|
Captured at `docs/evidence/market-lifecycle-2026-08-17/`, including
|
||||||
|
`_index.json._countdown_proof` which records `expires` decrementing (`frozen:
|
||||||
|
false`) so the live clock is machine-checkable rather than asserted in prose.
|
||||||
|
|
||||||
|
```text
|
||||||
|
tradeState = active
|
||||||
|
bidState = none
|
||||||
|
expires = decrementing (seconds remaining)
|
||||||
|
sellerName = CAGE
|
||||||
|
auction record = 12 atoms
|
||||||
|
response envelope = 4 members (auctionInfo, credits, duplicateItemIdList, total)
|
||||||
|
ISViewTrade body = 2 members (auctionInfo, credits)
|
||||||
|
pricelimits = BARE ARRAY (container type is load-bearing)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Do not "improve" this shape without a live-client retest.**
|
||||||
|
|
||||||
|
### The development rule this established
|
||||||
|
|
||||||
|
A response can be structurally plausible, pass differential parity, and render
|
||||||
|
perfectly while still being behaviourally wrong, because FIFA expects an **evolving
|
||||||
|
server-side state machine** rather than a static object that looks like one. The
|
||||||
|
frozen `expires` is the canonical example: every field was the right name, type and
|
||||||
|
vocabulary, and the feature was still broken.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## INVESTIGATION CLOSED — active own auction is not seller-actionable
|
||||||
|
|
||||||
|
**Do not reopen without new direct FIFA17.exe evidence contradicting the lifecycle
|
||||||
|
below.** The correct FIFA 17 lifecycle is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ACTIVE AUCTION tradeState=active, expires>0/counting down
|
||||||
|
-> seller CANNOT withdraw it through Transfer List actions
|
||||||
|
-> the item is not seller-actionable while the auction runs
|
||||||
|
|
||||||
|
EXPIRED UNSOLD tradeState=expired, expires=0
|
||||||
|
-> the item becomes actionable again
|
||||||
|
-> relist / return-to-club / other expired-item actions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Confidence tags
|
||||||
|
|
||||||
|
```text
|
||||||
|
CONFIRMED — live FIFA17.exe:
|
||||||
|
active listing with ticking expires is NON-selectable
|
||||||
|
expired listing IS selectable
|
||||||
|
relisting makes it active and therefore non-selectable again
|
||||||
|
no cancel request is ever emitted by the client
|
||||||
|
expires must advance with the wall clock
|
||||||
|
no button prompt is offered on the Transfer List for an active auction
|
||||||
|
|
||||||
|
CONFIRMED — local RE corpus:
|
||||||
|
every CardsDLL-side prerequisite passes (IS_TRADING_ENABLED=1,
|
||||||
|
TRADE_PILE_SIZE=100, watch-list=50, item+0x49 tradeable)
|
||||||
|
MAY_BE_REMOVED is a CONSTANT 1 — it cannot be the gate and the server
|
||||||
|
cannot move it
|
||||||
|
the eight-flag array FUN_18003e370 publishes is the CLUB-CARD Actions menu
|
||||||
|
and contains NO transfer-auction cancellation flag
|
||||||
|
the auction parser is limited to the known twelve atoms
|
||||||
|
|
||||||
|
HISTORICAL FUT / FIFA17-era:
|
||||||
|
active auctions are committed until sale or expiry
|
||||||
|
expired Transfer List items expose relist/return actions
|
||||||
|
FIFA 17 trading guidance tells players to relist once auctions expire
|
||||||
|
|
||||||
|
UNKNOWN, and no longer required for backend fidelity:
|
||||||
|
the exact Flash/ActionScript branch that makes active cards non-selectable
|
||||||
|
```
|
||||||
|
|
||||||
|
### Explicitly out of scope now
|
||||||
|
|
||||||
|
Do NOT add auction fields, change `itemState`, revisit `tradeOwner`, probe
|
||||||
|
`MAY_BE_REMOVED`, add an active-auction cancel feature, or disassemble Flash in
|
||||||
|
order to make active auctions selectable. Three of those were already tried and
|
||||||
|
refuted; the rest are ruled out above.
|
||||||
|
|
||||||
|
### Return-to-club transition (implemented)
|
||||||
|
|
||||||
|
A pile move to `club` now ENDS any `active` auction on that item. Without it the
|
||||||
|
pile reads `club` while the listing row stays `active`, so the card is filtered out
|
||||||
|
of `/club` (exclusion keys on active listings) AND still rendered in the Transfer
|
||||||
|
List — the move silently appears to do nothing. `reserved` (mid-sale) and `sold`
|
||||||
|
rows are never touched, so a card can never be both sold and returned.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q2 — the unlisted transfer-list item, MEASURED 2026-08-17
|
||||||
|
|
||||||
|
The operator moved a card Club -> Transfer List **without listing it**
|
||||||
|
(`PUT /ut/game/fifa17/item`, no `POST /auctionhouse`). State captured immediately,
|
||||||
|
read-only. Fixture: `docs/evidence/market-lifecycle-2026-08-17/`.
|
||||||
|
|
||||||
|
Server truth at capture: trade pile held **6** items, of which **1** had an active
|
||||||
|
auction and **5** were unlisted.
|
||||||
|
|
||||||
|
| Surface | Unlisted item | Actively-listed item (contrast) |
|
||||||
|
|---|---|---|
|
||||||
|
| `/tradePile` `auctionInfo` | **absent** | present, `tradeState: active` |
|
||||||
|
| `/tradePile/counts` | **not counted** (`count 1, selling 1`) | counted |
|
||||||
|
| `/club` | **present**, `clubPlayers` unchanged at 1965 | **absent** (excluded) |
|
||||||
|
| `itemData.itemState` | `free` | `listFS` |
|
||||||
|
| `itemData.pile` | **field not emitted at all** | not emitted |
|
||||||
|
| `itemData.untradeable` | `false` | `false` |
|
||||||
|
|
||||||
|
**Finding: we do not represent the unlisted state anywhere on the wire.** Such an
|
||||||
|
item is byte-identical to a club item; only the host's own pile store knows it is in
|
||||||
|
the trade pile, and nothing the client receives says so.
|
||||||
|
|
||||||
|
Still **UNKNOWN** (do not guess): the FIFA 17 encoding of that state. The PE
|
||||||
|
documents returned `itemData.pile` as NUMERIC with an unrecovered mapping, and
|
||||||
|
`tradeState` decodes through a closed table walk (`active=1 inactive=2 expired=3
|
||||||
|
closed=4`) where an unrecognised `bidState` is silently swallowed as `none` — so a
|
||||||
|
wrong enum yields a plausible-looking but WRONG UI. The corpus explicitly warns that
|
||||||
|
`inactive` decodes but "no client path treats it specially; do not emit it".
|
||||||
|
|
||||||
|
Open discriminator, needs the client only: after backing out of FUT and re-entering,
|
||||||
|
does the Transfer List still show an unlisted item? If it does not, the move is not
|
||||||
|
durable from the client's point of view and the state must be represented; if it
|
||||||
|
does, `tradePile` is auctions-only by design and there is nothing to fix.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Domain boundary: pile membership vs auction lifecycle
|
||||||
|
|
||||||
|
Two SEPARATE facts, but several mutations require a COORDINATED transition. Both
|
||||||
|
limbo bugs this session came from conflating them, so this is modelled explicitly
|
||||||
|
and covered by tests rather than patched route by route.
|
||||||
|
|
||||||
|
| Mutation | Pile | Auction |
|
||||||
|
|---|---|---|
|
||||||
|
| Club -> Transfer List | becomes `trade` | none necessarily exists |
|
||||||
|
| list item | stays `trade` | becomes `active` |
|
||||||
|
| clock runs out unsold | stays `trade` | becomes `expired` (projection; no row mutated) |
|
||||||
|
| relist | stays `trade` | `active` again, clock restarted, new prices |
|
||||||
|
| return expired item to Club | becomes `club` | any `active` association MUST end |
|
||||||
|
| sold / reserved | — | MUST NOT be ended by a generic move-to-club |
|
||||||
|
|
||||||
|
Invariant, stated so it can be tested rather than remembered: **an item must never be
|
||||||
|
simultaneously excluded from `/club` and absent from `/tradePile`.** That is the limbo
|
||||||
|
state, and it is reachable in two ways, both now closed:
|
||||||
|
|
||||||
|
1. Excluding by *pile* rather than by *active listing* (fixed in `f2c4927`) — the
|
||||||
|
pile can hold items with no auction, and `/tradePile` renders auctions only.
|
||||||
|
2. Moving to `club` while leaving the auction `active` (fixed in `4e31fb9`) — the
|
||||||
|
pile says club, the exclusion still fires, and the card shows in neither place.
|
||||||
|
|
||||||
|
Tests: `returning_an_expired_listing_to_the_club_ends_its_auction`,
|
||||||
|
`a_pile_move_never_disturbs_a_sale_in_flight`,
|
||||||
|
`club_excludes_listed_items_and_paginates_the_visible_set`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q2 representation — narrowed to ONE candidate by elimination (2026-08-17) — **SUPERSEDED, see the final section**
|
||||||
|
|
||||||
|
Re-entry discriminator result: **CONFIRMED BUG.** After fully leaving and re-entering
|
||||||
|
FUT, the active auction reconstructs correctly under LISTED ITEMS, but the unlisted
|
||||||
|
TRANSFER LIST section is **empty** — item `100000059` does not survive. The
|
||||||
|
immediate post-move visibility was client-local only, so our representation cannot
|
||||||
|
durably reconstruct trade-pile membership.
|
||||||
|
|
||||||
|
The representation is now pinned by elimination over PROVEN facts, not chosen:
|
||||||
|
|
||||||
|
1. **Only one route can own a trade-pile list.** The complete CardsDLL route table
|
||||||
|
(`.rdata 0x18021df80`, 45 routes + 3 empty admin slots) is dumped verbatim at
|
||||||
|
`docs/evidence/market-lifecycle-2026-08-17/cardsdll-route-table.txt` via
|
||||||
|
`fifa17-recon/tools/route_table_dump.py` (static, read-only, VA→offset resolved
|
||||||
|
through the real PE section table). Row 30 `ut/%s/tradePile` is the ONLY
|
||||||
|
trade-pile route. There is no trade-pile *items* route.
|
||||||
|
2. **That route carries only auction records.** `FutGetTradePile` (`0x180170810`)
|
||||||
|
deserializes the shared IS-list body (`0x18013e7f0`), whose `auctionInfo`
|
||||||
|
elements go through `0x18013e410` — the twelve-atom auction record.
|
||||||
|
3. **`pile` cannot be set from the wire.** `pile` (atom 0x226) has NO arm in the item
|
||||||
|
deserializer `FUN_18013fe00` (checked in all four dispatch forms); `item+0x60` is
|
||||||
|
assigned by the OWNING LIST, reading 1 for every `/club` item and 6 for every
|
||||||
|
`/purchased` item. So membership is conferred by *arriving in the list*, never by
|
||||||
|
a field we can add.
|
||||||
|
4. **Of the twelve atoms, only `tradeState` can express lifecycle.**
|
||||||
|
5. **`tradeState`'s vocabulary is closed and has exactly one unused value.** The table
|
||||||
|
walk at `0x180229e40` decodes `active=1 inactive=2 expired=3 closed=4`, anything
|
||||||
|
else `-1`. `active`, `expired` and `closed` are all already spoken for by the
|
||||||
|
observed lifecycle.
|
||||||
|
|
||||||
|
**Therefore an unlisted trade-pile item can only be an `auctionInfo` record with
|
||||||
|
`tradeState: "inactive"`.** That is the sole encoding the client's own parser can
|
||||||
|
accept for "in the pile, no auction" — reached by elimination, not invention.
|
||||||
|
|
||||||
|
Status: **INFERRED-BY-ELIMINATION**, not yet CONFIRMED. One binary question remains,
|
||||||
|
and it is about client *rendering*, not encoding: does the Flash Transfer List place
|
||||||
|
an `inactive` record in the unlisted section? The corpus's warning that "`inactive`
|
||||||
|
decodes but no client path treats it specially" was established for the market
|
||||||
|
PRESENTATION function (`flagA`/`flagB`), where `inactive` is indeed unremarkable —
|
||||||
|
that is consistent with, and does not contradict, using it for list membership.
|
||||||
|
|
||||||
|
Ruled out on evidence, do not retry: adding `itemData.pile` (inert, no deserializer
|
||||||
|
arm), a second route (none exists), and `tradeId: 0` / invented `itemState` values
|
||||||
|
(unnecessary — `tradeState` alone carries the distinction).
|
||||||
|
|
||||||
|
### Acceptance test for whatever lands
|
||||||
|
|
||||||
|
```text
|
||||||
|
move item Club -> Transfer List
|
||||||
|
leave FUT entirely
|
||||||
|
re-enter FUT
|
||||||
|
item appears in the unlisted TRANSFER LIST section
|
||||||
|
item can be returned to Club
|
||||||
|
no auction exists unless explicitly listed
|
||||||
|
```
|
||||||
|
|
||||||
|
Plus the active lifecycle must remain unchanged: active listing under LISTED ITEMS,
|
||||||
|
`expires` counting down, active non-selectable, expiry making it actionable, relist
|
||||||
|
working.
|
||||||
|
|
||||||
|
### Revised domain invariant
|
||||||
|
|
||||||
|
Core pile membership is NOT equivalent to `/tradePile` visibility. Core ownership/pile
|
||||||
|
state and auction lifecycle are separate authoritative facts; the wire may expose them
|
||||||
|
through different FIFA 17 resources. **A transition is complete only when a fresh FIFA
|
||||||
|
session can reconstruct the same user-visible state** — re-entry is the acceptance
|
||||||
|
test, not the immediate post-mutation response.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q2 — LIVE-CONFIRMED: `tradeState: "inactive"` is the unlisted representation — **SUPERSEDED, see the final section**
|
||||||
|
|
||||||
|
Operator-observed in the real FIFA 17 client, 2026-08-17, with the bounded one-item
|
||||||
|
probe (`OPENFUT_FIFA17_UNLISTED_PROBE`). Fixture:
|
||||||
|
`docs/evidence/market-lifecycle-2026-08-17/inactive-row-live-confirmed.json`.
|
||||||
|
|
||||||
|
### The observed `inactive` UI contract
|
||||||
|
|
||||||
|
| Wire | Client presentation |
|
||||||
|
|---|---|
|
||||||
|
| `tradeState: "inactive"` | row placed under the right-hand **TRANSFER LIST** section, NOT under LISTED ITEMS |
|
||||||
|
| `startingBid: 0` | Start Price = `0` |
|
||||||
|
| `buyNowPrice: 0` | Buy Now Price = `-` (absent) |
|
||||||
|
| `currentBid: 0` | Current Bid = `-` (absent) |
|
||||||
|
| `expires: 0` | Time Remaining = `-` (no timer) |
|
||||||
|
| `bidState: "none"` | no bid presentation |
|
||||||
|
| `itemData.itemState: "free"` | renders as a normal owned card |
|
||||||
|
|
||||||
|
An `active` row in the same body continued to render separately under LISTED ITEMS,
|
||||||
|
so the two sections are driven by `tradeState` and the experiment did not disturb the
|
||||||
|
known-good active path.
|
||||||
|
|
||||||
|
Token provenance: dumped from the client's own `{const char*, int}` table at
|
||||||
|
`0x180229e40` (`active=1 inactive=2 expired=3 closed=4`) — see
|
||||||
|
`cardsdll-vocab-tables.txt` and `fifa17-recon/tools/vocab_dump.py`. Nothing here was
|
||||||
|
guessed.
|
||||||
|
|
||||||
|
Regression test: `market::tests::unlisted_candidate_is_a_non_active_pile_row` pins the
|
||||||
|
whole tuple, plus that the row is never emitted for an item outside the `trade` pile
|
||||||
|
and never duplicates a real auction
|
||||||
|
(`unlisted_candidate_never_duplicates_a_real_auction`).
|
||||||
|
|
||||||
|
### Expired -> Club coupled transition, verified
|
||||||
|
|
||||||
|
Operator returned the naturally-expired Ronaldo to the club. Server state after:
|
||||||
|
|
||||||
|
```
|
||||||
|
listing 1000000097 state = cancelled (the coupled transition fired)
|
||||||
|
pile Ronaldo = club
|
||||||
|
/tradePile Ronaldo absent
|
||||||
|
/club Ronaldo present, itemState free
|
||||||
|
hub clubPlayers 1964 -> 1965, auctionCount 1
|
||||||
|
counts {count 1, selling 1, sold 0}
|
||||||
|
```
|
||||||
|
|
||||||
|
All of that is durable store state rather than a client-local view, so it survives a
|
||||||
|
session boundary by construction. Pending the operator's final exit/re-enter
|
||||||
|
confirmation before this is tagged CONFIRMED.
|
||||||
|
|
||||||
|
### Counts observations so far (semantics still UNCHANGED and unresolved)
|
||||||
|
|
||||||
|
| State | `count` | `selling` | `sold` |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 active | 1 | 1 | 0 |
|
||||||
|
| 2 active | 2 | 2 | 0 |
|
||||||
|
| 1 active + 1 inactive (probe) | 1 | 1 | 0 |
|
||||||
|
| unlisted pile items (no probe) | not counted | not counted | 0 |
|
||||||
|
|
||||||
|
So `count` currently tracks AUCTION entries, not total Transfer List membership. Do
|
||||||
|
not change this until the full state set (empty / unlisted / active / expired / sold /
|
||||||
|
mixed) has been observed — it remains an open question whether FIFA 17 expects
|
||||||
|
`count` to include non-active pile members.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q2 status downgrade + the "Expired" differential EXPLAINED (2026-08-17) — **SUPERSEDED, see the final section**
|
||||||
|
|
||||||
|
Phase C was a PARTIAL pass. Corrected conclusions:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tradeState "inactive" = CONFIRMED section/lifecycle discriminator
|
||||||
|
(rows persist across a fresh session and land under
|
||||||
|
TRANSFER LIST, not LISTED ITEMS)
|
||||||
|
|
||||||
|
complete ACTIONABLE unlisted representation = was UNKNOWN; see below
|
||||||
|
```
|
||||||
|
|
||||||
|
### The visual differential, and its cause
|
||||||
|
|
||||||
|
| | one-item probe | generalized rows |
|
||||||
|
|---|---|---|
|
||||||
|
| Start Price | 0 | 0 |
|
||||||
|
| Buy Now / Current Bid | `-` | `-` |
|
||||||
|
| Time Remaining | `-` | **`Expired`** |
|
||||||
|
|
||||||
|
Cause: **route coverage, not a field.** `/tradePile` advertised the unlisted
|
||||||
|
tradeIds, but `GET …/trade/status` (ISVIEWTRADE) answered them from the market store
|
||||||
|
ONLY — and an unlisted pile member has no listing row, so the poll returned an empty
|
||||||
|
`auctionInfo`. Observed live as `route=market-status requested=1 returned=0` repeating
|
||||||
|
for the row the operator had selected (`tradeIds=1000000122`), while the same id was
|
||||||
|
present in `/tradePile`.
|
||||||
|
|
||||||
|
The client polls `/trade/status` for the row it is displaying, and an empty answer
|
||||||
|
degrades it: Time Remaining renders `Expired` and no actions are offered. The earlier
|
||||||
|
probe showed `-` simply because the client had not yet polled that id — the logs at
|
||||||
|
the time show only `tradeIds=1000000097`. So `expires`, `tradeState`,
|
||||||
|
`itemData.itemState` and `pile` were all innocent, and NO field was changed.
|
||||||
|
|
||||||
|
This is the same defect class as the original `trade/status` bug: a route the client
|
||||||
|
polls being answered with an empty set. The corpus predicted it — `tradeId` must
|
||||||
|
resolve across `/transfermarket`, `/tradePile`, `/watchList` AND `/trade/status`. We
|
||||||
|
had stability but not coverage.
|
||||||
|
|
||||||
|
Fix: both routes now share ONE pile enumeration (`Server::resolve_trade_pile`), so an
|
||||||
|
id advertised by `/tradePile` always resolves on `/trade/status`. Verified live: the
|
||||||
|
six inactive ids went from `returned=0` to `returned=6`. `/trade/status` still answers
|
||||||
|
only the ids actually asked about, and a real auction always wins over an inactive row
|
||||||
|
for the same id.
|
||||||
|
|
||||||
|
Regression test: `trade_status_resolves_the_unlisted_ids_tradepile_advertises`.
|
||||||
|
|
||||||
|
### Investigations NOT needed as a result
|
||||||
|
|
||||||
|
`itemData.itemState` and numeric `itemData.pile` were queued for PE recovery on the
|
||||||
|
assumption the encoding was incomplete. The cause was route coverage, so neither was
|
||||||
|
touched and neither is implicated. If the actions still do not appear, those remain
|
||||||
|
the next candidates — and `itemState`'s vocabulary should be dumped statically with
|
||||||
|
`fifa17-recon/tools/vocab_dump.py` (already validated against the tradeState table)
|
||||||
|
rather than guessed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Q2 — RETRACTED, and CLOSED as unrepresentable (2026-08-17, RE of the FUT front-end)
|
||||||
|
|
||||||
|
**Everything in the two Q2 sections above is superseded.** The `tradeState:"inactive"`
|
||||||
|
projection has been removed from `GET …/tradePile` and `…/trade/status`, and the
|
||||||
|
`UnlistedCandidate` machinery deleted. Live after the fix: `total` 7 → 1, only the real
|
||||||
|
auction, `inactive` rows 0, coins unchanged, and all six former rows still present in
|
||||||
|
`/club` (invariant intact).
|
||||||
|
|
||||||
|
### Why the earlier conclusion was wrong
|
||||||
|
|
||||||
|
It was `LIVE-CONFIRMED` that the row **renders**. That was never in doubt, and it is
|
||||||
|
not the same claim as the row being usable. The rows were never actionable, and could
|
||||||
|
not have been:
|
||||||
|
|
||||||
|
| Fact | Confidence |
|
||||||
|
|---|---|
|
||||||
|
| `tradeState` reaches ONE native branch, `cmp …,0x4` at `0x18013e619` ("is it `closed`?"). `inactive`(2) and `expired`(3) take the same edge → bit-identical `flagA`/`flagB`. Exhaustive 22-site census of `[reg+0x88]` reads across the PE. | CONFIRMED |
|
||||||
|
| `tradeState` is handed to the movie verbatim as the Flash property `STATE` (publisher `0x1801bf030`); the action gate is in the APT/ActionScript FUT front-end, not CardsDLL. | CONFIRMED |
|
||||||
|
| The trade-pile movie class partitions rows with `getCardsInAuction`/`isInActiveAuction` (traces `initPile() - IN AUCTION:` / `- NOT IN AUCTION:`), and only auction rows reach `PreCheckCardOptions` → `handleTradeCardAction`. | CONFIRMED (interned AS symbols + trace strings, live) |
|
||||||
|
| A client rule excludes non-auction pile rows from the action path (Category C). | STRONGLY SUPPORTED |
|
||||||
|
| The gate term is `STATE == 2`. | **NOT PROVEN** — `STATE`, `CARD_OFFERSTATE` and the `RESERVEDPRICE`/`MAX_CREDITS` pair all differ together in every row we can observe |
|
||||||
|
| In the shipped game, unlisted pile rows were a CLIENT-SIDE transient built by the movie from a `TO_TRADEPILE` Flash message (`FUN_1801be6a0`), never server-delivered. | STRONGLY SUPPORTED |
|
||||||
|
|
||||||
|
This fires the corpus's own pre-registered falsifier E3
|
||||||
|
(`plan-2026-08-06-transfer-market.md:368-373`): the native predicates all pass
|
||||||
|
(`IS_TRADING_ENABLED=1`, `TRADE_PILE_SIZE=100`, `tradeable=1`) and an `expired` row in
|
||||||
|
the same section IS actionable, so "the Flash layer has its own gate and CardsDLL is no
|
||||||
|
longer the right place to look" is now an observation.
|
||||||
|
|
||||||
|
**The corpus said so before any of this was written**, at
|
||||||
|
`plan-2026-08-06-transfer-market.md:731-733`: "`inactive` decodes but no client path
|
||||||
|
treats it specially; **do not emit it**." The projection was built anyway, and the
|
||||||
|
earlier reasoning above (that the warning "was written about the PRESENTATION
|
||||||
|
function") was motivated rather than evidenced.
|
||||||
|
|
||||||
|
**Consequence for the product:** a server cannot put an unlisted item on the Transfer
|
||||||
|
List in an actionable state. `/tradePile` carries only twelve-atom auction records and
|
||||||
|
`pile` has no deserializer arm, so anything rendered there must be a real auction.
|
||||||
|
Unlisted trade-pile membership is now internal state with no wire expression; those
|
||||||
|
cards live in `/club`, which is where the client can act on them.
|
||||||
|
|
||||||
|
### Open: `itemData.itemState: "listFS"` is not a FIFA 17 token
|
||||||
|
|
||||||
|
Independent of the gate, and CONFIRMED: `listFS` occurs **0 times** in
|
||||||
|
`CardsDLL_Win64_retail.dll` (md5 `4de3493131d7d2ff7f8b360c5ac9b655`) and 0 times in
|
||||||
|
4.26 GiB of live process memory; it decodes to **−1** through `FUN_180166660`, observed
|
||||||
|
live on our listed row. FIFA 17's value for a listed item is `forSale` (5), from the
|
||||||
|
12-row table at `0x180229cc0`. We emit `listFS` on `/tradePile` and `/trade/status`, and
|
||||||
|
so does the Python oracle (`utas_server.py:3921`) — which is exactly why the
|
||||||
|
differential never caught it: **oracle parity is necessary but not sufficient.**
|
||||||
|
|
||||||
|
### Fix B — `forSale` PROMOTED, live-confirmed on the expired path (2026-08-17)
|
||||||
|
|
||||||
|
Shipped alone, then validated by an operator FIFA 17 session against the already-expired
|
||||||
|
row `1000000155` (res 158023, 93 RW).
|
||||||
|
|
||||||
|
| Claim | Status |
|
||||||
|
|---|---|
|
||||||
|
| `listFS` is not a FIFA 17 token and decodes to −1 | **CONFIRMED** (binary + 4.26 GiB process scan + live row) |
|
||||||
|
| `forSale` is FIFA 17-native, value 5 | **CONFIRMED** (12-row table at `0x180229cc0`) |
|
||||||
|
| An expired row carrying `forSale` is still ACTIONABLE, and Return to Club works | **LIVE-CONFIRMED** — operator returned the card; server verified below |
|
||||||
|
| The result is durable (a fresh session reconstructs it) | **LIVE-CONFIRMED** (Gap 2, 2026-08-17) — the operator exited FUT and re-entered; the host log shows `auth-delete` then two fresh `sid_opened=true` sessions, each re-fetching `/club` and `/tradePile`, and the card was still in My Club. 18/18 server checks passed identically before and after. See the Gap 2 closure section. |
|
||||||
|
| An ACTIVE seller row carrying `forSale` renders correctly | **LIVE-CONFIRMED** (Gap 1, 2026-08-17) — see the closure section below; the client decoded `CARD_OFFERSTATE = 5` out of its own auction record |
|
||||||
|
| `CARD_OFFERSTATE` is the Flash action-gate term | **STILL UNPROVEN** — this change does not test it, and the result does not bear on it |
|
||||||
|
|
||||||
|
What this rules out: `CARD_OFFERSTATE` is not a gate term that *requires* −1. An expired
|
||||||
|
row decoding `CARD_OFFERSTATE = 5` remained actionable, so the earlier worry — that every
|
||||||
|
actionable row we had ever seen carried `itemState = −1` — was a coincidence of our own
|
||||||
|
bad token, not a client rule. It narrows nothing else: `STATE` and the
|
||||||
|
`RESERVEDPRICE`/`MAX_CREDITS` pair are untouched and still confounded with each other.
|
||||||
|
|
||||||
|
Wire A/B (`docs/evidence/market-lifecycle-2026-08-17/itemstate-forsale-ab.json`): on the
|
||||||
|
same expired row, **exactly one field changed** — `itemState` `listFS` → `forSale`.
|
||||||
|
`tradeId`, `tradeState`, `expires`, `startingBid`, `buyNowPrice`, `currentBid`,
|
||||||
|
`bidState`, `sellerName`, `sellerEstablished`, `watched`, `coinsProcessed`, the atom
|
||||||
|
count (12), and the whole `itemData` card are byte-identical; coins unchanged; Fix A's
|
||||||
|
zero `inactive` rows intact.
|
||||||
|
|
||||||
|
Risk was lower than it first appeared, for a reason worth recording: the market SEARCH
|
||||||
|
route (`/transfermarket`) **already** emitted `forSale`, so the client has been decoding
|
||||||
|
`forSale` (5) on a live path all along — only the seller's own pile carried the invalid
|
||||||
|
token.
|
||||||
|
|
||||||
|
Differential consequence: `market query tradePile` moves from `PARITY` to
|
||||||
|
`DIFFERENT-BY-DESIGN`, pinning `oracle == "listFS"` and `rust == "forSale"` so the
|
||||||
|
divergence stays visible. That assertion previously demanded parity and passed while
|
||||||
|
**both** sides were wrong — the mechanism by which this defect survived every
|
||||||
|
differential run. **Where the FIFA 17 binary contradicts the Python oracle, the binary
|
||||||
|
wins.**
|
||||||
|
|
||||||
|
Revert is one line if the live A/B fails: both `auction_record_as(l, "forSale")` calls in
|
||||||
|
`market.rs` back to `"listFS"`, rebuild, restart `prod-host`. On failure the rule is to
|
||||||
|
revert and go to the AVM1 gate work — NOT to compensate with another field change.
|
||||||
|
|
||||||
|
**Claim boundary:** this is protocol correctness of one token. It is not evidence about
|
||||||
|
the Flash decision tree; `STATE`, `CARD_OFFERSTATE` and `RESERVEDPRICE`/`MAX_CREDITS`
|
||||||
|
remain confounded.
|
||||||
|
|
||||||
|
### Gap 1 CLOSED — an ACTIVE seller row under `forSale` (2026-08-17)
|
||||||
|
|
||||||
|
The one thing the Fix B promotion could not test, because no active listing existed
|
||||||
|
during that session. Closed with a deliberately disposable card: a **75 ST**, resourceId
|
||||||
|
`212188`, wire id `100000178` — one of **three identical owned copies**, not in the
|
||||||
|
11-man squad, `untradeable: false`, discardValue 600. Listed **through the real FIFA 17
|
||||||
|
client** at Start 150 / Buy Now 200 for the shortest offered duration (1h), so expiry
|
||||||
|
would arrive naturally. No timestamp was touched.
|
||||||
|
|
||||||
|
Listing creation was entirely Rust-owned:
|
||||||
|
`PUT …/item` (club → pile) → `route=market-list POST item_id=100000178 listed=true
|
||||||
|
trade_id=1000000178` → `POST …/auctionhouse 200`, then the client polling
|
||||||
|
`GET …/trade/status?tradeIds=1000000178` (`requested=1 returned=1`) for the row it draws.
|
||||||
|
|
||||||
|
Wire, active row `1000000178`
|
||||||
|
(`docs/evidence/market-lifecycle-2026-08-17/gap1-active-forsale.json`):
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| `itemState` | **`forSale`** |
|
||||||
|
| `tradeState` | `active` |
|
||||||
|
| `startingBid` / `buyNowPrice` / `currentBid` | 150 / 200 / 0 |
|
||||||
|
| `bidState` | `none` |
|
||||||
|
| `expires` | **3562 → 3556** over a 6-second sample — a live clock, not a frozen scalar |
|
||||||
|
| atom count | **12**, exactly what `0x18013e410` reads |
|
||||||
|
| `itemData` | full card, 6-entry `attributeList` |
|
||||||
|
| `/tradePile/counts` | `count 1, selling 1, offered 0, sold 0` |
|
||||||
|
| coins | 29,843,976 unchanged |
|
||||||
|
| Fix A | 0 `inactive` rows |
|
||||||
|
|
||||||
|
`/trade/status` stayed coherent with `/tradePile` throughout, agreeing on tradeId,
|
||||||
|
`active`, `forSale` and a decreasing `expires`.
|
||||||
|
|
||||||
|
**The decisive observation** is not the wire — it is what the client made of it. A
|
||||||
|
read-only `/proc/<pid>/mem` decode of the live trade-pile auction record (slide proven by
|
||||||
|
FNV control MATCH first; `O_RDONLY` + `pread` only) returned:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tradeId=1000000178 state=1(active) bid=0(none) buyNow=200 start=150 cur=0 expires=3457
|
||||||
|
item CARD_ID=212188 cardType=1 tradeable=1 itemState=5(forSale) pile=0 rating=75
|
||||||
|
```
|
||||||
|
|
||||||
|
`itemState = 5(forSale)`. The same field read `-1(<unrecognised>)` under `listFS`. This is
|
||||||
|
the direct A/B on the only changed field, taken from the client's own memory rather than
|
||||||
|
from our response — the strongest form of this evidence available without AVM1 work.
|
||||||
|
`pile = 0` again, confirming pile is still never set from the wire.
|
||||||
|
|
||||||
|
Operator UI acceptance: card under **LISTED ITEMS** (not the TRANSFER LIST section), Start
|
||||||
|
150 / Buy Now 200 correct, **Time Remaining visibly counting down**, normal card art, no
|
||||||
|
malformed row, no duplicate, no premature Expired, no crash, and correctly
|
||||||
|
**non-actionable while active**. The client's container populating with exactly this row
|
||||||
|
(`rows=1`) independently corroborates that the Transfer List screen bound and drew it.
|
||||||
|
|
||||||
|
**No code change was required** — the promoted implementation was already correct on the
|
||||||
|
active path, which is what the test was designed to find out.
|
||||||
|
|
||||||
|
Two process notes worth keeping. First, an early capture reported "countdown NO"; that was
|
||||||
|
a bug in the capture script (it re-read a hardcoded copy id rather than the row it had
|
||||||
|
resolved), not a server fault — the same run's `/trade/status` already showed a lower
|
||||||
|
`expires` than `/tradePile`. Verify the instrument before believing a negative. Second,
|
||||||
|
the client's auction-record containers read **0 rows** while the operator was on any other
|
||||||
|
screen; they populate only while the Transfer List is bound. An empty container is not
|
||||||
|
evidence of an empty pile.
|
||||||
|
|
||||||
|
**Claim boundary unchanged:** this proves the client *decodes* the token. It says nothing
|
||||||
|
about the Flash action-gate term.
|
||||||
|
|
||||||
|
|
||||||
|
### Proving the gate (not yet done)
|
||||||
|
|
||||||
|
Disassemble the AVM1 action buffer of `tradepile.isInActiveAuction` (and
|
||||||
|
`PreCheckCardOptions`): either walk from the interned symbol at `0xb84d7498` to its APT
|
||||||
|
function object in the live process, or extract
|
||||||
|
`external/ion_fut/screens/trading/tradepile` from `ui.sb` + the `cas` archives offline.
|
||||||
|
Nothing else in reach separates `STATE` from `CARD_OFFERSTATE` from the price pair,
|
||||||
|
because all three differ together in every observable row.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gap 2 CLOSED — Return to Club survives a full exit/re-entry (2026-08-17)
|
||||||
|
|
||||||
|
The claim the Fix B promotion could only make server-side. Closed on the same
|
||||||
|
disposable card as Gap 1 (75 ST, res `212188`, wire id `100000178`, tradeId
|
||||||
|
`1000000178`) after it expired **naturally** — the 1h auction was allowed to run
|
||||||
|
out. No timestamp was mutated at any point in either gap.
|
||||||
|
|
||||||
|
### The whole lifecycle, observed rather than forced
|
||||||
|
|
||||||
|
A read-only sampler polled `/tradePile` once a minute for the auction's full hour
|
||||||
|
(55 samples). `expires` is derived from `created_at + duration`, so watching is the
|
||||||
|
only honest way to see expiry happen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
t=1787012091 active expires 3175 itemState forSale
|
||||||
|
... counting down, one sample a minute ...
|
||||||
|
t=1787015271 expired expires 0 itemState forSale <- natural expiry
|
||||||
|
t=1787015301 (absent) total 0 <- Return to Club
|
||||||
|
```
|
||||||
|
|
||||||
|
`itemState` stayed `forSale` across the `active -> expired` transition, which is the
|
||||||
|
Fix B behaviour under the one state change we had never watched live.
|
||||||
|
|
||||||
|
### Return to Club, then two session boundaries
|
||||||
|
|
||||||
|
The host log records the coupled transition and the re-entries:
|
||||||
|
|
||||||
|
```text
|
||||||
|
route=move-items wire=100000178 pile=club auction_cancelled=1
|
||||||
|
route=auth-delete
|
||||||
|
route=auth ... ip=10.10.0.105 persona=33068179 sid_opened=true
|
||||||
|
route=hub clubPlayers=1966 auctionCount=0
|
||||||
|
route=club total=1986 emitted=1966 dropped_no_asset=0
|
||||||
|
... the full auth -> userMassInfo -> hub -> club -> tradePile -> trade/status
|
||||||
|
cycle then repeats for a SECOND fresh session ...
|
||||||
|
```
|
||||||
|
|
||||||
|
`auction_cancelled=1` is `cancel_active_for_core_item` firing: the pile move to
|
||||||
|
`club` ends the auction that put the card there, which is the coupled transition
|
||||||
|
that keeps pile membership and auction lifecycle from disagreeing.
|
||||||
|
|
||||||
|
Two independent fresh sessions each rebuilt the state from durable storage, and the
|
||||||
|
operator confirmed the card was still in My Club. One session boundary was the
|
||||||
|
requirement; two is stronger.
|
||||||
|
|
||||||
|
### Server state — 18/18, byte-identical before and after re-entry
|
||||||
|
|
||||||
|
| Surface | Result |
|
||||||
|
|---|---|
|
||||||
|
| `/tradePile` | `total 0`, 0 rows, no reference to the item or the tradeId |
|
||||||
|
| `/tradePile/counts` | `count 0, selling 0, sold 0, offered 0` |
|
||||||
|
| `/trade/status` | tradeId `1000000178` → **`closed`**, `expires 0` — still resolves, correctly terminal |
|
||||||
|
| `/club` | **1966** items; `id=100000178 res=212188 rating=75 ST itemState=free untradeable=False` |
|
||||||
|
| Duplicate owned ids | **0** across all 1966 |
|
||||||
|
| Market store | listing `1000000178` = `cancelled`; **0 active, 0 reserved** anywhere |
|
||||||
|
| Coins | **29,843,976** unchanged for the entire test |
|
||||||
|
| Fix A | 0 `inactive` rows |
|
||||||
|
|
||||||
|
The `1965 → 1966` club count is the pile-aware exclusion working in both directions:
|
||||||
|
the card left `/club` while actively listed and returned on cancel. A tradeId that
|
||||||
|
can still be asked about still answers — terminally — rather than vanishing.
|
||||||
|
|
||||||
|
**No code change was required for either gap.** Both tests were designed to find out
|
||||||
|
whether the promoted implementation was already correct on paths it had not been
|
||||||
|
exercised on. It was.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FROZEN BASELINE — the known-good transfer-list lifecycle (2026-08-17)
|
||||||
|
|
||||||
|
With both gaps closed, this lifecycle is the reference. A future change that alters
|
||||||
|
any line here is a regression until proven otherwise.
|
||||||
|
|
||||||
|
```text
|
||||||
|
CLUB
|
||||||
|
itemState free, present in /club, absent from /tradePile
|
||||||
|
| PUT /item {pile:"trade"} + POST /auctionhouse (ISStart)
|
||||||
|
v
|
||||||
|
ACTIVE AUCTION
|
||||||
|
/tradePile: tradeState active, itemState forSale, 12 atoms,
|
||||||
|
expires counting down from duration, currentBid 0, bidState none
|
||||||
|
/tradePile/counts: count 1, selling 1
|
||||||
|
/club: EXCLUDED while listed
|
||||||
|
/trade/status: agrees with the pile on every field
|
||||||
|
client: renders under LISTED ITEMS, CARD_OFFERSTATE = 5, non-actionable
|
||||||
|
| wall clock reaches created_at + duration
|
||||||
|
v
|
||||||
|
EXPIRED
|
||||||
|
/tradePile: tradeState expired, expires 0, itemState still forSale
|
||||||
|
client: row becomes ACTIONABLE, offers Return to Club
|
||||||
|
| PUT /item {pile:"club"} -> cancel_active_for_core_item
|
||||||
|
v
|
||||||
|
CLUB (terminal)
|
||||||
|
/tradePile: total 0 /tradePile/counts: all zero
|
||||||
|
/club: present again, itemState free market store: cancelled
|
||||||
|
/trade/status: old tradeId -> closed, expires 0
|
||||||
|
coins: unchanged throughout
|
||||||
|
survives: FUT exit + re-entry, repeatedly
|
||||||
|
```
|
||||||
|
|
||||||
|
Invariants this baseline pins:
|
||||||
|
|
||||||
|
* an item is never in limbo — it is in `/club` or in an active auction, never neither;
|
||||||
|
* `expires` is SECONDS REMAINING and must evolve with wall-clock;
|
||||||
|
* pile membership and auction lifecycle are separate facts that must be changed
|
||||||
|
together;
|
||||||
|
* a tradeId resolves on every route that can be asked about it, even when terminal;
|
||||||
|
* only real active auctions appear in `/tradePile` (Fix A);
|
||||||
|
* a listed item carries `itemState forSale` (Fix B), and the client decodes 5;
|
||||||
|
* an active own auction is intentionally non-actionable;
|
||||||
|
* coins move only when coins should move — this whole lifecycle moves none.
|
||||||
|
|
||||||
|
What is still NOT established: the SOLD path (no counterparty exists yet; settlement
|
||||||
|
is implemented and unit-tested in Core but never client-validated — see
|
||||||
|
`MARKET_SOLD_SETTLEMENT.md`), `/tradePile/counts` exact FIFA semantics, and the AVM1
|
||||||
|
action gate.
|
||||||
|
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
# Transfer-market sold settlement + transaction fee
|
||||||
|
|
||||||
|
How a completed market sale moves coins and ownership. Written while the
|
||||||
|
`forSale` expiry lifecycle test was running, so **nothing here has been deployed
|
||||||
|
or validated against a FIFA client** — see the confidence table.
|
||||||
|
|
||||||
|
Every claim carries one of:
|
||||||
|
|
||||||
|
| Tag | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| **CODE** | Read directly out of the current implementation |
|
||||||
|
| **TESTED** | Pinned by a unit or integration test that fails if it regresses |
|
||||||
|
| **FIFA17-HISTORICAL** | Supported by contemporaneous FIFA 17 knowledge, not by our client binary |
|
||||||
|
| **NEEDS-LIVE** | Requires a real FIFA 17 client to confirm; must not be treated as settled |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What the Buy Now path did before this work
|
||||||
|
|
||||||
|
Traced end to end, **CODE**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST/PUT /ut/game/fifa17/trade/<id>
|
||||||
|
-> openfut-utas-host/src/lib.rs route classification (EconomyRoute::MarketBuy)
|
||||||
|
-> market.rs::handle_market_buy
|
||||||
|
store.get_listing(id) market_store.rs
|
||||||
|
bid < buy_now -> echo raised bid, NO coin movement, return
|
||||||
|
store.reserve_listing(id) CAS active -> reserved
|
||||||
|
econ.balance() pre-check, 461 if short
|
||||||
|
econ.purchase_item(price, "market-buy:<id>", listing.card_id)
|
||||||
|
-> POST {core}/economy/purchase-item
|
||||||
|
-> openfut-core services/economy.rs::purchase_item
|
||||||
|
BEGIN IMMEDIATE; debit(club, cost); add_item(club, item, card); COMMIT
|
||||||
|
store.complete_sale(id) CAS reserved -> sold
|
||||||
|
-> auctionInfo record with tradeState "closed", bidState "highest",
|
||||||
|
currentBid = price, itemData.itemState "free"
|
||||||
|
```
|
||||||
|
|
||||||
|
The module header at `openfut-utas-host/src/market.rs:12-16` states it plainly:
|
||||||
|
a buy-now *"debits the buyer and MINTS the won card into their club … there is no
|
||||||
|
real counterparty and no seller credit"*.
|
||||||
|
|
||||||
|
### The ten questions, answered from code
|
||||||
|
|
||||||
|
| # | Question | Answer (**CODE**) |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | Where are buyer coins debited? | `services/economy.rs::debit` inside `purchase_item`, one `BEGIN IMMEDIATE` transaction. Host pre-checks with `econ.balance()` only to distinguish 461 from 503. |
|
||||||
|
| 2 | Does Core know the seller? | **No.** Every `/economy/*` route resolves ONE club from the `X-OpenFUT-Game` active profile (`routes/economy.rs:23-28`); no request shape carries a counterparty. |
|
||||||
|
| 3 | Does the auction persist seller-owned item identity? | **Yes** — `listings.core_item_id` and `wire_item_id` (`market_store.rs:199-203`). The settlement inputs already existed; the buy path just ignored them. |
|
||||||
|
| 4 | Transfer or mint? | **Mint.** `purchase_item` INSERTs a new `owned_cards` row keyed `market-buy:<tradeId>`. The seller's row is untouched, so a sold card existed **twice**. |
|
||||||
|
| 5 | Is the seller credited? | **No.** Nowhere. |
|
||||||
|
| 6 | Any tax calculation? | **No.** Zero occurrences of `*5/100`, `0.95`, `fee`, `tax` in Core, host, adapter *or* the Python oracle. It existed only as prose in `FIFA17_HUB_BEHAVIOR_SPEC.md:884` and `FIFA17_TRANSFER_MARKET_WIRE.md:127-131`, both labelled unimplemented. |
|
||||||
|
| 7 | Auction state sequence? | `active -> reserved -> sold`, host-side (`market_store.rs`). On any Core failure `reserved -> active`. |
|
||||||
|
| 8 | Is there a `reserved` state? | **Yes**, and it is the concurrency guard, not decoration — `reserve_listing` is a `BEGIN IMMEDIATE` compare-and-swap, so of two concurrent buyers exactly one proceeds to a debit. |
|
||||||
|
| 9 | When does the item leave the seller's `/tradePile`? | When the listing stops being `active`: the pile projection emits only real active auctions (the Fix A invariant). `sold`/`cancelled` rows vanish from the pile the moment the CAS lands. |
|
||||||
|
| 10 | What makes a retry safe? | The same CAS. A second Buy Now finds the listing non-`active`, so `reserve_listing` returns `false` and the handler returns an empty `auctionInfo` — the client reads "auction gone", and no economy call is made. |
|
||||||
|
|
||||||
|
### Deficiencies
|
||||||
|
|
||||||
|
1. **Ownership duplication.** A sold card ends up owned by both parties. Structural, not a race.
|
||||||
|
2. **No seller credit** — selling was economically pointless.
|
||||||
|
3. **No fee**, so no sink; every synthetic buy was pure coin destruction and every sale would have been pure creation.
|
||||||
|
4. **Core could not express a counterparty at all** — the single-club resolver had no seller/buyer distinction to make.
|
||||||
|
5. `UPDATE owned_cards SET club_id` did not exist anywhere in Core: there was **no ownership-transfer primitive** to call.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The settlement transaction
|
||||||
|
|
||||||
|
`openfut-core/src/services/economy.rs`, alongside the existing tx-scoped
|
||||||
|
primitives so it reuses the module's proven atomicity (`pool.acquire()` +
|
||||||
|
`BEGIN IMMEDIATE` + `finish()`), **CODE**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum SaleBuyer<'a> { Club(&'a str), Outside }
|
||||||
|
pub struct SaleTerms { pub gross: i64, pub fee: i64 }
|
||||||
|
|
||||||
|
pub async fn settle_sale(
|
||||||
|
pool: &Pool, item_id: &str, seller_club_id: &str,
|
||||||
|
buyer: SaleBuyer<'_>, terms: SaleTerms,
|
||||||
|
) -> AppResult<SaleReceipt>
|
||||||
|
```
|
||||||
|
|
||||||
|
One transaction: debit buyer `gross` → evict the item from every squad →
|
||||||
|
transfer the existing row → credit seller `gross - fee`. The fee is simply never
|
||||||
|
credited anywhere, which is what destroys it.
|
||||||
|
|
||||||
|
Exposed as `POST /economy/settle-sale`. It is the **only** economy route that
|
||||||
|
names clubs explicitly, because a sale has two sides and the active-profile
|
||||||
|
resolver can only describe one. Both are optional: an omitted `seller_club_id`
|
||||||
|
means the active club (the production shape — the player listed the item), and an
|
||||||
|
omitted `buyer_club_id` means `Outside`, *not* the active club, so the route can
|
||||||
|
never accidentally settle a club against itself.
|
||||||
|
|
||||||
|
### Ownership transfer, not duplication
|
||||||
|
|
||||||
|
```sql
|
||||||
|
UPDATE owned_cards SET club_id = ? WHERE id = ? AND club_id = ?
|
||||||
|
```
|
||||||
|
|
||||||
|
No INSERT and no DELETE on the two-party path, so the instance id, its
|
||||||
|
`chemistry_style`/`position_override`/`training_bonus` and its `acquired_at` all
|
||||||
|
survive and the inventory row count cannot change. Duplication is ruled out
|
||||||
|
structurally rather than by an assertion. `acquired_at` is deliberately preserved:
|
||||||
|
this is the same instance under new ownership, not a new acquisition.
|
||||||
|
|
||||||
|
**TESTED** — `settle_sale_transfers_ownership_and_splits_coins` asserts both that
|
||||||
|
the item's owner changed and that `SELECT COUNT(*) FROM owned_cards` is still 1.
|
||||||
|
|
||||||
|
### The seller is pinned, not inferred — and this was a real bug
|
||||||
|
|
||||||
|
The first implementation derived the seller from *current* ownership, which reads
|
||||||
|
naturally ("the seller is whoever owns it") and is wrong. The concurrency test
|
||||||
|
caught it immediately: **both** racing buyers succeeded, because after the first
|
||||||
|
sale the item was owned by buyer B, so the second call read B as the seller and
|
||||||
|
cheerfully chain-sold it B → C. Core has no listing concept, so it had no way to
|
||||||
|
notice that this was a replay of one listing rather than a new sale.
|
||||||
|
|
||||||
|
The fix is that `seller_club_id` is the club the caller **believes** owns the item,
|
||||||
|
and every ownership statement is predicated on it. That turns the `UPDATE` into an
|
||||||
|
ownership compare-and-swap which is authoritative about "this sale already
|
||||||
|
happened", independently of any caller-side listing state. Safety is not weakened:
|
||||||
|
a caller still cannot credit a club that never owned the item, because the credit
|
||||||
|
only follows a matched CAS.
|
||||||
|
|
||||||
|
**TESTED** — `two_buyers_racing_one_item_settle_once` (exactly one winner, loser's
|
||||||
|
balance untouched, one instance, seller paid once) and
|
||||||
|
`settling_the_same_sale_twice_pays_once`.
|
||||||
|
|
||||||
|
### Squad eviction is mandatory
|
||||||
|
|
||||||
|
`squad_players.owned_card_id` is a foreign key onto `owned_cards(id)` and the pool
|
||||||
|
enables `foreign_keys`. Two consequences, both **CODE**:
|
||||||
|
|
||||||
|
* an `Outside` sale of a squadded card would fail outright on the FK;
|
||||||
|
* a two-party transfer preserves the row id, so a surviving `squad_players` row
|
||||||
|
would leave the **previous** owner fielding a card they no longer own.
|
||||||
|
|
||||||
|
So both paths evict first, and the receipt reports `squad_slots_freed`.
|
||||||
|
**TESTED** — `sale_evicts_the_item_from_the_sellers_squad`.
|
||||||
|
|
||||||
|
> Noticed while reading, NOT fixed here (out of scope, no behaviour changed):
|
||||||
|
> `routes/cards.rs:187-192` (quick sell) and `services/market.rs` both DELETE an
|
||||||
|
> owned card without clearing `squad_players` and without a transaction, so
|
||||||
|
> quick-selling a squadded card should fail on the same FK. Worth a follow-up.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. The fee arithmetic
|
||||||
|
|
||||||
|
Lives in `openfut-adapter-fifa17/src/fut/economy_policy.rs`, beside `pack_price`
|
||||||
|
and `match_reward_total`, because 5% is a **FIFA policy constant** and Core must
|
||||||
|
stay game-neutral. Core validates `0 <= fee <= gross` and never computes a rate.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub const TRANSFER_MARKET_FEE_PERCENT: i64 = 5;
|
||||||
|
pub fn transfer_market_fee(gross: i64) -> i64 { /* floor, i128 intermediate */ }
|
||||||
|
pub fn seller_proceeds(gross: i64) -> i64 { gross.max(0) - transfer_market_fee(gross) }
|
||||||
|
```
|
||||||
|
|
||||||
|
Integer only. Floating point is never used for coin settlement: `0.05` is not
|
||||||
|
representable in binary, and at large prices a `f64` round trip can create or
|
||||||
|
destroy a coin. The multiply widens to `i128`, so overflow is unreachable for any
|
||||||
|
`i64` price and no price ceiling has to be assumed.
|
||||||
|
|
||||||
|
**Rounding is a CHOICE, and it is NEEDS-LIVE.** The fee is floored, so the seller
|
||||||
|
keeps the fractional coin. That was chosen because it makes
|
||||||
|
|
||||||
|
```text
|
||||||
|
fee + proceeds == gross
|
||||||
|
```
|
||||||
|
|
||||||
|
hold exactly at every input, which is what the accounting invariant rests on. The
|
||||||
|
discriminating case against the alternative (flooring the seller's 95% instead) is
|
||||||
|
a gross of **150**: this rule pays **143**, the alternative pays **142**. Nothing
|
||||||
|
in the corpus or the client binary settles which the real server did — the client
|
||||||
|
is only ever told the gross, and no `tax`/`netPrice`/`sellerProceeds` wire field
|
||||||
|
exists. **A future live test should list something at 150 coins and read the
|
||||||
|
credited amount.**
|
||||||
|
|
||||||
|
**TESTED** at `0, 1, 19, 20, 21, 39, 40, 100, 150, 200, 1_000, 15_000,
|
||||||
|
15_000_000, i64::MAX`, plus `fee + proceeds == gross` swept over 0..2000 and the
|
||||||
|
extremes, plus negative-gross rejection.
|
||||||
|
|
||||||
|
| gross | fee | proceeds | why it is in the table |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 19 | 0 | 19 | last fee-free price |
|
||||||
|
| 20 | 1 | 19 | first price that pays |
|
||||||
|
| 150 | 7 | 143 | **the NEEDS-LIVE discriminator** (alternative: 8 / 142) |
|
||||||
|
| 15,000 | 750 | 14,250 | canonical fixture |
|
||||||
|
| i64::MAX | MAX/20 | remainder | proves no overflow |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Accounting invariant
|
||||||
|
|
||||||
|
For `SaleBuyer::Club`, **TESTED** by `sale_conserves_coins_minus_the_fee`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
buyer_debit == seller_credit + fee
|
||||||
|
total modelled coins shrink by EXACTLY the fee
|
||||||
|
21,000 -> 20,250 (difference 750)
|
||||||
|
```
|
||||||
|
|
||||||
|
For `SaleBuyer::Outside` the identity deliberately differs and the doc comment
|
||||||
|
says so: the counterparty owns no `clubs` row, so nothing is debited and the
|
||||||
|
seller's proceeds **enter** the economy from outside. **TESTED** by
|
||||||
|
`settle_sale_to_outside_retires_the_item_and_pays_net`, which asserts total coins
|
||||||
|
*rise* by the proceeds. Conflating the two would look like a conservation bug.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Rejected sales
|
||||||
|
|
||||||
|
All **TESTED** by `invalid_sales_change_nothing`, which re-seeds a fresh fixture
|
||||||
|
per case and asserts both balances and the item's owner afterwards:
|
||||||
|
|
||||||
|
| Case | Outcome |
|
||||||
|
|---|---|
|
||||||
|
| buyer cannot afford `gross` | rejected, nothing moves |
|
||||||
|
| buyer == seller | rejected at the Core domain boundary — not a market path, and it would otherwise debit and credit one club while destroying the fee |
|
||||||
|
| item does not exist | rejected |
|
||||||
|
| item not owned by the named seller (**includes every replay**) | rejected |
|
||||||
|
| buyer club does not exist | rejected |
|
||||||
|
| `fee > gross` | rejected |
|
||||||
|
| negative fee / negative gross | rejected |
|
||||||
|
| ownership changed mid-flight | rejected by the CAS |
|
||||||
|
|
||||||
|
A zero-price sale is **legal** (a free transfer, no fee) — **TESTED** by
|
||||||
|
`zero_price_sale_is_a_free_transfer`.
|
||||||
|
|
||||||
|
Auction-level states (`expired`, `cancelled`, already-`sold`) are **not** Core's
|
||||||
|
business: Core has no listing concept, and the host's `active -> reserved` CAS
|
||||||
|
already refuses those. Duplicating that check in Core would create a second
|
||||||
|
authority on auction lifecycle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Atomicity
|
||||||
|
|
||||||
|
`services::economy` is the **only** module in Core that is genuinely atomic; it
|
||||||
|
uses `pool.acquire()` + raw `BEGIN IMMEDIATE` + `finish()` deliberately, because a
|
||||||
|
DEFERRED `pool.begin()` upgrades to a write at first write, where SQLite returns
|
||||||
|
`SQLITE_BUSY` immediately instead of honouring `busy_timeout`
|
||||||
|
(`services/economy.rs:218-223`). Settlement is built inside that module for
|
||||||
|
exactly this reason, so it inherits the guarantee rather than re-deriving it.
|
||||||
|
|
||||||
|
Handlers receive a pooled clone, never `&mut SqliteConnection`, so the transaction
|
||||||
|
boundary is inside the service function. **CODE.** No externally observable state
|
||||||
|
exists where the buyer is debited without the item moving, or the seller credited
|
||||||
|
without the buyer debited — every step shares one connection inside one
|
||||||
|
transaction, and `finish()` rolls back the whole thing on any error.
|
||||||
|
|
||||||
|
### Rejection precedence, and a test that passed for the wrong reason
|
||||||
|
|
||||||
|
Inside one transaction the order of the steps cannot change the final state — any
|
||||||
|
error rolls everything back. It does change **which reason** a rejection carries,
|
||||||
|
and that turned out to matter.
|
||||||
|
|
||||||
|
The first implementation debited the buyer before touching ownership, on the
|
||||||
|
reasonable-sounding grounds that an unaffordable sale should fail before ownership
|
||||||
|
moves. The isolated staging harness then showed that a replayed settlement was
|
||||||
|
refused with `insufficient balance: have 5000, need 15000` — because the buyer had
|
||||||
|
already spent the coins on the sale that succeeded. The affordability guard fired
|
||||||
|
first, so **the ownership CAS was never consulted**, and the unit test named
|
||||||
|
`settling_the_same_sale_twice_pays_once` was passing without ever exercising the
|
||||||
|
replay guard it claimed to test.
|
||||||
|
|
||||||
|
Two fixes, both **TESTED**:
|
||||||
|
|
||||||
|
* ownership is now judged before affordability, so a replay is reported as
|
||||||
|
`item not owned by club` (404) — the true cause;
|
||||||
|
* the replay test now asserts the error is specifically `AppError::NotFound`, and
|
||||||
|
adds a deliberately CHEAP replay (gross 100 out of the buyer's remaining 5,000)
|
||||||
|
that only ownership can possibly refuse.
|
||||||
|
|
||||||
|
The lesson generalises: a guard that is merely *shadowed* by an earlier guard is
|
||||||
|
untested, and a green assertion on "the second call failed" says nothing about
|
||||||
|
which mechanism failed it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6b. Isolated staging harness
|
||||||
|
|
||||||
|
`scripts/settlement-staging.py` — stdlib-only, exercises the settlement against a
|
||||||
|
**real Core over real HTTP** with zero production contact. It picks an ephemeral
|
||||||
|
loopback port (production 8099/8199/18080 sit in a hard deny-list checked in three
|
||||||
|
places), runs Core once to apply its own migrations, seeds the canonical two-party
|
||||||
|
fixture by direct SQL, relaunches Core, then prints BEFORE / PURCHASE / AFTER with
|
||||||
|
PASS-FAIL lines and cleans up in a `finally`.
|
||||||
|
|
||||||
|
Result: **31/31 checks pass**, exit 0.
|
||||||
|
|
||||||
|
```text
|
||||||
|
BEFORE seller 1,000 buyer 20,000 owner club-seller rows 1 total 21,000
|
||||||
|
POST /economy/settle-sale {item_id, gross 15000, fee 750, seller, buyer} -> 200
|
||||||
|
AFTER seller 15,250 buyer 5,000 owner club-buyer rows 1 total 20,250
|
||||||
|
fee destroyed 750 duplicates 1 coins destroyed 750
|
||||||
|
RETRY (identical) -> 404 item not owned by club: item-x nothing moved
|
||||||
|
REPLAY GUARD (gross 100) -> 404 item not owned by club: item-x nothing moved
|
||||||
|
```
|
||||||
|
|
||||||
|
Two things it found that the unit tests had not:
|
||||||
|
|
||||||
|
1. the rejection-precedence bug above;
|
||||||
|
2. Core's **content preflight** aborts startup when an owned card references a
|
||||||
|
`CardDefinitionId` that no loaded pack defines (`content preflight failed: 1
|
||||||
|
owned card(s) reference CardDefinitionId(s) not loaded (e.g. ["def-x"])`), so
|
||||||
|
the harness writes a one-entry content pack for its synthetic card rather than
|
||||||
|
quietly substituting a stock card id.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. What is NOT done, and why
|
||||||
|
|
||||||
|
* **No production wire change, no deployment.** The host gained the *capability*
|
||||||
|
(`CoreEconomy::settle_sale`) but `handle_market_buy` is untouched: the synthetic
|
||||||
|
buy path (`core_item_id == None`) has no counterparty, so minting there is
|
||||||
|
correct and unchanged.
|
||||||
|
* **Nothing decides that a player's listing has sold.** That trigger is the real
|
||||||
|
remaining feature, and it needs the seller-facing sold wire representation,
|
||||||
|
which is deliberately deferred — `tradeState`/`itemState` for a sold row are
|
||||||
|
**NEEDS-LIVE** and must not be guessed. `docs/FIFA17_TRANSFER_MARKET_WIRE.md`
|
||||||
|
records that no `sold` token exists in FIFA 17's `tradeState` table
|
||||||
|
(`active=1 inactive=2 expired=3 closed=4`).
|
||||||
|
* **`/tradePile/counts` semantics unchanged.** Its `sold` field stays as-is; the
|
||||||
|
Core facts a correct `sold` count would need now exist, but the route output
|
||||||
|
must not change without client evidence.
|
||||||
|
* **The 5% rate itself is FIFA17-HISTORICAL**, and the rounding rule is
|
||||||
|
**NEEDS-LIVE** (the 150-coin discriminator above).
|
||||||
|
|
||||||
|
## 8. Confidence summary
|
||||||
|
|
||||||
|
| Claim | Status |
|
||||||
|
|---|---|
|
||||||
|
| The old buy path minted a duplicate and never credited a seller | **CODE** |
|
||||||
|
| No fee arithmetic existed anywhere in the project | **CODE** |
|
||||||
|
| Ownership now transfers as one row, with no duplicate possible | **TESTED** |
|
||||||
|
| Buyer debited, seller credited net, fee destroyed, in one transaction | **TESTED** |
|
||||||
|
| `buyer_debit == seller_credit + fee` | **TESTED** |
|
||||||
|
| Canonical 15,000 fixture: 20,000→5,000 / 1,000→15,250 / fee 750 | **TESTED** |
|
||||||
|
| A replayed settlement pays once | **TESTED** |
|
||||||
|
| Two racing buyers settle exactly once | **TESTED** |
|
||||||
|
| A sold card leaves the seller's lineup | **TESTED** |
|
||||||
|
| Invalid sales leave the economy unchanged | **TESTED** |
|
||||||
|
| The fee rate is 5% of gross | **FIFA17-HISTORICAL** |
|
||||||
|
| The fee is floored (150 → 143, not 142) | **UNDECIDABLE from available evidence** — CardsDLL contains no fee arithmetic, no `0.95`/`0.05` constant and no tax/net caption, so the client never computes or displays a net and there is no oracle to measure against. See `FIFA17_SOLD_WIRE_RE.md` §9. Remains a pinned CHOICE. |
|
||||||
|
| The seller-facing sold wire state | **PARTIALLY RECOVERED** from the binary — see `FIFA17_SOLD_WIRE_RE.md`. Proven: no `sold` token in either vocabulary (exhaustive); `coinsProcessed` is published to Flash as `COINS_AWARDED`; a bulk clear-sold verb `DELETE …/trade/sold` exists; the seller's SOLD counter is real (atom `sold` 0x2c9 → `FUT_TF_SOLD`). Still UNDECIDABLE statically: `highest` vs `buyNow`. |
|
||||||
|
| The client renders a settled sale correctly | **NEEDS-LIVE** — never exercised |
|
||||||
|
|
||||||
|
No Core test here establishes anything about the FIFA 17 wire contract. They
|
||||||
|
establish the domain transaction only.
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# FIFA 17 FUT Match Lifecycle
|
||||||
|
|
||||||
|
Design + contract reference for the **FUT match loop** as OpenFUT implements it on
|
||||||
|
the backend/responder side. Consolidates knowledge previously scattered across
|
||||||
|
`docs/PROJECT_STATE.md`, `fifa17-recon/tools/utas_server.py` (`match_route`),
|
||||||
|
`openfut-utas-host` (Rust economy END leg), `fifa17-recon/tools/test_match_lifecycle.py`,
|
||||||
|
and the vault (`Protocol Findings.md`, `Project State.md`, `Known Issues.md`).
|
||||||
|
|
||||||
|
Evidence labels: **OBSERVED** (live), **PROVEN** (test/static-analysis),
|
||||||
|
**HYPOTHESIS** (reasoned, not yet live-confirmed).
|
||||||
|
|
||||||
|
> ## Status caveat (read first)
|
||||||
|
> **No football match has ever started or completed in FIFA against this stack.**
|
||||||
|
> The lifecycle below is validated by CardsDLL static analysis (RPC descriptor
|
||||||
|
> blocks) + isolated persistence replay (`test_match_lifecycle.py`), **not** in-game
|
||||||
|
> acceptance. The reward amounts are FUT-plausible env-tunable defaults, **not**
|
||||||
|
> reversed values. Two blockers keep `FUT_MODES` **off by default** (see
|
||||||
|
> [Open questions](#open-questions--blockers)).
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This documents the **UTAS responder handshake + economy reward** for a match — the
|
||||||
|
HTTP calls CardsDLL makes around a match and the state they mutate. It does **not**
|
||||||
|
cover the actual football simulation (Blaze game-server side, "past the FUT hub"),
|
||||||
|
which remains unverified.
|
||||||
|
|
||||||
|
## The loop: a four-call state machine
|
||||||
|
|
||||||
|
CardsDLL issues six match RPCs; four form the playable loop. All share the base
|
||||||
|
path `ut/<sku>/match`; the operation is discriminated by **suffix + body**, not by
|
||||||
|
HTTP verb (verb selection lives outside CardsDLL, so the classifier is verb-agnostic).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Created: POST /match (no matchId)
|
||||||
|
Created --> Ready: POST|PUT /match/ready {matchId}
|
||||||
|
Ready --> Playing: POST /match {matchId} (bare path + int matchId)
|
||||||
|
Playing --> Ended: POST|PUT|DELETE /match/end {matchId, endReason, ...}
|
||||||
|
Ended --> [*]: rewards credited, W/D/L + matchesPlayed persisted
|
||||||
|
```
|
||||||
|
|
||||||
|
### Call classifier (`_match_call`, utas_server.py:3325)
|
||||||
|
|
||||||
|
The discriminator (**PROVEN** from CardsDLL RPC descriptors):
|
||||||
|
|
||||||
|
1. path ends `/match/end` → **END** (routed as `FutDestroyMatch` regardless of verb).
|
||||||
|
2. body has integer `matchId` on the **bare** `/match` path → **PLAY** (`FutPlayGame`).
|
||||||
|
CREATE and PLAY share `ut/<sku>/match`; the presence of an int `matchId` is the
|
||||||
|
only discriminator — this is why a bare `/match` carrying `matchId` must NOT
|
||||||
|
allocate a new match.
|
||||||
|
3. path ends `/match/ready` → **READY** (`FutMatchReady`).
|
||||||
|
4. otherwise → **CREATE** (`FutCreateMatch`).
|
||||||
|
|
||||||
|
## Per-call contracts
|
||||||
|
|
||||||
|
### CREATE — `POST /ut/<sku>/match` (empty/no `matchId`)
|
||||||
|
CardsDLL: `FutCreateMatch` @ `0x180120380`; deserializes `startDateTime`(740,int),
|
||||||
|
`reportIdEnabled`(641,bool). `squad`(717,nested) is a **FREEZE-RISK** and is omitted
|
||||||
|
(SKIP-safe).
|
||||||
|
|
||||||
|
Response (`match_route`, utas_server.py:3399):
|
||||||
|
```json
|
||||||
|
{"startDateTime": <unix_ts:int>, "reportIdEnabled": false, "id": <matchId:int>}
|
||||||
|
```
|
||||||
|
Effect: allocates a match id; **advances `nextItemId` by 1** (PROVEN,
|
||||||
|
`test_match_lifecycle.py:51`).
|
||||||
|
|
||||||
|
### READY — `POST|PUT /ut/<sku>/match/ready` (`{matchId}`)
|
||||||
|
CardsDLL: `FutMatchReady` — no deserializer at all on the request; the server
|
||||||
|
response parser has two scalar members + one nested member.
|
||||||
|
|
||||||
|
Response (`match_ready_body`, utas_server.py:3353):
|
||||||
|
```json
|
||||||
|
{"matchId": <int>, "opponentPersonaId": <int, default 0>}
|
||||||
|
```
|
||||||
|
- `opponentPersonaId` defaults to `0` — a neutral placeholder, **never** the
|
||||||
|
logged-in user's persona.
|
||||||
|
- The parser also has a nested `items` member (the opponent squad). It is **omitted
|
||||||
|
deliberately** until the opponent-squad item contract is recovered from a live
|
||||||
|
capture; unrecognized/absent members are skip-safe. **This omission is one of the
|
||||||
|
two blockers.**
|
||||||
|
|
||||||
|
### PLAY — `POST /ut/<sku>/match` (bare path, `{matchId:int}`)
|
||||||
|
CardsDLL: `FutPlayGame` — no request deserializer.
|
||||||
|
|
||||||
|
Response: `{}` (empty). Effect: **none** — must NOT allocate a match or advance
|
||||||
|
`nextItemId` (PROVEN, `test_match_lifecycle.py:62-63`). This is purely a client
|
||||||
|
keepalive/transition ack.
|
||||||
|
|
||||||
|
### END — `POST|PUT|DELETE /ut/<sku>/match/end` (`{matchId, endReason, myMatchStats, opponentMatchStats}`)
|
||||||
|
CardsDLL: `FutDestroyMatch` @ `0x180121b60` — **the rewards call**. Routed as
|
||||||
|
DestroyMatch regardless of verb (`FUT_MATCH_END`, default ON).
|
||||||
|
|
||||||
|
> **Wire path (Rust vs Python).** The Rust-owned reward is classified by `classify_economy`
|
||||||
|
> on **`POST /ut/delete/game/<sku>/match`** — EA/CardsDLL tunnels DELETE-semantics ops through the
|
||||||
|
> `/ut/delete/game/` prefix (`FutDestroyMatch` = `DELETE ut/%s/match/{id}`). Python's `match_route`
|
||||||
|
> additionally accepts `/ut/game/<sku>/match/end`. Which exact form the retail client emits is
|
||||||
|
> unverified (no match ever played); the Rust economy owns the `/ut/delete/game` form, and a
|
||||||
|
> `/ut/game/.../match/end` would fall to Python. Both credit the same reward shape.
|
||||||
|
|
||||||
|
Request fields that matter:
|
||||||
|
- `endReason` (atom 260) — **STRING enum, the AUTHORITATIVE result signal**. A score
|
||||||
|
comparison is NOT how the client reports the outcome. Nine values, mapped to a
|
||||||
|
win/draw/loss bucket:
|
||||||
|
|
||||||
|
| endReason | bucket |
|
||||||
|
|---|---|
|
||||||
|
| `WIN`, `DNF_WIN` | won |
|
||||||
|
| `DRAW`, `DNF_DRAW`, `NO_CONTEST` | draw |
|
||||||
|
| `LOSS`, `DNF_LOSS`, `DNF`, `QUIT` | loss |
|
||||||
|
| (missing/unknown) | draw (neutral fallback — credits without inventing a win) |
|
||||||
|
|
||||||
|
- `myMatchStats` / `opponentMatchStats` — literal-keyed objects, 15 int fields each,
|
||||||
|
first is `goals`. **Omitted by the client when `endReason` is `DNF`/`QUIT`**, so
|
||||||
|
nothing may require them. Used only as a fallback outcome probe if `endReason` is
|
||||||
|
absent.
|
||||||
|
|
||||||
|
Response (`destroy_match_body` / Rust `build_match_reward_body`) — every field a
|
||||||
|
**top-level scalar** (zero freeze risk) except the deliberately nested reward:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"allCoins": <post-credit balance:int>,
|
||||||
|
"matchCoins": <per-result coins:int>,
|
||||||
|
"seasonCoins": 0,
|
||||||
|
"tournamentCoins": 0,
|
||||||
|
"boostConis": 0, // EA's typo — exact key required
|
||||||
|
"participationAward": <int>,
|
||||||
|
"teamOfTournamentWinner": false,
|
||||||
|
"gameModeAward": { "coins": <total award:int> }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
> **Critical correction (2026-08-04):** the reward `coins` (atom 149) is read by the
|
||||||
|
> deserializer **only inside `gameModeAward`**, never as a top-level key. An earlier
|
||||||
|
> top-level `"coins"` was silently skipped and never reached the client — the one
|
||||||
|
> field most obviously named "the reward" was the one going nowhere.
|
||||||
|
|
||||||
|
Effect (persisted to the active FUT save): credit coins; increment the matching
|
||||||
|
`record.{won,draw,loss}`; increment `matchesPlayed` (PROVEN,
|
||||||
|
`test_match_lifecycle.py:74-81`).
|
||||||
|
|
||||||
|
## Reward policy
|
||||||
|
|
||||||
|
Env-tunable defaults (FUT-plausible, **not reversed** — `economy_policy.rs:20-36`,
|
||||||
|
`utas_server.py:3201-3206`):
|
||||||
|
|
||||||
|
| Result | Match coins | Participation | Total |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Win | 400 (`FUT_MATCH_COINS_WIN`) | 0 (`FUT_MATCH_PARTICIPATION`) | 400 |
|
||||||
|
| Draw | 200 (`FUT_MATCH_COINS_DRAW`) | 0 | 200 |
|
||||||
|
| Loss | 100 (`FUT_MATCH_COINS_LOSS`) | 0 | 100 |
|
||||||
|
|
||||||
|
`allCoins` = post-credit balance; `gameModeAward.coins` = per-result + participation.
|
||||||
|
|
||||||
|
## Ownership split (Rust vs Python)
|
||||||
|
|
||||||
|
The match loop is **partially migrated**. Only the coin-crediting END leg is
|
||||||
|
economy state, so only it is Rust/Core-owned; the CREATE/READY/PLAY legs are still
|
||||||
|
served by the Python oracle.
|
||||||
|
|
||||||
|
| Call | Owner | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| CREATE | Python | `utas_server.py::match_route` (via host `Route::Passthrough`) |
|
||||||
|
| READY | Python | `utas_server.py::match_route` |
|
||||||
|
| PLAY | Python | `utas_server.py::match_route` |
|
||||||
|
| END (reward) | **Rust/Core** (on `POST /ut/delete/game/<sku>/match`) | `EconomyRoute::MatchEnd` → `handle_match_end` → `build_match_reward_body`; outcome via `economy_policy::match_result_from_reason`, coins via `match_result_coins`/`match_reward_total`. 503 on Core error, never Python. Python also accepts `/ut/game/<sku>/match/end`. |
|
||||||
|
|
||||||
|
END is classified in `classify_economy` (`openfut-utas-host/src/lib.rs`) and dispatched
|
||||||
|
by the economy authority barrier ahead of the general classifier — so it can never
|
||||||
|
also reach the Python passthrough (NEVER-BOTH). The Rust END writes the coin reward
|
||||||
|
through the single Core economy transaction (`grant_reward`); W/D/L record + match
|
||||||
|
count still live in the Python save until CREATE/READY/PLAY migrate.
|
||||||
|
|
||||||
|
## Test coverage
|
||||||
|
|
||||||
|
`fifa17-recon/tools/test_match_lifecycle.py` (PROVEN, isolated — temp profile, no
|
||||||
|
live server): drives CREATE→READY→PLAY→END and asserts:
|
||||||
|
- CREATE returns `reportIdEnabled:false` + advances `nextItemId` by 1.
|
||||||
|
- READY returns exactly `{matchId, opponentPersonaId:0}`.
|
||||||
|
- PLAY returns `{}` and does **not** advance `nextItemId`.
|
||||||
|
- END credits `MATCH_COINS["won"] + MATCH_PARTICIPATION`, persists
|
||||||
|
`record == {won:1,draw:0,loss:0}` and `matchesPlayed == 1`.
|
||||||
|
|
||||||
|
## Open questions / blockers
|
||||||
|
|
||||||
|
1. **READY `items` contract (opponent squad)** — the nested `items` member of
|
||||||
|
`FutMatchReadyServerResponse` is unserved pending a live capture. Whether the
|
||||||
|
client requires it present/non-empty to enter a match is unknown. **Blocker.**
|
||||||
|
2. **Client mode-entry gate** — reaching a match from the FUT hub UI is unverified;
|
||||||
|
`FUT_MODES` stays **off by default** (the season/tournament routes return `{}`).
|
||||||
|
3. **No in-game acceptance** — every claim here is static-analysis + isolated replay.
|
||||||
|
The first live match is expected to reveal whether the static read was complete
|
||||||
|
(every match body is logged for exactly this reason).
|
||||||
|
4. **Football simulation** — the actual gameplay (Blaze game-server) is out of scope
|
||||||
|
here and unverified.
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- `docs/PROJECT_STATE.md` (match lifecycle status), `docs/direction.md` (Tier-2 loop).
|
||||||
|
- `fifa17-recon/tools/utas_server.py`: `_match_call` (3325), `match_ready_body` (3346),
|
||||||
|
`match_route` (3357), `destroy_match_body` (3281), `_match_result` (3236),
|
||||||
|
`MATCH_COINS`/`MATCH_PARTICIPATION` (3201), `_END_REASON` (3229); CardsDLL RPC
|
||||||
|
descriptor block (3183-3197).
|
||||||
|
- `openfut-adapter-fifa17/src/fut/economy_policy.rs` (reward policy + outcome map).
|
||||||
|
- `openfut-utas-host/src/lib.rs`: `EconomyRoute::MatchEnd`, `handle_match_end`,
|
||||||
|
`build_match_reward_body`.
|
||||||
|
- `fifa17-recon/tools/test_match_lifecycle.py` (lifecycle regression).
|
||||||
|
- Vault: `02 Reverse Engineering/FIFA 17/Protocol Findings.md`,
|
||||||
|
`06 Agent Memory/{Project State,Known Issues}.md`.
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Overnight session handoff — 2026-08-17
|
||||||
|
|
||||||
|
Autonomous session while you slept. Low-ceremony per your instruction. Everything
|
||||||
|
below is verified as noted; nothing was committed or pushed (see
|
||||||
|
[Uncommitted work](#uncommitted-work--needs-your-review)).
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
|
||||||
|
1. **Production promotion completed** (you chose "promotion"): prod-host swapped to the
|
||||||
|
post-P1 build, content-gap imported. Then I migrated the **remaining Python UTAS
|
||||||
|
routes that have a known contract** to Rust: account/sync, ut/auth (SID mint),
|
||||||
|
userMassInfo (full), clientdata, club/stats/{country,league,team}, and the trivial
|
||||||
|
static acks. The client's **observed FUT-hub/economy flow is now fully Rust**; a
|
||||||
|
tail of lower-traffic routes **without a captured wire shape** (item-defs,
|
||||||
|
user-identity, watchList/marketdata, non-active `squad/<n>`, draft, and mode-gated
|
||||||
|
season/tournament/champion/leaderboards/sbs) **still proxy to the Python oracle**.
|
||||||
|
2. **Launcher redesigned to a shareholder-grade egui UI** (your headline ask). Builds
|
||||||
|
clean; screenshots captured.
|
||||||
|
3. **New docs:** `MATCH_LIFECYCLE.md` (you asked), `CORE_CORRECTNESS_ISSUES.md`
|
||||||
|
(4 known Core bugs + 1 bonus exploit, ready to fix on your go/no-go).
|
||||||
|
4. **Nothing committed** — all work is in the working tree for your review (git state is
|
||||||
|
delicate: preserved-dirty Core submodule + a concurrent `funman300` actor + detached
|
||||||
|
launcher branch; I didn't want to entangle that unsupervised).
|
||||||
|
|
||||||
|
## What's live in production now (`10.10.0.120:8099`)
|
||||||
|
|
||||||
|
| Thing | State |
|
||||||
|
|---|---|
|
||||||
|
| prod-host binary | post-P1 `fda40d12` **+ my migration rebuild** (release, in `target/release/openfut-utas-host`) |
|
||||||
|
| prod-host pid | 3207781 (hub-managed, restart=no; retained spec points at the rebuilt binary) |
|
||||||
|
| Catalog | `9f6addaa` (post-P1) |
|
||||||
|
| Core content (cards) | `136d8d68` (post-P1, +18 content-gap defs) → Core loads **1710** defs |
|
||||||
|
| Core owned | **1982** (1962 players + 17 consumables + 3 staff) |
|
||||||
|
| Coins | **29,876,776** (baseline — reset from the P1 test value when the content-gap DB was swapped in; you said data isn't precious) |
|
||||||
|
| prod-core | **fixed build** from canonical submodule (`fbb54ea` + 4 correctness fixes), DB migrated ver 18 → 19; binary now `/home/alex/OpenFUT/target/release/openfut-core` |
|
||||||
|
| UTAS routes (Rust) | economy, club, squad (0/active/list/PUT), account/sync, ut/auth, userMassInfo, **user**, clientdata, hub, settings, accountinfo, leaderboards/options, match/reset, phishing, club/stats/{year,consumables,staff,country,league,team}, watchList, static acks (store/keepalive/captcha/tfa/livemessage/activeMessage) |
|
||||||
|
| Still Python (:8199) | item-defs (item/resource, defid), club-identity (clubUser, user/list, user/club), `squad/<n>` (n≠0), draft, marketdata, mode-gated (season/tournament/champion/leaderboards/sbs → `{}` while off), and match CREATE/READY/PLAY. See `docs/PRODUCTION_AUTHORITY_MATRIX.md`. |
|
||||||
|
|
||||||
|
Smoke-verified live in prod (in the prod netns): all migrated routes return
|
||||||
|
`owner=RUST`, coins consistent, clientdata round-trips, club/stats context modes emit
|
||||||
|
distinct nation/league/team buckets. Scripts:
|
||||||
|
`/home/alex/openfut-promotion/economy-2026-08-17-p2/{p2_precheck,smoke_migrated,smoke_clubstats}.py`.
|
||||||
|
|
||||||
|
## Changes made (all verified: builds clean, tests green)
|
||||||
|
|
||||||
|
### 1. Production promotion (deployed)
|
||||||
|
- Host binary `e5be8730` (P1) → `fda40d12` (post-P1) + catalog `9f6addaa`.
|
||||||
|
- Content-gap DB swapped in (owned 1962 → 1982). Backups in
|
||||||
|
`/home/alex/openfut-promotion/economy-2026-08-17-p2/backup/` + `ROLLBACK.txt`.
|
||||||
|
|
||||||
|
### 2. Route migration to Rust (deployed, rebuilt binary)
|
||||||
|
- `POST /ut/auth` — Rust mints the SID (`OPENFUT-SID-{:016X}`), opens the Rust session,
|
||||||
|
adopts persona from body. No Python. (`+ /ut/delete/auth`.)
|
||||||
|
- `POST /openfut/account/sync` — full Rust envelope; coins/unopenedPacks from Core.
|
||||||
|
- `GET /userMassInfo` — **full** Rust envelope (was a Python-proxy+overlay hybrid).
|
||||||
|
- `GET/PUT /clientdata/<key>` — new host `ClientDataStore` (JSON-persisted).
|
||||||
|
- `GET /club/stats/{country,league,team}` — made `club_stats_body` context-aware
|
||||||
|
(nation/league/team buckets); classify now routes all `club/stats/*` to Rust.
|
||||||
|
- `GET /squad/0` — routed to the Rust active-squad projection (verified structurally
|
||||||
|
identical to Python `squad/0`: same 15 keys, players=23).
|
||||||
|
- `GET /watchList` (+ no-op add/remove) — empty list + authoritative Core credits.
|
||||||
|
- Static acks (`store`, `match/keepalive`, `captcha`, `tfa`, `livemessage`,
|
||||||
|
`activeMessage`) — Rust constants (StaticAck route), byte-identical to the oracle.
|
||||||
|
- Captured the remaining routes' Python wire shapes as reference fixtures for later
|
||||||
|
migration: `docs/evidence/route-shapes-2026-08-17/` (user, defs, marketdata,
|
||||||
|
clubUser, watchList, squad/0, season/tournament/champion/sbs, draft).
|
||||||
|
- Files: `openfut-utas-host/src/{lib.rs,clientdata_store.rs(new),config.rs}`,
|
||||||
|
`openfut-adapter-fifa17/src/fut/{non_economy.rs,club_stats.rs}`,
|
||||||
|
`openfut-utas-host/tests/economy_integration.rs`.
|
||||||
|
- Tests: `openfut-utas-host` + `openfut-adapter-fifa17` full suites **GREEN**
|
||||||
|
(188 adapter + 76 host lib + all integration incl the 116s economy integration).
|
||||||
|
|
||||||
|
### 3. Launcher redesign + polish + live account panel (built, NOT deployed — client tool)
|
||||||
|
- **Redesign**: new `openfut-launcher/src/theme.rs` design system (palette, embedded
|
||||||
|
fonts, egui Visuals/Style, card/pill helpers). Branded hero header (OF monogram),
|
||||||
|
left nav rail, card-based dashboard with status pills, prominent accent Launch CTA,
|
||||||
|
console-style Logs. All existing launch/health/preflight/service/config logic preserved.
|
||||||
|
- **Polish**: OpenFUT window/taskbar icon (OF monogram `IconData`), Config tab rebuilt
|
||||||
|
into themed cards, consistency sweep.
|
||||||
|
- **Live "Your Club" panel** (new feature): a background `AccountMonitor` (mirrors
|
||||||
|
`HealthMonitor`, 5s poll, non-blocking) fetches the account summary and the Dashboard
|
||||||
|
shows a "Your Club" card — club name/abbr, Manager, **COINS hero number**, Level + XP
|
||||||
|
bar, unopened packs, account funds — with clean loading/offline/error states.
|
||||||
|
- Builds clean (0 warnings). Screenshots preserved (for your shareholder demo) in
|
||||||
|
`/home/alex/openfut-post-p1/launcher-screenshots-2026-08-17/` — `launcher_account.png`
|
||||||
|
(the populated "Your Club" card: COINS 29,876,776, Level 12, packs 3) is the headline;
|
||||||
|
plus dashboard/setup/logs/config + the offline state. All reviewed — product-quality.
|
||||||
|
- Files: `openfut-launcher/src/{theme.rs(new),account_monitor.rs(new),app.rs,main.rs,
|
||||||
|
account_sync.rs,config.rs}` + `assets/` (fonts + icon). All additive; behavior preserved.
|
||||||
|
|
||||||
|
### 4. Core correctness fixes (deployed 2026-08-17)
|
||||||
|
|
||||||
|
All four `CORE_CORRECTNESS_ISSUES.md` classes fixed in the canonical `openfut-core`
|
||||||
|
submodule and deployed to prod-core (see that doc's "Resolution" section):
|
||||||
|
- **Issue 3 (HIGH, exploit):** SBC duplicate-card free-reward — `submit_sbc` now dedups
|
||||||
|
ids + bounds the list (`MAX_SBC_CARDS`=30) → `BadRequest`. Regression test added.
|
||||||
|
- **Issue 2 (HIGH):** non-atomic economy mutations — closed the concurrency-exploit
|
||||||
|
races with single-statement compare-and-swap (`spend_coins` conditional debit,
|
||||||
|
`open_pack`/`buy_listing`/`sell_card`/`checkin` claim-then-act). Multi-statement
|
||||||
|
partial-failure edges (`buy_pack`, concurrent SBC, match/season chains) left as
|
||||||
|
documented residual — need the transaction refactor, negligible for single-player.
|
||||||
|
- **Issue 4 (LOW):** `season.rs` `.expect()` panics → graceful `AppError`; checkin index
|
||||||
|
`% 7` → `.rem_euclid(7)`.
|
||||||
|
- **Issue 1 (LOW):** `sbc_submissions.club_id` — migration `0019` (add + backfill) +
|
||||||
|
`submit_sbc` binds it; milestone query now correct.
|
||||||
|
- Verified: Core suite **179 green** + clippy clean; migration dry-run on a prod-DB copy;
|
||||||
|
post-deploy prod migration ver 19, owned 1982, coins 29,876,776, all Core + host
|
||||||
|
endpoints 200. Rollback: `backup/prod-core.preCoreFix.db` (ver 18) + old binary path —
|
||||||
|
see `backup/ROLLBACK_CORE_FIX.txt`.
|
||||||
|
|
||||||
|
## New / updated docs
|
||||||
|
|
||||||
|
- `docs/MATCH_LIFECYCLE.md` (NEW) — consolidated FUT match loop design (CREATE→READY→
|
||||||
|
PLAY→END), contracts, reward policy, ownership split, blockers. (You asked for this.)
|
||||||
|
- `docs/CORE_CORRECTNESS_ISSUES.md` (NEW) — 4 known Core bugs + 1 bonus SBC
|
||||||
|
duplicate-card exploit, each with file:line + concrete fix + severity. **Needs your
|
||||||
|
go/no-go** (fixing bumps Core off the frozen P1 reference).
|
||||||
|
- `docs/PRODUCTION_AUTHORITY_MATRIX.md` (UPDATED) — reflects the completed migration.
|
||||||
|
- Vault `06 Agent Memory/Current Priorities.md` (UPDATED).
|
||||||
|
|
||||||
|
## Uncommitted work — needs your review
|
||||||
|
|
||||||
|
**I committed nothing** (git state is delicate: openfut-core is intentionally
|
||||||
|
preserved-dirty; a concurrent `funman300` actor; launcher on detached HEAD `d1a71bd`).
|
||||||
|
Review + commit these when you're ready:
|
||||||
|
|
||||||
|
- Superproject (mine): `openfut-utas-host/src/{lib.rs,config.rs}`,
|
||||||
|
`openfut-utas-host/src/clientdata_store.rs`,
|
||||||
|
`openfut-utas-host/tests/{economy_integration.rs,host_test.rs}`,
|
||||||
|
`openfut-adapter-fifa17/src/fut/{club_stats.rs,non_economy.rs}`,
|
||||||
|
`docs/{PRODUCTION_AUTHORITY_MATRIX.md,MATCH_LIFECYCLE.md,CORE_CORRECTNESS_ISSUES.md,OVERNIGHT_HANDOFF_2026-08-17.md,PYTHON_RETIREMENT_PLAN.md}`,
|
||||||
|
and the reference fixtures `docs/evidence/route-shapes-2026-08-17/`.
|
||||||
|
- Launcher submodule (mine): `src/{app.rs,main.rs,theme.rs(new),account_monitor.rs(new),account_sync.rs,config.rs}`, `assets/` (fonts + icon).
|
||||||
|
- **Leave the pre-existing dirt alone** (not mine): `CLAUDE.md`, `README.md`,
|
||||||
|
`.env.example`, `docker-compose.yml`, `AGENTS.md`, `setup.sh`, `openfut-bridge`,
|
||||||
|
`fifa17-recon/docker/...`, `docs/{ARCHITECTURE,ROADMAP,docker,fifa17-emulation}.md`,
|
||||||
|
`docs/research/`, `scripts/utas-filter-diff.py`.
|
||||||
|
- `openfut-core` (mine, this session): `migrations/0019_sbc_submissions_club_id.sql` (new),
|
||||||
|
`src/services/{sbc.rs,club.rs,pack.rs,market.rs,checkin.rs,season.rs}`,
|
||||||
|
`tests/integration_test.rs`. Built + deployed to prod-core; still detached HEAD at
|
||||||
|
`fbb54ea` (edits uncommitted, per your branch strategy).
|
||||||
|
|
||||||
|
## Open decisions for you
|
||||||
|
|
||||||
|
1. ~~**Core correctness bugs**~~ — **DONE (2026-08-17):** all four classes fixed +
|
||||||
|
deployed to prod-core (see "Core correctness fixes" above and the Resolution section
|
||||||
|
of `docs/CORE_CORRECTNESS_ISSUES.md`). prod-core is now off the frozen P1 point,
|
||||||
|
running `fbb54ea` + fixes at migration ver 19. Residual (documented): the
|
||||||
|
multi-statement transaction refactor for partial-failure atomicity — negligible for
|
||||||
|
single-player; do it if/when concurrency matters.
|
||||||
|
2. **Match handshake legs** (CREATE/READY/PLAY) — the only routes still on Python.
|
||||||
|
Deferred: no match has ever been played in-game, READY `items` contract unknown,
|
||||||
|
`FUT_MODES` off (see `docs/MATCH_LIFECYCLE.md`). Migrating them risks the economy
|
||||||
|
`/match/end` routing for a never-exercised path — I judged it not worth it unmonitored.
|
||||||
|
3. **Aux service container cutover** (blaze/redirector/roster → Rust) — operator-gated
|
||||||
|
container change; unchanged.
|
||||||
|
|
||||||
|
## Rollback (still hot)
|
||||||
|
|
||||||
|
- Python P2 image `openfut-fut-backend:p2-rollback` (b1b929953f) + profile 39bb3e83 +
|
||||||
|
`rollback-to-python-p2.sh`.
|
||||||
|
- prod-host P1 binary + P1 catalog backed up in
|
||||||
|
`/home/alex/openfut-promotion/economy-2026-08-17-p2/backup/` (see `ROLLBACK.txt`).
|
||||||
|
- prod state (pre-content-gap) backed up: `backup/prod-core.preB.db`,
|
||||||
|
`prod-identity.preB.json`, plus P1 `fifa17-production-{catalog,cards}.p1.json`.
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Production Authority Matrix (post-P1 -> P2-routes promoted 2026-08-17)
|
||||||
|
|
||||||
|
> **P2-routes promoted to production 2026-08-17.** prod-host swapped P1 `e5be8730` -> post-P1
|
||||||
|
> `fda40d12`; catalog `35a0913b` -> `9f6addaa` (resolves all owned assets, dropped_no_asset=0).
|
||||||
|
> Every previously-Python non-economy route now RUST in prod (OBSERVED prod log). Rollback hot:
|
||||||
|
> restore P1 binary + backup catalog (`economy-2026-08-17-p2/backup/`).
|
||||||
|
|
||||||
|
Definitive inventory of every production-reachable FIFA17 route/service and its
|
||||||
|
current owner. Derived from the live `prod-host` dispatch log (owner= labels,
|
||||||
|
real Client A session 2026-08-14), `openfut-utas-host/ROUTE_AUTHORITY.md`, and the
|
||||||
|
prod container config (`OPENFUT_SERVERS="blaze roster pow"`).
|
||||||
|
|
||||||
|
Evidence labels: OBSERVED (live log/db), PROVEN (test), INFERRED, HYPOTHESIS.
|
||||||
|
|
||||||
|
Legend: owner R = Rust/Core, P = Python oracle (:8199 proxied via PYTHON_FALLBACK).
|
||||||
|
|
||||||
|
## UTAS HTTP (front door: openfut-utas-host :8099)
|
||||||
|
|
||||||
|
### ECONOMY — already Rust (Python economy hits = 0, OBSERVED)
|
||||||
|
| Method/path (tail) | Prod owner | Writes state | Rust handler | Py proxy |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| GET /user/credits | R | no | handle_credits | NO |
|
||||||
|
| GET /store/purchasegroup[/all] | R | no | handle_purchasegroup | NO |
|
||||||
|
| PUT /store/transaction[/<id>] | R | coins,inv,pile | handle_store_buy | NO |
|
||||||
|
| POST /purchased[/items] | R | coins,inv,ent,pile | handle_pack_open | NO |
|
||||||
|
| GET /purchased[/items] | R | no | shape_purchased_reveal | NO |
|
||||||
|
| DELETE /item/<id> | R | coins,inv | handle_quick_sell_path | NO |
|
||||||
|
| POST /ut/delete/../item | R | coins,inv | handle_quick_sell_body | NO |
|
||||||
|
| PUT /item | R | inv,pile | handle_move_items | NO |
|
||||||
|
| POST /ut/delete/../match | R | coins | handle_match_end | NO |
|
||||||
|
| POST /auctionhouse,/transfermarket | R | listings | handle_market_list | NO |
|
||||||
|
| GET /tradePile | R | no | handle_market_query | NO |
|
||||||
|
| GET /tradePile/counts | R | no | handle_market_counts | NO |
|
||||||
|
| /trade/<id> (POST/PUT/GET) | R | coins,inv,listings | handle_market_buy | NO |
|
||||||
|
| DELETE /ut/delete/../trade/<id> | R | listings | handle_market_cancel | NO |
|
||||||
|
|
||||||
|
**Transfer-market semantics (live-verified 2026-08-17).** Three things here are
|
||||||
|
load-bearing and were each a live defect:
|
||||||
|
1. `/tradePile` and `/tradePile/counts` are **different deserializers** and MUST NOT
|
||||||
|
share a handler. `/counts` is FutGetAuctionCount: five scalar ints
|
||||||
|
(`count`, `maxAuctionsAllowed`, `offered`, `selling`, `sold`) and nothing else.
|
||||||
|
Served the `auctionInfo` body it skips every field, leaving the counts at 0 — the
|
||||||
|
hub tile shows a listing while the Transfer List screen shows no active sale.
|
||||||
|
2. An auction record's `itemData` MUST be the **full card object** (the same shape
|
||||||
|
`/club` emits), not a stub. A listing therefore persists a shaped-card SNAPSHOT
|
||||||
|
(`listings.item_json`) at list time. The seller's own pile stamps
|
||||||
|
`itemState: listFS`; market search uses `forSale`.
|
||||||
|
3. `POST /auctionhouse` resolves the listed card **server-side** from the wire item
|
||||||
|
id (`wire → Core instance → card_id + resourceId`); the client's FutISStart body
|
||||||
|
carries only the id. This also enforces that you can only list what you own.
|
||||||
|
|
||||||
|
### NON-ECONOMY — Rust-owned (route migration 2026-08-17, OBSERVED prod log)
|
||||||
|
| Route | Owner | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| POST /ut/auth (+ /ut/delete/auth) | R | Rust mints the SID (OPENFUT-SID-{:016X}); opens Rust session; adopts persona from body. No Python. |
|
||||||
|
| POST /openfut/account/sync | R | full Rust envelope; coins/unopenedPacks from Core; clubName OpenFUT / clubAbbr OFC constants |
|
||||||
|
| GET /userMassInfo | R | FULL Rust envelope (userInfo+squad+settings+pileSizeClientData); no Python. coins from Core, squad == /squad/active |
|
||||||
|
| GET/PUT /clientdata/<key> | R | host ClientDataStore (JSON-persisted); PUT acks {}, GET returns blob or {} |
|
||||||
|
| capability (/openfut/fifa17/capability) | R | -> Bound (CleanV1) |
|
||||||
|
| GET /club, /club/* readers | R | Core-backed collection (dropped_no_asset=0). Cards with an **ACTIVE listing are excluded** — a listed card has left the club. Keyed on the listing, NOT on the `trade` pile: the pile can hold cards with no listing (bare move, or cancelled/sold) and `/tradePile` renders only ACTIVE listings, so hiding the pile would make those invisible in BOTH views. Keying on the listing is self-healing — cancel/sale restores club visibility with no extra transition. Pagination then runs over the club-visible set (Core cannot filter host-owned listing state, so letting it paginate would yield short pages); with nothing hidden the fast Core-paginated path is unchanged. |
|
||||||
|
| GET /squad/0, /squad/active, /squad/list; PUT /squad/<n> | R | Core squad projection + tx; GET /squad/0 == active squad (verified structurally identical) |
|
||||||
|
| GET /user/accountinfo | R | {} |
|
||||||
|
| GET /user | R | `{"userInfo": …}` — same userInfo builder as userMassInfo (shared); squad rating is Core-authoritative (DIFFERENT-BY-DESIGN vs Python's stale value) |
|
||||||
|
| GET /settings | R | {"configs":[]} |
|
||||||
|
| GET /leaderboards/options | R | {} |
|
||||||
|
| PUT /match/reset | R | {} |
|
||||||
|
| GET /phishing/trusteddevice | R | security-question stateless ack |
|
||||||
|
| GET /hub | R | Core-derived counts; `clubPlayers` excludes actively-listed cards, `auctionCount`/`tradePile` from the durable market store |
|
||||||
|
| GET /club/stats/{year,consumables,staff,country,league,team} | R | Core aggregation; context buckets keyed nation/league/team (owned=1982); staff={} |
|
||||||
|
| GET /store, /match/keepalive, /captcha, /tfa, /livemessage, /activeMessage | R | unconditional constant acks (byte-identical to the oracle; StaticAck route) |
|
||||||
|
| GET /watchList (+ PUT/POST/DELETE) | R | empty watch list + authoritative Core credits; add/remove is a no-op ack (oracle persists none) |
|
||||||
|
| GET /season, /tournament, /champion, /clubUser, /user/list | R | FUT modes + club-identity off → `{}` (FeatureOffEmpty; byte-identical to the flag-off oracle). Migrated + deployed 2026-08-17 |
|
||||||
|
| POST /ut/.../match/end (DestroyMatch) | R | economy reward (coins credited via Core grant_reward) |
|
||||||
|
| GET /item/resource, /defid | R | `{itemData:[item_def…]}` — asset=rid&0xffffff; hardcoded Ronaldo (20801) + placeholder ("Player",75,CM,attrs 70), mirroring the oracle's `item_def`. Client renders from its LOCAL DB, so the placeholder is exact parity. Migrated + deployed 2026-08-17 |
|
||||||
|
| GET /marketdata, /marketdata/pricelimits | R | suggested pricing, constant band 150..15000. `/pricelimits` = bare ARRAY (one per defId); plain `/marketdata` = OBJECT — container type is load-bearing (object-where-array froze a live client). Migrated + deployed 2026-08-17 |
|
||||||
|
|
||||||
|
### NON-ECONOMY — still Python (PYTHON_FALLBACK)
|
||||||
|
| Method/path | Owner | Reason |
|
||||||
|
|---|---|---|
|
||||||
|
| POST /user/club (rename) | P | mutating club rename; Core has `clubs` but rename needs a Core write (deferred). Reads `clubUser`/`user/list` are now Rust. |
|
||||||
|
| GET /squad/<n> (n≠0, non-active) | P | no multi-squad Core model (Rust owns squad/0, squad/active, /squad/list, PUT) |
|
||||||
|
| /squad/mode/draft/* | P | FUT Draft mode |
|
||||||
|
| GET /leaderboards, /sbs/* | P | mode-gated (FUT_MODES/_SBC off → `{}`/content; `/sbs/sets` ships content); real behavior needs the mode logic ported. `season`/`tournament`/`champion` are now Rust. |
|
||||||
|
| POST /ut/.../match (CREATE), /match/ready (READY), /match (PLAY) | P | match handshake legs; no match ever played in-game (see docs/MATCH_LIFECYCLE.md) |
|
||||||
|
|
||||||
|
## AUXILIARY SERVICES (prod container OPENFUT_SERVERS="blaze roster pow")
|
||||||
|
All aux services are **Python in production today** (container `entrypoint.sh` runs
|
||||||
|
`blaze_responder_v3b.py`/`roster_server.py`/`pow_server.py`). Rust equivalents exist
|
||||||
|
outside Docker; deploying them is operator-gated (production deployment forbidden here).
|
||||||
|
|
||||||
|
| Service | Rust crate | Completeness | Prod owner | Reachable | Blocker to candidate |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Blaze (:42130) | openfut-blaze-host + openfut-protocol-blaze + adapter::blaze | COMPLETE, gate-proven to real FUT (Gate 10: 3 logins, 10 pack opens, 448/448 py suite) | Python | yes | ERRC error-reply placement unresolved; wire into candidate bring-up |
|
||||||
|
| Redirector (:42127 TLS) | openfut-redirector-host + openfut-tls (OpenSSL vendored) | COMPLETE, 1 live handshake 2026-08-11 (TLSv1.2/AES256-GCM-SHA384) | Python | yes | never full-path gated; cert consistency |
|
||||||
|
| Roster (:8081) | openfut-roster-host | COMPLETE, oracle-parity + lifecycle tests, unit-only | Python | yes | never live-gated |
|
||||||
|
| POW (:8094 + :8080) | NONE (only pow_server.py) | no Rust host; 58 templates, bodies placeholder | Python | yes | LARGEST: no crate + bodies un-reversed |
|
||||||
|
| Nucleus (:42131) | none (advertised string only) | n/a | Python stub (advertised, unused) | NO (0 live hits) | DEAD in current flow — keep advertising URL, no migration |
|
||||||
|
|
||||||
|
RETIRED/out-of-scope: fifa-blaze (FIFA23 capture stub), openfut-bridge (FIFA23). Not in FIFA17 flow.
|
||||||
|
|
||||||
|
## NON-ECONOMY UTAS TARGET OWNER (confirmed via scouts)
|
||||||
|
Core already exposes GET/PUT /settings, GET /club|/collection|/statistics|/profile, /squad/*.
|
||||||
|
New Rust arm = Route variant + classify() arm (lib.rs:118-140) + owner-labelled handler.
|
||||||
|
|
||||||
|
| Route | Port complexity | Target | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| user/accountinfo | trivial-static ({}) | R | host constant |
|
||||||
|
| settings | trivial-static ({"configs":[]}) | R | host constant / Core /settings |
|
||||||
|
| leaderboards/options | trivial-static | R | host constant |
|
||||||
|
| match/reset | trivial-static ({}) | R | host constant ack |
|
||||||
|
| phishing/trusteddevice | small (validate hex + constant) | R | security-question: stateless ack, always verified/trusted |
|
||||||
|
| clientdata/userHubData | small stateful | R | Core PUT /settings upsert keyed userHubData |
|
||||||
|
| hub | medium (derived counts) | R | Core collection + market counts |
|
||||||
|
| club/stats/year | medium (rating-tier aggregation) | R | Core /collection + /statistics |
|
||||||
|
| club/stats/consumables | medium | R | BLOCKED on 17-consumable import |
|
||||||
|
| club/stats/staff | trivial ({}) or derived | R | BLOCKED on 3-staff import |
|
||||||
|
| account/sync | small-medium (persona select + save) | R | hardest: no direct Core route; host session/persona logic |
|
||||||
|
| ut/auth envelope | small (SID mint) | R (currently OBSERVE) | Python still mints envelope; boundary decision |
|
||||||
|
|
||||||
|
## MIGRATION PRIORITY ORDER (Phase 12)
|
||||||
|
1. Content gap consumable+staff emit (unblocks club/stats/{consumables,staff}) — import crate.
|
||||||
|
2. Trivial-static host routes: accountinfo, settings, leaderboards/options, match/reset, phishing/trusteddevice.
|
||||||
|
3. clientdata/userHubData (Core settings upsert).
|
||||||
|
4. hub + club/stats/year (Core-derived aggregation).
|
||||||
|
5. userMassInfo full ownership (envelope scaffold; econ+squad already Rust).
|
||||||
|
6. account/sync + ut/auth envelope (account/session boundary).
|
||||||
|
7. Aux candidate wiring (blaze/roster/redirector already built) + POW (blocked) + Nucleus (dead).
|
||||||
|
|
||||||
|
## ECONOMY EXPECTATION
|
||||||
|
Python economy hits = 0 (OBSERVED live + PROVEN by NEVER-BOTH/no-fallback tests).
|
||||||
|
Any nonzero Python economy hit = P1 regression, priority zero.
|
||||||
|
|
||||||
|
## STATUS
|
||||||
|
Baseline + economy + Rust-owned non-economy rows: OBSERVED/PROVEN.
|
||||||
|
Aux + non-economy target owners: confirmed via HostSourceMap/PythonContractMap/AuxServiceMap/ContentGapMap scouts.
|
||||||
|
|
||||||
|
## CANDIDATE MIGRATION STATUS (post-P1 progress, off-production)
|
||||||
|
DONE (Rust-owned in candidate source; committed on top of 5020137; workspace tests green):
|
||||||
|
- Content gap: consumable+staff emit (20 instances / 18 defs; verified real-profile re-import 1962+20).
|
||||||
|
- Non-economy routes migrated to Rust: user/accountinfo, settings, leaderboards/options,
|
||||||
|
match/reset, phishing/{trusteddevice,question,validate}, club/stats/staff, hub,
|
||||||
|
club/stats/{year,consumables}.
|
||||||
|
(hub clubPlayers = owned player count from Core; may be < Python profile count by the deferred
|
||||||
|
Legend instances = DIFFERENT-BY-DESIGN.)
|
||||||
|
- Reachability reporter (scripts/openfut-reachability.py) gates Python-hit invariants.
|
||||||
|
|
||||||
|
RESIDUAL PYTHON (still proxied; each has a concrete blocker, not ordinary difficulty):
|
||||||
|
- club/stats/{country,league,team} — RESIDUAL (nation/league/team context sub-screens). The global
|
||||||
|
MY-CLUB stat set (club/stats/{year,consumables,staff}) is now Rust (adapter club_stats.rs faithful
|
||||||
|
port of fut_club_stats.py VOCAB + counts, Core-accurate over real imported content, staging-verified
|
||||||
|
RUST route=club-stats owned=1982 players=1962). The per-nation-bucket context sub-screens still proxy
|
||||||
|
Python (low value, unrecognized atoms inert); migrate with a live context capture if ever needed.
|
||||||
|
- account/sync — launcher-facing (POST /openfut/account/sync, pre-auth); envelope has fields not in
|
||||||
|
Core (clubAbbr, established, profilePath, accountFunds cap); changing it risks the launcher. Needs
|
||||||
|
a Core account endpoint or host account logic + launcher-contract verification.
|
||||||
|
- userMassInfo — SAFE hybrid today (Rust overlays economy+squad on Python envelope). Full ownership
|
||||||
|
needs a real full-envelope capture first (missing one scaffold field breaks a hot route); deferred.
|
||||||
|
- ut/auth — ORDERING-BLOCKED: still-proxied Python routes rely on Python's session table, so auth
|
||||||
|
must stay proxied (Python mints SID, Rust OBSERVEs) until every session-needing route is Rust. Migrate LAST.
|
||||||
|
- clientdata/userHubData — BLOCKED: Core /settings supports only 2 fixed keys (difficulty,
|
||||||
|
preferred_formation); storing an arbitrary blob needs a frozen-Core change or a new host store.
|
||||||
|
Low-value UI-pref blob; kept Python per Phase 23 (no Python-by-ideology).
|
||||||
|
|
||||||
|
AUX (Rust built, deploy = operator-gated container cutover, NOT this task): blaze/roster/redirector.
|
||||||
|
POW = blocked (no crate, bodies un-reversed). Nucleus = dead (0 live hits).
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# OpenFUT — Project State
|
||||||
|
|
||||||
|
> **Canonical source:** `../OpenFUT-Vault/06 Agent Memory/Project State.md`
|
||||||
|
> This file is a mirror. If the two disagree, the vault wins. Update the vault first.
|
||||||
|
|
||||||
|
Factual snapshot. Prefer this over the stale root `README.md`/`CLAUDE.md` status tables (FIFA 23).
|
||||||
|
Last compiled from repository evidence during context initialization.
|
||||||
|
|
||||||
|
## Working
|
||||||
|
|
||||||
|
- **FIFA 17 offline FUT stack, end-to-end.** Proven 2026-08-01: auth → Blaze login → device-trust →
|
||||||
|
the FUT hub. Brought up by `fifa17-recon/tools/openfut-fut.sh start`. Evidence: `FUT-RUNBOOK.md`,
|
||||||
|
`fifa17-recon/README.md`, the five responder scripts, gate-ladder troubleshooting table.
|
||||||
|
- **ProtoSSL cert-pin defeat** — two live `/proc/PID/mem` patches (`autopatch.py`), VAs stable
|
||||||
|
across launches. gdb-verified which gate was the wall.
|
||||||
|
- **LSX / Origin layer** — crypto handshake reversed byte-exact and confirmed against the repack's
|
||||||
|
own emu disassembly (`docs/REPACK_INTEL.md`); Origin login gates cleared.
|
||||||
|
- **Blaze redirector + Fire2/Heat2** — both hops defeated; preAuth/login/personas answered.
|
||||||
|
- **UTAS/RS4 FUT API** — `ut/auth` + boot calls + device-trust reach the hub with hand-authored JSON.
|
||||||
|
- **Persistent FIFA 17 account selection** — the launcher synchronizes one configured EA persona to
|
||||||
|
the Python backend before starting LSX/FIFA. LSX, Blaze, POW/EASFC, and UTAS then share that
|
||||||
|
identity, while FUT coins, inventory, squads, progression, and unopened packs persist in an
|
||||||
|
isolated save beneath `fifa17-recon/docker/state/accounts/<persona-id>/`. The POW level/XP/funds
|
||||||
|
shown in FIFA's general account bar are account-scoped but remain distinct from FUT club coins.
|
||||||
|
A reversible server test on 2026-08-09 verified profile switching, POW values, a 400-coin pack
|
||||||
|
debit, five awarded items, and restoration of the original profile.
|
||||||
|
- **Account-scoped FUT security compatibility** — launcher account synchronization initializes a
|
||||||
|
persisted `securityQuestion` verification record in that persona's FIFA 17 profile. The UTAS
|
||||||
|
PHISHING handler returns the complete CardsDLL trusted-console response (`changed`, `exists`,
|
||||||
|
`locked`, `trusted`), accepts only well-formed legacy setup/validate requests under `X-UT-SID`,
|
||||||
|
and never stores or logs the client-transformed answer. This is server-side emulation; the hook
|
||||||
|
and launcher do not contain an answer or add UI automation. Automated contract coverage is in
|
||||||
|
`fifa17-recon/docker/ctx/tools/test_security_question.py`; live first/repeat-launch acceptance is
|
||||||
|
partially complete: the first launch entered FUT without a security dialog on 2026-08-09; a
|
||||||
|
second fresh-process FUT entry is still required to close persistence acceptance.
|
||||||
|
- **Safe responder diagnostics** — ordinary LSX logs redact challenge/session/auth-code attributes;
|
||||||
|
ordinary Blaze logs redact auth/session keys and no longer emit raw Fire2 hex, decoded TDF, or
|
||||||
|
config values. Forensic Blaze capture remains available only with the explicit
|
||||||
|
`OPENFUT_BLAZE_DUMP_FRAMES=1` opt-in. LSX and Blaze self-tests cover the new defaults.
|
||||||
|
- **OpenFUT Core** — Rust FUT economy backend, feature-complete for its scope and tested: profiles,
|
||||||
|
clubs, coins, packs, cards, squads, chemistry styles, SBCs, objectives, matches, market (NPC),
|
||||||
|
draft, FUT Champs, seasons, statistics, achievements, events, daily check-in, division
|
||||||
|
leaderboard, market trade history. 13 migrations. Integration suite (`tests/integration_test.rs`,
|
||||||
|
96 test fns) runs against in-memory SQLite; CI (fmt/clippy/build/test) green on `openfut-core`.
|
||||||
|
|
||||||
|
## Partially implemented
|
||||||
|
|
||||||
|
- **FIFA 17 FUT hub depth** — reaching the hub is proven, but how much of FUT is fully navigable
|
||||||
|
beyond it (playing matches, pack opening, SBC submission through the *game* UI vs. spinner/error
|
||||||
|
states) is not documented as complete. The runbook's gate ladder lists failure modes still
|
||||||
|
guarded against. Treat "past the hub" as unverified.
|
||||||
|
- **Pack opening through the game UI** — proven live on 2026-08-09 with the recovered CAGE test
|
||||||
|
profile: purchase, reveal, item assignment/quick-sell, wallet refresh, and return from the reveal
|
||||||
|
all completed. The Python transaction path also passes its 446-check contract suite. FIFA's
|
||||||
|
hardcoded post-reveal `mypacks` return is supported by a short-lived active grace record for every
|
||||||
|
opened pack; it is excluded from unopened-pack counts and retired at the next hub request.
|
||||||
|
- **FIFA 17 FUT match lifecycle** — CardsDLL static analysis and isolated responder tests now cover
|
||||||
|
CREATE→READY→PLAY→END. Bare `/match` requests carrying body `matchId` are classified as PLAY
|
||||||
|
instead of accidentally allocating another match; READY returns the verified scalar `matchId`
|
||||||
|
and `opponentPersonaId` fields; END persists W/D/L, matches played, and coin rewards per account.
|
||||||
|
The implementation is deployed and `test_match_lifecycle.py` passes, but no football match has
|
||||||
|
started or completed in FIFA yet. The READY opponent `items` contract and client mode-entry gate
|
||||||
|
remain unresolved; `FUT_MODES` therefore stays off by default.
|
||||||
|
- **Core ↔ emulation integration** — the two halves exist and wiring has **started**. First
|
||||||
|
slice (2026-08-11): the My Squad owned-player search. `openfut-core` gained a semantic,
|
||||||
|
game-independent owned-inventory query (`services::inventory::{OwnedItemQuery, apply_query}`
|
||||||
|
+ a `Quality` tier) that filters (AND) → orders deterministically → paginates, wired into
|
||||||
|
`GET /collection`; `openfut-adapter-fifa17::fut::owned_query` parses the FIFA17 `/club` wire
|
||||||
|
query and resolves numeric league/nation/team ids → semantic names (unknown id = hard error,
|
||||||
|
no raw-id passthrough). Intentional fix, not parity: Python applies only `league`+`team` and
|
||||||
|
ignores `level`/`rare`/`position`/`nation`/`start`/`count` (the request-amplification bug);
|
||||||
|
Core applies all proven filters and paginates. `rare=SP` semantics UNKNOWN, unimplemented.
|
||||||
|
Slice 2 (2026-08-11): `openfut-utas-host` — the first live UTAS host. Serves `GET …/club`
|
||||||
|
from Core through the adapter and reverse-proxies every other UTAS route verbatim to the
|
||||||
|
Python oracle (`utas_server.py`); plaintext HTTP/1.1 keep-alive, classify-before-execute,
|
||||||
|
no python-fallback after a Core error. `CoreAccess` is a host-owned boundary (the adapter
|
||||||
|
stays transport-agnostic). 11 host + 22 adapter tests; 10/10 mutations killed; fmt/clippy
|
||||||
|
clean. Slice 3 (2026-08-11, `3ef3bc3`): the real `Fifa17IdentityResolver` — catalog
|
||||||
|
(card id → real asset id) + persistent `openfut-identity` store (owned instance →
|
||||||
|
stable/reversible wire int) + wire-id policy, replacing all placeholders (one
|
||||||
|
production path). Wire-id namespace is globally monotonic within `(fifa17, owned-item)`,
|
||||||
|
not per-account (Core owned ids are UUIDs → unambiguous reverse). Slice 4 (2026-08-11,
|
||||||
|
core `36abd4b`): a curated 32-card real FIFA17 dev content pack
|
||||||
|
(`data/games/fifa17/dev/cards.json`, ids `fifa17_<asset>`), loaded only via opt-in
|
||||||
|
`Config.dev_content_games`; `seed-dev` grants a `game_id=fifa17` profile+club real
|
||||||
|
`OwnedCard`s (no FIFA wire ids — the resolver mints those at request time), idempotent,
|
||||||
|
default content untouched. Slice 5 (2026-08-11, `5276dd2`): the host sends
|
||||||
|
`X-OpenFUT-Game: fifa17` so `/club` resolves the all-mapped fifa17 profile —
|
||||||
|
**composition proven live** over HTTP (real Core+host, no FIFA client): FIFA wire query
|
||||||
|
→ real `resourceId`s (catalog) + stable/reversible wire `id`s (store); 33 renderable,
|
||||||
|
gold=22, Premier League=18, pagination page1=11/page2=7/overlap=0 (clean paging, no
|
||||||
|
drops). **The only remaining gate is the live retail FIFA A/B** (no FIFA client in the
|
||||||
|
build env; runbook `openfut-utas-host/README.md`). See the vault UTAS Endpoint Map +
|
||||||
|
Known Issues (incl. "identity resolution is NOT authorization" for later mutations).
|
||||||
|
**Slice 6 (2026-08-11): `/club` RUNTIME VALIDATED on retail FIFA 17** — operator-assisted
|
||||||
|
live A/B (`.105` client → `.120` backend via a source-scoped NAT redirect into a staged
|
||||||
|
`36abd4b` Core + `openfut-utas-host`). Every checkpoint passed on the real client:
|
||||||
|
transport, per-route Python fallback, Python-negative `/club`, Core `X-OpenFUT-Game`
|
||||||
|
scoping, real card + persistent owned-item identity (incl. the two-copy fixture),
|
||||||
|
no-filter/Gold/position/nation/league/team/combined-AND filtering, retail pagination
|
||||||
|
with no amplification, card selection, identity stability across relaunch, Python
|
||||||
|
rollback, and Rust re-enable (identity store byte-identical across the cycle, 0
|
||||||
|
reallocation). Live findings: the retail client steps `start += 10` with `count=11`
|
||||||
|
(sometimes bulk `count=100`) — Core honours `offset` and terminates; the My Club UI
|
||||||
|
nests team under league; the "~1900" club counter is Python `userMassInfo`, not `/club`.
|
||||||
|
Remaining is operational only (promote the staged stack to a durable deployment).
|
||||||
|
- **Slice 7 (2026-08-12): FUT squad authority (read + write) RUNTIME VALIDATED on retail FIFA 17.**
|
||||||
|
Staged operator-assisted A/B (`.105` retail client → `.120`, source-scoped utas switch
|
||||||
|
`:8099→:8199` into `openfut-utas-host` over a staged `615c5fd` Core seeded by `seed-dev` with the
|
||||||
|
dev 33-card inventory). FIFA itself consumed the Rust squad path end-to-end: FUT boot served
|
||||||
|
`RUST_OVERLAY userMassInfo` (squad overlay) and the squad screen rendered the dev XI; a controlled
|
||||||
|
in-game squad edit issued `PUT …/squad/0` → host `squad-replace` → Core → `{"id":0}`; an in-game
|
||||||
|
formation change f442→f433 persisted to Core (canonical fingerprint changed, `position_index`
|
||||||
|
remapped 0–10); a FULL FIFA relaunch cold-fetched and rendered the persisted f433 squad (no client
|
||||||
|
cache). Reversibility proven on the exact client path by log presence, not response data (both
|
||||||
|
backends coincidentally hold the same dev squad — the Python oracle `fifa17_profile.json` was
|
||||||
|
seeded from the same `squad_put_f442.json` capture): disarmed `.105:8099` reached Python (absent
|
||||||
|
from host log), re-armed reached Rust (`route=club limit=Some(9)` present); the persistent identity
|
||||||
|
store survived the cycle with 0 reallocation. Non-migrated routes (`/ut/auth`, `account/sync`,
|
||||||
|
`accountinfo`, `settings`, `hub`, store txn) correctly Python-fallback. NEW startup requirement:
|
||||||
|
the Core server loads dev card defs only for games in env `OPENFUT_DEV_CONTENT_GAMES` (comma-sep);
|
||||||
|
omitting `fifa17` makes `get_collection` silently drop every owned card (empty `/collection`,
|
||||||
|
"no squad") despite a successful seed — MUST become a deployment/preflight assertion. Preceded the
|
||||||
|
same day by a staged two-process parity gate (real Core+host over HTTP, no FIFA) confirming byte-
|
||||||
|
shape parity of `userMassInfo.squad` vs the captured oracle. Next milestone: real-data Core
|
||||||
|
import / profile strategy → production Rust UTAS. Blaze deferred.
|
||||||
|
- **Slice 8 (2026-08-12): REAL-DATA staged retail A/B PASS (import + club + squad).** The real FIFA 17
|
||||||
|
profile was imported into a staged Core via the two-store protocol (`openfut-import-fifa17 --apply`:
|
||||||
|
generic transactional Core import + `openfut-identity` seeding, idempotent), then FIFA itself
|
||||||
|
consumed it end-to-end over the source-scoped utas switch (`.105`→`.120:8199`). Imported population
|
||||||
|
1949 of 1962 player instances (13 Legend/special assets deferred as unnameable — 9 NoName + the 4
|
||||||
|
copies of `169193`), every original Python wire id preserved (set-equal, 0 minted/dropped), each
|
||||||
|
owned instance an opaque Core UUID. On the retail client: real club rendered (1949, not the 33-card
|
||||||
|
dev XI); `/club` pagination clean (paged==full, no loop/overlap/drop); the `Special` quality filter
|
||||||
|
returned only specials (1665, 0 base leaked) and paginated the filtered set; a squad edit persisted
|
||||||
|
to Core (canonical + opaque extension atomically, fingerprint recomputed) and survived a full cold
|
||||||
|
relaunch; rollback→Python→Rust re-enable proven on the exact client path (disarm reached Python,
|
||||||
|
re-arm returned the imported club + edited squad; identity store 0 reallocation). Three real-data
|
||||||
|
fidelity gaps the base-only dev fixture had hidden were found on the live client and fixed:
|
||||||
|
(1) `e187cd4` versioned `resourceId` — `shape_item` emitted the base assetId as `resourceId`,
|
||||||
|
collapsing every special onto its base card art; `Fifa17Identity` now carries the versioned
|
||||||
|
`resource_id`. (2) `626c972` observed `rareflag` — `shape_item` hardcoded `rareflag=1`, so all
|
||||||
|
specials rendered as basic rare; `rareflag` now flows through the FIFA catalog and onto the wire
|
||||||
|
(wire distribution == source exactly). (3) `6f16a23` `rare=SP` Special filter — previously a no-op
|
||||||
|
("semantics UNKNOWN"), now grounded as `rareflag > 1` and applied host-side (Core has no rareflag).
|
||||||
|
Also `44fcf24`: nation/league/team proven to be INSTANCE metadata (not definition identity — the 4
|
||||||
|
copies of `169193` are identical but for club), so the definition-consistency gate now compares
|
||||||
|
identity only (no `--defer-conflict` needed; no majority-vote). PRODUCTION NOT READY: the 13 deferred
|
||||||
|
Legends are unnameable from the `.120` client dump (absent/placeholder in `players.json`; DLC/Legends
|
||||||
|
name tables empty) — honest names require the FIFA 17 Legends locale data from the `.105` client.
|
||||||
|
Next: resolve the 13 names → re-import to 1962/0 → full-fidelity gate → gitlink reconcile →
|
||||||
|
production Rust UTAS.
|
||||||
|
|
||||||
|
## Stubbed / planned
|
||||||
|
|
||||||
|
- **`fifa-blaze`** (Rust) — Milestone 1 capture stub only. Two TLS listeners that log packets; no
|
||||||
|
FIFA 23 component/command handlers. Its own README says IDs are unknown. Superseded in practice by
|
||||||
|
the Python FIFA 17 responders, kept as the intended FIFA 23 implementation surface.
|
||||||
|
- **`openfut-launcher` legacy controls** — core/bridge and FIFA 23 setup controls belong to a
|
||||||
|
superseded plan. The launcher now also owns the live FIFA 17 client flow: server/hook config,
|
||||||
|
account synchronization, local LSX, privileged autopatch, and game launch.
|
||||||
|
- **`tools/`** (file-watch-diff, squad-injector, exporters) — helpers for the FLE-Lua-bridge idea in
|
||||||
|
`docs/direction.md`. Not part of the live FIFA 17 path.
|
||||||
|
- **`docs/foundational-xi-injection-test.md`** — a planned (not executed) test procedure for the FLE
|
||||||
|
bridge route.
|
||||||
|
|
||||||
|
## Stubbed / blocked (FIFA 23 lineage)
|
||||||
|
|
||||||
|
- **`openfut-bridge`** — in-process `version.dll` hook on ProtoSSL. Git history: injection works but
|
||||||
|
the effort hit an "architectural wall" (async event-driven gate, not a poll). Superseded first by
|
||||||
|
the FLE-bridge pivot, then by the FIFA 17 route. Its `CLAUDE.md` task list is historical.
|
||||||
|
|
||||||
|
## Unknown / requires investigation
|
||||||
|
|
||||||
|
- Whether the FIFA 17 hub supports actually **playing a FUT match** offline and getting results back.
|
||||||
|
- Whether FUT actions beyond the now-verified pack reveal/assignment flow (submit SBC, transfer
|
||||||
|
market buy/sell, matches) round-trip correctly through `utas_server.py`.
|
||||||
|
- The exact division of FUT state ownership once Core is wired in (who is source of truth).
|
||||||
|
- Degree of FIFA 23 wire-format identity — asserted ("identical wire format") but the FIFA 23 client
|
||||||
|
has not been re-tested against these responders in this repo's evidence.
|
||||||
|
|
||||||
|
## Known technical debt / hazards
|
||||||
|
|
||||||
|
- **Root docs are stale.** `README.md`, `CLAUDE.md`, `openfut-bridge/CLAUDE.md` all describe FIFA 23
|
||||||
|
as the target and mark FIFA 23 integration as the open item — they predate the FIFA 17 success.
|
||||||
|
- **All host state is volatile** across reboot except the `/etc/hosts` line — re-run
|
||||||
|
`openfut-fut.sh start`. Requires `ptrace_scope=0` + root arming (security-relevant).
|
||||||
|
- **Whole stack rides on EAAC staying neutralized** and game updates being off; a client update can
|
||||||
|
break the memory patches (VAs) and cert bypass.
|
||||||
|
- **`fifa17-recon/tools/lsx_responder_v2.py` is currently modified in the working tree** (uncommitted).
|
||||||
|
- `33068179` / `CAGE` remains the responder fallback, but the launcher now blocks one-button launch
|
||||||
|
until an explicit persona is configured and synchronized across LSX, Blaze, POW, and UTAS.
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# Python Retirement Readiness (post-P1)
|
||||||
|
|
||||||
|
Classification of every Python component still present. Evidence: prod-host
|
||||||
|
`owner=` dispatch log (OBSERVED), AuxServiceMap/PythonContractMap scouts,
|
||||||
|
container `entrypoint.sh`. **P2 rollback stays hot regardless** (do NOT remove
|
||||||
|
rollback-to-python-p2.sh, :p2-rollback image b1b929953f, profile 39bb3e83).
|
||||||
|
|
||||||
|
## A. LIVE PRODUCTION REQUIRED (still owns behavior in the live flow)
|
||||||
|
| Component | Role | Rust status | Retire when |
|
||||||
|
|---|---|---|---|
|
||||||
|
| oracle utas_server.py (:8199) NON-ECONOMY | **remaining**: user/club rename, non-active squad/<n>, draft/*, mode-gated leaderboards + /sbs/* | **Most non-economy migrated 2026-08-17** (account/sync, auth, userMassInfo, user, clientdata, hub, club/stats/*, settings, accountinfo, phishing, match/reset, leaderboards/options, watchList, static acks, item-defs (item/resource,defid), marketdata (+/pricelimits), and the flag-off empty reads season/tournament/champion/clubUser/user/list → `{}` — all Rust). See PRODUCTION_AUTHORITY_MATRIX | remaining tail is mutation (user/club), no-Core-model (squad/<n>), or unimplemented modes (draft/leaderboards/sbs) |
|
||||||
|
| blaze_responder_v3b.py (:42130 Blaze) | FUT Blaze transport | Rust openfut-blaze-host COMPLETE, gate-proven (Gate 10) | container cutover (operator-gated deploy) |
|
||||||
|
| blaze_responder_v3b.py (:42127 redirector TLS) | first-hop TLS redirect | Rust openfut-redirector-host COMPLETE (OpenSSL) | container cutover + cert consistency |
|
||||||
|
| roster_server.py (:8081) | roster-update XML | Rust openfut-roster-host COMPLETE (unit-only) | container cutover |
|
||||||
|
| pow_server.py (:8094/:8080) | Proof-of-Work / content | NO Rust crate; 58 templates, bodies un-reversed | needs Rust host + body RE (BLOCKED) |
|
||||||
|
|
||||||
|
## B. ORACLE ONLY (reference / differential, not production authority)
|
||||||
|
- utas_server.py economy handlers: now oracle-only (economy is Rust; Python economy hits = 0). Used by
|
||||||
|
`economy_differential.rs` as the parity oracle. KEEP.
|
||||||
|
- fut_store.py / fut_accounts.py / fut_consumables.py: reference models + Ghidra-derived taxonomy source. KEEP.
|
||||||
|
|
||||||
|
## C. MIGRATION TOOL ONLY (offline)
|
||||||
|
- scripts/*.py (seed_fifa17_cards.py, utas-observe/mutate/diff, check-*). KEEP (dev tooling).
|
||||||
|
- fifa17-recon/tools/db_dump.py etc.: table extraction. KEEP.
|
||||||
|
|
||||||
|
## D. ROLLBACK ONLY (hot, DO NOT REMOVE)
|
||||||
|
- rollback-to-python-p2.sh; image openfut-fut-backend:p2-rollback (b1b929953f) + :dev; profile 39bb3e83;
|
||||||
|
compose/env backup; tuple OPENFUT_SERVERS="blaze roster utas pow".
|
||||||
|
|
||||||
|
## E. DEAD (advertised but not followed in current FIFA17 flow)
|
||||||
|
- Nucleus /connect/token stub (:42131): 0 live hits across Gate 5-10 (client never POSTs). Keep the ADVERTISED
|
||||||
|
URL so the client can't reach real EA, but no migration needed. No Rust listener required.
|
||||||
|
- fifa-blaze (FIFA23 capture stub), openfut-bridge (FIFA23): retired lineage, not in FIFA17 flow.
|
||||||
|
|
||||||
|
## Retirement gating
|
||||||
|
1. **Mostly DONE (2026-08-17)**: account/sync, ut/auth (Rust SID mint), userMassInfo (full), clientdata,
|
||||||
|
club/stats/{country,league,team}, watchList, static acks, the flag-off empty reads
|
||||||
|
(season/tournament/champion/clubUser/user/list → `{}`), item-defs (item/resource,defid), and
|
||||||
|
marketdata (+/pricelimits, container-type-exact) migrated + deployed. **Remaining on Python**:
|
||||||
|
user/club rename (mutating; needs a Core write), non-active squad/<n> (no multi-squad Core model),
|
||||||
|
and the unimplemented modes draft/* + leaderboards + /sbs/* (need the mode logic ported, not a proxy).
|
||||||
|
2. Deploy Rust blaze/roster/redirector via container cutover (operator-gated) — then those Python responders
|
||||||
|
are class D/E only.
|
||||||
|
3. POW: build Rust host + reverse bodies (largest blocker) OR keep Python POW as class A indefinitely.
|
||||||
|
4. Only after a long stable window: consider retiring the P2 rollback (separate explicit decision — NOT now).
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
# SOLD-row A/B experiment — isolated FIFA-17 staging runbook
|
||||||
|
|
||||||
|
Operator procedure for driving the seller-facing **SOLD row** experiment with a real
|
||||||
|
FIFA 17 client against a **completely separate** staging stack, while the frozen P1
|
||||||
|
production stack keeps running untouched.
|
||||||
|
|
||||||
|
The experiment asks one question the PE cannot answer: for a `closed` trade-pile row,
|
||||||
|
does the FUT ActionScript front end treat `bidState:"highest"` or `bidState:"buyNow"`
|
||||||
|
as *"you sold this"*? Both are bit-identical to every native CardsDLL consumer, but
|
||||||
|
`bidState` is published to the movie verbatim as `YOURBID`, so only the client can
|
||||||
|
say. See `openfut-utas-host/src/sold_experiment.rs`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What the two scripts do
|
||||||
|
|
||||||
|
| Script | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `scripts/sold-staging-up.py` | Brings up the **whole** staging stack (Core + utas-host + Blaze), seeds two identities, prints the client config block. One entry point. |
|
||||||
|
| `scripts/sold-staging-down.py` | Stops **exactly** the processes the up script recorded, proves the staging ports are free, and proves production is still alive. |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/alex/OpenFUT
|
||||||
|
|
||||||
|
# variant A — the sold row is closed / highest
|
||||||
|
python3 scripts/sold-staging-up.py --variant highest
|
||||||
|
|
||||||
|
# variant B — same state, only bidState (and optionally coinsProcessed) differ
|
||||||
|
python3 scripts/sold-staging-up.py --variant buyNow --coins-processed 1 \
|
||||||
|
--count-mode active_plus_sold
|
||||||
|
|
||||||
|
# projection disabled: identical to production behaviour (control run)
|
||||||
|
python3 scripts/sold-staging-up.py --variant off
|
||||||
|
|
||||||
|
python3 scripts/sold-staging-down.py # stop, keep dbs/logs as evidence
|
||||||
|
python3 scripts/sold-staging-down.py --purge # stop and delete the staging dir
|
||||||
|
```
|
||||||
|
|
||||||
|
To flip A → B, run `sold-staging-down.py` then `sold-staging-up.py --variant buyNow`.
|
||||||
|
The staging databases are throwaway: a fresh `up` deletes the previous ones and
|
||||||
|
reseeds, so each variant run starts from a known state. Never edit a live stack's
|
||||||
|
environment in place — the banner in `logs/utas-host.log` is the only record of which
|
||||||
|
variant produced a capture, and it is written at startup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Staging port block and state
|
||||||
|
|
||||||
|
Everything lives under `/home/alex/openfut-sold-staging/` (override with `--dir` or
|
||||||
|
`OPENFUT_SOLD_STAGING_DIR`).
|
||||||
|
|
||||||
|
| Service | Bind | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| staging Core | `127.0.0.1:18081` | loopback only; the client never talks to Core |
|
||||||
|
| staging utas-host | `0.0.0.0:8299` | the UTAS the client reaches |
|
||||||
|
| staging Blaze redirector | `0.0.0.0:42327` | TLS; receives EA `:10041` and `:42230` |
|
||||||
|
| staging Blaze main | `0.0.0.0:42330` | receives EA `:42127` |
|
||||||
|
| staging Blaze nucleus | `0.0.0.0:42331` | local OAuth stub |
|
||||||
|
| dead Python upstream | `127.0.0.1:8399` | **must stay unbound** (see §5) |
|
||||||
|
|
||||||
|
The redirector is on `42327`, not the "obvious" `42227`: that port is permanently
|
||||||
|
held by `openfut-redirector-host` (pid 862419).
|
||||||
|
|
||||||
|
| State file | Path |
|
||||||
|
| --- | --- |
|
||||||
|
| Core sqlite | `staging-core.db` |
|
||||||
|
| market sqlite | `staging-market.db` |
|
||||||
|
| pile sqlite | `staging-pile.db` |
|
||||||
|
| identity store | `staging-identity.json` |
|
||||||
|
| clientdata blobs | `staging-clientdata.json` |
|
||||||
|
| content pack | `content/fifa17-production-cards.json` |
|
||||||
|
| identity catalog | `content/fifa17-production-catalog.json` |
|
||||||
|
| patched Blaze | `blaze/blaze_responder_staging.py` |
|
||||||
|
| process manifest | `manifest.json` |
|
||||||
|
| logs | `logs/{core,utas-host,blaze,blaze-responder}.log` |
|
||||||
|
|
||||||
|
Seeded identities (direct SQL against the schema Core migrates for itself):
|
||||||
|
|
||||||
|
* **Seller A** — profile `prof-seller-a-cage`, club `club-seller-a-cage`, username
|
||||||
|
`CAGE`, `game_id = fifa17`, 1 000 coins, 11 starters in a canonical squad, plus
|
||||||
|
**one disposable item** (`owned-a-disposable`, card `fifa17_232273`, Nelson Atiagli
|
||||||
|
LB 51) — that is the card to list and sell during the experiment.
|
||||||
|
Seller A carries `game_id = fifa17`, so the retail client (which sends
|
||||||
|
`X-OpenFUT-Game: fifa17`) resolves to this profile and can log in as persona
|
||||||
|
`33068179`.
|
||||||
|
* **Buyer B** — profile `prof-buyer-b`, club `club-buyer-b`, 20 000 coins, parked on
|
||||||
|
its own `game_id = fifa17-buyer-b`. Core is single-profile-per-game, so this keeps
|
||||||
|
Buyer B from ever shadowing Seller A as the active `fifa17` profile while still
|
||||||
|
being reachable by club id for a settlement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. The client change — the only thing the operator touches
|
||||||
|
|
||||||
|
The FIFA client learns its **UTAS** base URL from Blaze, not from `openfut.cfg`:
|
||||||
|
`blaze_responder_v3b.py` builds `UTAS_BASE = "http://%s:8099/" % _ADVERTISE` with the
|
||||||
|
port **hardcoded**. The up script therefore copies the responder into the staging dir
|
||||||
|
and rewrites that literal to `:8299`. **Pointing the client at staging Blaze is
|
||||||
|
sufficient to move UTAS too** — there is no UTAS line in `openfut.cfg`.
|
||||||
|
|
||||||
|
On the FIFA client machine **10.10.0.105**, file
|
||||||
|
`"/mnt/games/FIFA 17/openfut.cfg"`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# back it up FIRST
|
||||||
|
cd "/mnt/games/FIFA 17"
|
||||||
|
cp openfut.cfg openfut.cfg.prod
|
||||||
|
```
|
||||||
|
|
||||||
|
Set these **three** lines:
|
||||||
|
|
||||||
|
```
|
||||||
|
host=10.10.0.120
|
||||||
|
blaze_redirector_port=42327
|
||||||
|
blaze_main_port=42330
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave `https_port=8443` **unchanged** — that is Bridge, which holds no economy
|
||||||
|
state. The resulting file is:
|
||||||
|
|
||||||
|
```
|
||||||
|
host=10.10.0.120
|
||||||
|
https_port=8443
|
||||||
|
blaze_redirector_port=42327
|
||||||
|
blaze_main_port=42330
|
||||||
|
```
|
||||||
|
|
||||||
|
Then **relaunch the FIFA 17 client.** A client that is already running caches its
|
||||||
|
UTAS session (SID) in memory and will not re-auth against a different stack; the
|
||||||
|
symptom is the dialog *"An error occurred downloading the FUT Squad Update"* with
|
||||||
|
**zero** requests in the staging host log. "The operator is at the main menu" is not
|
||||||
|
the same as "the client disconnected".
|
||||||
|
|
||||||
|
### Revert to production
|
||||||
|
|
||||||
|
```
|
||||||
|
host=10.10.0.120
|
||||||
|
https_port=8443
|
||||||
|
blaze_redirector_port=42127
|
||||||
|
blaze_main_port=42130
|
||||||
|
```
|
||||||
|
|
||||||
|
or simply `cp openfut.cfg.prod openfut.cfg` — then **relaunch the client again**.
|
||||||
|
These are the values the file holds today; the staging scripts never write to
|
||||||
|
10.10.0.105, so this revert is the *only* client-side change to undo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Running the experiment
|
||||||
|
|
||||||
|
1. `python3 scripts/sold-staging-up.py --variant highest`
|
||||||
|
2. Apply §3 to the client and relaunch it. Confirm it reaches the FUT hub;
|
||||||
|
`logs/utas-host.log` should show `route=auth status=200 sid_opened=true`.
|
||||||
|
3. List the disposable item on the transfer market from the client.
|
||||||
|
4. Complete the sale (Buyer B side) through the real settlement path.
|
||||||
|
5. Observe the seller's Transfer List: which bucket the row lands in, what the
|
||||||
|
counter says, and which request the client issues to clear it. Capture both the
|
||||||
|
screen and `logs/utas-host.log`.
|
||||||
|
6. `python3 scripts/sold-staging-down.py`
|
||||||
|
7. `python3 scripts/sold-staging-up.py --variant buyNow`, relaunch the client, and
|
||||||
|
repeat steps 3–5. The two runs differ **only** in `bidState` (and
|
||||||
|
`coinsProcessed`/`count` if those flags are used), which is what makes the client's
|
||||||
|
reaction attributable.
|
||||||
|
8. When done: `sold-staging-down.py` and revert the client per §3.
|
||||||
|
|
||||||
|
`DELETE /ut/delete/game/fifa17/trade/sold` clears the sold rows (logged as
|
||||||
|
`route=market-clear-sold cleared=N`) — that is the verb the client is expected to
|
||||||
|
issue from the *Clear Sold* affordance, and seeing whether it does is part of the
|
||||||
|
observation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Isolation: what guarantees production is untouched
|
||||||
|
|
||||||
|
Production frozen P1 must keep running throughout. The scripts enforce, not assume:
|
||||||
|
|
||||||
|
* **Hard port deny-list.** `8099 8199 18080 8443 42127 42130 42131 4216 8080 8081
|
||||||
|
8094` are refused before every bind and before every HTTP request the scripts make.
|
||||||
|
Each staging port is also proven free first, and bring-up aborts before launching
|
||||||
|
anything if one is not.
|
||||||
|
* **Hard path deny-list.** Every filesystem path goes through `safe_path()`, which
|
||||||
|
refuses `/home/alex/openfut-promotion/state/` (the live `prod-core.db`,
|
||||||
|
`prod-market.db`, `prod-pile.db`, `prod-identity.json`). Nothing there is ever
|
||||||
|
opened — not even read. The FIFA17 content the staging stack loads is copied from
|
||||||
|
`/home/alex/openfut-post-p1/staging/emit/content/`, a build tree outside that
|
||||||
|
directory.
|
||||||
|
* **The Python fallback fails closed.** `OPENFUT_UTAS_PYTHON_URL` points at
|
||||||
|
`http://127.0.0.1:8399`, where **nothing listens**, so a fallback to the Python
|
||||||
|
oracle surfaces as a visible connection error instead of silently serving the
|
||||||
|
production oracle on `8199`.
|
||||||
|
* **Binaries are copied into the staging dir and run from there.** A later
|
||||||
|
`cargo build` cannot change what staging is running, and — more importantly — every
|
||||||
|
staging process's `/proc/<pid>/cmdline` provably contains the staging directory,
|
||||||
|
which production's never can.
|
||||||
|
* **Teardown kills by recorded pid only, never by pattern.** `openfut-utas-host` and
|
||||||
|
`openfut-core` each name *two* live processes on this machine. `sold-staging-down.py`
|
||||||
|
reads pids from `manifest.json`, re-reads `/proc/<pid>/cmdline` and **refuses** to
|
||||||
|
signal anything whose cmdline does not contain the staging dir; it refuses the known
|
||||||
|
production pids explicitly; and it signals only the process group the up script
|
||||||
|
created (`pgid == pid`, via a new session). There is no `pkill`/`pgrep` anywhere.
|
||||||
|
* **The Blaze patch cannot silently no-op.** Six substitutions are applied to the
|
||||||
|
copy, each anchored to a whole assignment line and each required to match **exactly
|
||||||
|
once**; the file is then re-read from disk and every value re-checked, including an
|
||||||
|
explicit assertion that `UTAS_BASE` no longer contains `:8099`. A responder that
|
||||||
|
changed shape aborts bring-up rather than running half-patched and pointing at
|
||||||
|
production. The four protocol values are `REDIR_PORT`, `BLAZE_PORT`,
|
||||||
|
`NUCLEUS_PORT`, `UTAS_BASE`; the two extra ones are the responder's hardcoded
|
||||||
|
`LOG = "/tmp/blaze_responder.log"` and `RXDIR = "/tmp/blaze_rx"`, redirected into
|
||||||
|
the staging dir so a capture can never be ambiguous about which stack wrote it.
|
||||||
|
* **Production liveness is asserted** at preflight, at the end of bring-up, and after
|
||||||
|
teardown (`prod utas-host` pid 3631953, `prod Core` pid 3374264).
|
||||||
|
* **Nothing on 10.10.0.105 is written.** The scripts only *print* the config block;
|
||||||
|
the operator edits it by hand.
|
||||||
|
|
||||||
|
### Production services deliberately shared, read-only
|
||||||
|
|
||||||
|
Staging Blaze advertises the **production** auxiliary endpoints, exactly the strings
|
||||||
|
production Blaze advertises:
|
||||||
|
|
||||||
|
| Advertised to client | Value | Why sharing is safe |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| roster XML / roster URL | `10.10.0.120:8081` | static roster update XML; **no economy state**. Hardcoded as `ROSTER_HOST = "%s:8081" % _ADVERTISE` in the responder. |
|
||||||
|
| `FIFA_POW_CONTENT_SERVER_URL` | `10.10.0.120:8085` | static POW content; no economy state (`POW_CONTENT_HOST`) |
|
||||||
|
| `FIFA_POW_URL` / nucleus proxy | `10.10.0.120:8094` | POW API, only emitted when `FUT_POW=1`, which staging leaves unset (`POW_HOST`) |
|
||||||
|
| LSX layer-1 responder | `:4216` | persona/session handshake for the same persona `33068179`; no economy state |
|
||||||
|
|
||||||
|
The staging stack **never connects** to any of these itself — it only hands the client
|
||||||
|
the same strings production hands it. Every service that owns economy state (UTAS,
|
||||||
|
Core, market/pile/identity) is duplicated on staging ports against staging files.
|
||||||
|
Because those aux services are shared, **do not run a staging session and a
|
||||||
|
production session simultaneously**: one client at a time.
|
||||||
|
|
||||||
|
`heat2` (TDF codec) and `fut_account` (the single source of truth for persona
|
||||||
|
`33068179` / `CAGE`, shared with LSX and the oracle) are imported read-only from
|
||||||
|
`fifa17-recon/tools/` via `PYTHONPATH` rather than copied, so the staging Blaze
|
||||||
|
asserts a **byte-identical** identity to what LSX already asserts. Cross-layer
|
||||||
|
identity consistency is the constraint; a divergent copy would break login.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Verifying a live stack by hand
|
||||||
|
|
||||||
|
All read-only, and none of it touches a production port:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
S=/home/alex/openfut-sold-staging
|
||||||
|
|
||||||
|
# which variant is live
|
||||||
|
grep sold-experiment $S/logs/utas-host.log
|
||||||
|
|
||||||
|
# staging UTAS answers, from staging state
|
||||||
|
python3 - <<'PY'
|
||||||
|
import http.client, json
|
||||||
|
c = http.client.HTTPConnection("127.0.0.1", 8299, timeout=5)
|
||||||
|
c.request("GET", "/ut/game/fifa17/tradePile/counts",
|
||||||
|
headers={"X-OpenFUT-Game": "fifa17"})
|
||||||
|
print(c.getresponse().read().decode())
|
||||||
|
PY
|
||||||
|
|
||||||
|
# the patched Blaze points at staging UTAS, not production
|
||||||
|
grep -nE '^(UTAS_BASE|REDIR_PORT|BLAZE_PORT|NUCLEUS_PORT) = ' \
|
||||||
|
$S/blaze/blaze_responder_staging.py
|
||||||
|
|
||||||
|
# exactly six lines differ from the original responder
|
||||||
|
diff fifa17-recon/tools/blaze_responder_v3b.py \
|
||||||
|
$S/blaze/blaze_responder_staging.py
|
||||||
|
|
||||||
|
# no staging process has a handle on production state
|
||||||
|
for p in $(python3 -c "import json;print(' '.join(str(x['pid']) for x in json.load(open('$S/manifest.json'))['processes']))"); do
|
||||||
|
echo "pid $p: $(sudo ls -l /proc/$p/fd | grep -c openfut-promotion/state) prod-state handles"
|
||||||
|
done
|
||||||
|
|
||||||
|
# production is still up
|
||||||
|
ps -o pid=,cmd= -p 3631953
|
||||||
|
```
|
||||||
|
|
||||||
|
Never `curl` port 8099/8199/18080 to "compare with production" — those ports are on
|
||||||
|
the deny-list. Independence is proven structurally (separate pids, separate ports,
|
||||||
|
separate files, zero shared handles), not by poking the live stack.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Troubleshooting
|
||||||
|
|
||||||
|
| Symptom | Cause / fix |
|
||||||
|
| --- | --- |
|
||||||
|
| `REFUSING to start -- these staging ports are not free` | something else holds a staging port. Nothing was launched. Free it, or edit the port block at the top of `sold-staging-up.py`. |
|
||||||
|
| `a previous staging stack is STILL UP` | run `sold-staging-down.py` first. The up script never steps on a live stack. |
|
||||||
|
| `blaze patch <NAME> applied 0 times` | `blaze_responder_v3b.py` changed shape. Fix the regex in `blaze_patches()`; do **not** disable the assertion. |
|
||||||
|
| `seed card ids are not resolvable` | the emitted content in `CONTENT_SRC` no longer contains a seeded `card_id`. Re-point `CONTENT_SRC` or update the seed ids — both memberships (content pack **and** identity catalog) are required. |
|
||||||
|
| client shows *"error occurred downloading the FUT Squad Update"*, staging host log shows **no request at all** | the failure is upstream of UTAS. Almost always a client that was not relaunched after the config change (§3). Check the *absence* of requests in `logs/utas-host.log` before touching any code. |
|
||||||
|
| `<name> pid N is alive but its cmdline does NOT contain <staging dir>` | pid reuse or a stale manifest. Teardown refuses rather than guessing — identify the process by hand and stop it by pid. |
|
||||||
|
| staging host log shows connection errors to `127.0.0.1:8399` | expected and intended: a UTAS route fell through to the Python fallback, which is deliberately dead. Fix the route; do **not** point the fallback at `8199`. |
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
# FIFA 17 — Empty "My Packs" Client Contract (store/purchasegroup)
|
||||||
|
|
||||||
|
> **STATUS (2026-08-13): ROOT CAUSE ESTABLISHED; P2 backend compatibility workaround
|
||||||
|
> IMPLEMENTED.** Root cause: FIFA 17's Store/Scaleform path resolves the `mypacks`
|
||||||
|
> category even with zero unopened packs, and CardsDLL `FUN_1800147f0` assumes the
|
||||||
|
> resolved group is non-null (crash if absent). Backend decision: **P2** — emit an
|
||||||
|
> **active** non-openable synthetic `mypacks` placeholder (id 65534) when
|
||||||
|
> `unopenedPackIds == []` (`fifa17-recon/tools/utas_server.py` `store_catalog`; tests
|
||||||
|
> `fifa17-recon/tools/test_empty_mypacks.py`). Crash-safe + economy-safe; known UX
|
||||||
|
> limitations (fake tile, click-dialog, Browse→My-Packs nav quirk) are Scaleform-driven
|
||||||
|
> and require the client-side fix in `docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md`.
|
||||||
|
> This is a FIFA-17-specific compatibility shim, NOT an EA-authentic representation,
|
||||||
|
> and is confined to the FIFA-17 adapter/backend (NOT OpenFUT Core).
|
||||||
|
|
||||||
|
Evidence labels: **OBSERVED** (runtime capture / crash dump / already-decompiled RE
|
||||||
|
quoted in-repo), **INFERRED**, **HYPOTHESIS**, **UNKNOWN**. No server behavior is
|
||||||
|
changed by this document; it is analysis only.
|
||||||
|
|
||||||
|
Binaries (hashes verified 2026-08-13 on `.105`):
|
||||||
|
`CardsDLL_Win64_retail.dll` SHA-256 `4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c`
|
||||||
|
(load base in the crash dump `0x00006FFFFC120000`; RE-space base `0x180000000`).
|
||||||
|
`FIFA17.exe` SHA-256 `29c31cef12b0c3c2a7305220617c7b4fa139ab76b8c857851bdbe88987962899` (packed).
|
||||||
|
|
||||||
|
## 1. Question
|
||||||
|
|
||||||
|
How must the server represent an account that owns **zero unopened packs** in
|
||||||
|
`GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true` so that FIFA 17 neither
|
||||||
|
(a) crashes nor (b) shows "The pack you've selected is currently not available",
|
||||||
|
**without granting the user a real/openable pack** and without breaking the normal
|
||||||
|
bronze/gold/special store? The current OpenFUT answer (a synthetic inactive `mypacks`
|
||||||
|
sentinel) only downgrades a crash to a dialog; it is not the correct contract.
|
||||||
|
|
||||||
|
## 2. Established experimental behavior (OBSERVED)
|
||||||
|
|
||||||
|
Profile-state ladder, all with normal packs 1/5/6/7 unchanged:
|
||||||
|
|
||||||
|
| profile `unopenedPackIds` | `mypacks` group in response | client outcome |
|
||||||
|
|---|---|---|
|
||||||
|
| `[]` (baseline) | one **inactive** sentinel pack `65534`, empty description | "pack not available" dialog → FUT Hub |
|
||||||
|
| `[70]` (Exp A) | one **active** real owned pack `70` | **store works**; My Packs visible |
|
||||||
|
| `[]` + sentinel suppressed (Exp B) | **no `mypacks` group at all** | **client CRASH** |
|
||||||
|
|
||||||
|
Captures: `store_purchasegroup_capture_2026-08-12.json` (baseline),
|
||||||
|
`…_mypacks70_2026-08-12.json` (A), `…_empty_no_sentinel_2026-08-12.json` (B).
|
||||||
|
Details in `STORE_TILE_6C.md` §14-§15.
|
||||||
|
|
||||||
|
## 3. Existing RE evidence (from `docs/plan-2026-08-05-store-subsystem.md`, decompiled)
|
||||||
|
|
||||||
|
All addresses RE-space (CardsDLL base `0x180000000`):
|
||||||
|
- **`FUN_18013af30`** — per-element pack deser; each `purchase[]` entry → a `0x158`
|
||||||
|
wire record. `displayGroup.value`(atom 0x377) → record `+0x00` (ctor default the
|
||||||
|
literal `"unknown"`). `displayGroup.priority`(0x250) → `+0x34`.
|
||||||
|
(store-subsystem §"wire record", :996.)
|
||||||
|
- **`FUN_1800150d0`** — group builder. Walks `purchase[]` in array order; for each
|
||||||
|
pack, finds-or-creates a `0x108` display group by **exact strcmp of
|
||||||
|
`displayGroup.value` against `group+0x70`** (`FUN_180014380`). New group
|
||||||
|
(`FUN_180012950`): `+0x00` = 1-based ordinal (groups-so-far+1), `+0x100` =
|
||||||
|
priority, **`+0x104` = (value == "mypacks")**, `+0x40` = vector of `0x1a8` tile
|
||||||
|
models. Pack→tile via `FUN_18002c3c0`. (:152-161, :1042-1049.)
|
||||||
|
- **`FUN_1800147f0`** — group **resolver/renderer**, called as
|
||||||
|
`FUN_1800147f0(model, screen+0x290, dataProvider, 0, 0)` from `FUN_18007dab0`.
|
||||||
|
`screen+0x290 == 0` → "list the group tiles" (`FUN_180014610`); **any other value
|
||||||
|
→ `FUN_180014420`, which exact-matches `group+0x00` (the ordinal) and returns NULL
|
||||||
|
on a miss, after which `FUN_1800147f0` dereferences `[RAX+0x40]` with NO guard**
|
||||||
|
("checked in raw disassembly … a real absence"). **"The only legal category values
|
||||||
|
are 0 and the ordinals 1..N."** (:163-169, :1051-1054.)
|
||||||
|
- **`FUN_180014580`** — the six-tab bar: switch 0..5 over the hardcoded lowercase
|
||||||
|
literals `mypacks, points, bronze, silver, gold, special`. `FUN_18007e5e0` gives
|
||||||
|
each panel a `PANEL_ID` = the matching group's ordinal, **or HIDES the panel** if no
|
||||||
|
matching group; `FUN_18007df60` publishes `MYPACK_/…/SPECIAL_CATEGORY_ID`.
|
||||||
|
(:202-206, :1058-1062.)
|
||||||
|
|
||||||
|
## 4. Store parser code path (OBSERVED, decompiled)
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP 200 {"purchase":[...]} (server: store_catalog / _pack_body)
|
||||||
|
→ per-element deser FUN_18013af30 → 0x158 wire records
|
||||||
|
→ FUN_1800150d0 → 0x108 display groups (by displayGroup.value),
|
||||||
|
tiles (0x1a8, FUN_18002c3c0) into group+0x40
|
||||||
|
→ render FUN_18007dab0 → FUN_1800147f0(model, screen+0x290, …)
|
||||||
|
screen+0x290 == 0 → FUN_180014610 list this group's tiles
|
||||||
|
screen+0x290 == N>0 → FUN_180014420 exact-match ordinal; NULL on miss
|
||||||
|
→ [RAX+0x40] dereference (NO NULL GUARD) ← crash site
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. My Packs group construction (OBSERVED)
|
||||||
|
|
||||||
|
A `mypacks` group exists **iff at least one `purchase[]` entry carries
|
||||||
|
`displayGroup.value == "mypacks"`** (the group is derived from packs; there is no
|
||||||
|
independent group object on the wire). Its ordinal is its 1-based creation position
|
||||||
|
in array order; `group+0x104` is set because the value is `"mypacks"`; its tiles live
|
||||||
|
in `group+0x40`. Consequently the server **cannot** emit an "empty `mypacks` group"
|
||||||
|
via `purchase[]` — removing the pack removes the group entirely.
|
||||||
|
|
||||||
|
## 6. Default / selected group logic (partly UNKNOWN)
|
||||||
|
|
||||||
|
- `FUN_1800147f0` resolves whatever category ordinal it is handed via `screen+0x290`;
|
||||||
|
legal values are `0` (list tiles) and `1..N` (existing ordinals). A value that is
|
||||||
|
not an existing ordinal (e.g. `-1` for a hidden/absent panel) → `FUN_180014420`
|
||||||
|
NULL → crash. (OBSERVED via §8 crash + RE.)
|
||||||
|
- **Whether the store defaults to / auto-resolves the `mypacks` category on open, and
|
||||||
|
why it does so even when the unopened count is 0, is UNKNOWN** — the store-screen
|
||||||
|
controller and default-tab selection are Scaleform/packed-FIFA17.exe
|
||||||
|
("Which tile the movie thinks you clicked | CLIENT | Scaleform, unread",
|
||||||
|
store-subsystem :695). Experiment B proves that in this configuration the client
|
||||||
|
DID resolve a `mypacks` ordinal that did not exist (it crashed), so the store is
|
||||||
|
reaching the My Packs category with zero owned packs. Root of "why" = UNKNOWN.
|
||||||
|
|
||||||
|
## 7. Pack availability predicate (UNKNOWN)
|
||||||
|
|
||||||
|
The predicate that turns the baseline inactive sentinel into "pack not available"
|
||||||
|
(while active pack 70 passes) is **in the packed FIFA17.exe and unread**
|
||||||
|
(`plan-2026-08-05-pack-opening.md:553`: `state/saleType/quantity/purchaseLimit/
|
||||||
|
purchaseCount/start/end` are parsed and copied to the tile, "the predicate that greys
|
||||||
|
a tile is in the packed exe and unread"). Candidate deciding fields, from the
|
||||||
|
baseline↔A diff (INFERRED, unproven): **`state` (`inactive`→`active`)** and/or
|
||||||
|
**`unopened` (`false`→`true`)**. The exact field is **UNKNOWN**.
|
||||||
|
|
||||||
|
## 8. Experiment B crash analysis (OBSERVED)
|
||||||
|
|
||||||
|
Minidump `CrashDump_…18.21.08…dmp` (the only crash in the hour; minute `:21` matches
|
||||||
|
the `00:21:07` store request; SHA-256 `fbddda18…`), parsed:
|
||||||
|
- Exception: **`0xC0000005` ACCESS_VIOLATION**, access type **READ**, **faulting VA
|
||||||
|
`0x0000000000000048`**.
|
||||||
|
- Faulting instruction: **`CardsDLL_Win64_retail.dll + 0x14882`** → RE-space
|
||||||
|
**`0x180014882`** = **`0x92` bytes into `FUN_1800147f0`** (entry `0x1800147f0`).
|
||||||
|
- Interpretation (OBSERVED crash ⋂ decompiled RE): the group pointer returned by
|
||||||
|
`FUN_180014420` was **NULL** (no `mypacks` group present with `unopenedPackIds==[]`
|
||||||
|
and the sentinel suppressed), and `FUN_1800147f0` dereferenced `[NULL+0x48]` → read
|
||||||
|
of address `0x48` → access violation. This is exactly the "no null guard" branch
|
||||||
|
the RE flagged (RE said `[RAX+0x40]`; the actual faulting offset is `+0x48`, same
|
||||||
|
member region — the group struct's `+0x40` vector accessed via a `+0x48` field).
|
||||||
|
- Note: the pre-built Ghidra project and `/tmp/fut/cardsdll.dll` were absent on `.105`
|
||||||
|
(tmp cleared); confirmation used the OBSERVED crash dump + the previously-decompiled
|
||||||
|
RE rather than a fresh (expensive) re-analysis. CardsDLL is unpacked, so this code
|
||||||
|
is statically readable if a fresh project is ever needed.
|
||||||
|
|
||||||
|
## 9. Empty My Packs client contract (the answer, as far as evidence allows)
|
||||||
|
|
||||||
|
- **The client has NO guard for a missing selected group.** If the store resolves the
|
||||||
|
`mypacks` category and no `mypacks` group exists, it null-derefs and crashes.
|
||||||
|
(OBSERVED.)
|
||||||
|
- **A `mypacks` group can only exist if a `purchase[]` entry carries
|
||||||
|
`displayGroup.value=="mypacks"`.** (OBSERVED.) There is no wire representation of an
|
||||||
|
"empty group."
|
||||||
|
- **If the `mypacks` group's selected pack is not a valid/active pack, the client
|
||||||
|
shows "pack not available".** (OBSERVED baseline vs A; deciding field UNKNOWN, §7.)
|
||||||
|
- Therefore, under the *current* client behavior, a zero-unopened-packs account is
|
||||||
|
only cleanly handled when the `mypacks` group contains a **valid active pack**
|
||||||
|
(Exp A). Whether an active-but-non-openable placeholder would also satisfy the
|
||||||
|
availability predicate is **UNKNOWN** (depends on §7).
|
||||||
|
- **The correct retail-EA representation of zero unopened packs is UNKNOWN.** It is
|
||||||
|
NOT "omit the group" (crash) and NOT "inactive placeholder" (dialog). It is most
|
||||||
|
likely one of: (i) the retail store does not auto-select My Packs when the unopened
|
||||||
|
count is 0 (a client/Scaleform decision, possibly gated by a count the server sets),
|
||||||
|
or (ii) retail sends a `mypacks` entry the client treats as an empty-but-valid state
|
||||||
|
via a field we have not identified. Neither is established.
|
||||||
|
|
||||||
|
## 10. Candidate server representations
|
||||||
|
|
||||||
|
| # | Candidate | Client evidence | Expected behavior | Confidence | Safe to test? |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| A | No `mypacks` pack, no `mypacks` group | Exp B crash (`0x180014882`, `[NULL+0x48]`) | **CRASH** | OBSERVED | Already tested — reject |
|
||||||
|
| B | `mypacks` group present but zero packs | Not representable via `purchase[]` (groups derive from packs, `FUN_1800150d0`) | UNKNOWN | INFERRED-not-representable | No server mechanism |
|
||||||
|
| C | Inactive placeholder pack (current sentinel) | Baseline dialog | "pack not available" → Hub | OBSERVED | Already tested — reject |
|
||||||
|
| C′ | **Active** placeholder pack, id absent from `PACK_CATALOG` (so open/buy handlers reject it) | Exp A shows an *active* mypacks pack works; sentinel id 65534 is already rejected by open/buy (not in `PACK_CATALOG`) | Group resolves (no crash); MIGHT pass availability (no dialog) while remaining non-openable → no free pack | HYPOTHESIS | Yes — code change, restart; economy-safe (non-openable) |
|
||||||
|
| D | Hide My Packs when unopened count == 0 | `FUN_18007e5e0` hides a panel with no group, but the store still resolved `mypacks` at count 0 (Exp B crash) | Hiding via absence CRASHES; a client count-gate is Scaleform/unknown | UNKNOWN | Not server-controllable as far as known |
|
||||||
|
| E | Different default category when count == 0 | Default-tab selection is Scaleform/packed | UNKNOWN | UNKNOWN | Not server-controllable as far as known |
|
||||||
|
| F | A count/quantities field (e.g. `ut/v2/store` `FutStorePackQuantities`, or `userInfo.unopenedPacks`) that suppresses the My Packs auto-select | eligibility gate exists (`ENDPOINT_MAP.md:60`); relationship to My Packs default UNKNOWN | UNKNOWN | HYPOTHESIS | Read-only RE first |
|
||||||
|
|
||||||
|
## 11. Recommended next controlled experiment
|
||||||
|
|
||||||
|
**Experiment C′ (economy-safe placeholder).** Keep `unopenedPackIds == []`; change the
|
||||||
|
synthetic sentinel `65534` ONLY in `state` (and, if needed, `unopened`) so the
|
||||||
|
`mypacks` group's single tile is **active** — but leave its id `65534` **absent from
|
||||||
|
`PACK_CATALOG`** so `store_buy`/`open_pack`/`consume_unopened_pack` still reject it
|
||||||
|
(no pack can be opened → **no free pack, no economy change**). Observe whether the
|
||||||
|
store then opens without the "pack not available" dialog (would identify `state` as
|
||||||
|
the availability field and give an economy-safe fix), or still shows the dialog
|
||||||
|
(implicating another field / packed predicate).
|
||||||
|
- Requires a temporary code change to `store_catalog` (sentinel construction) →
|
||||||
|
restart. Same experiment discipline as Experiment B (patch container copy, capture,
|
||||||
|
revert, restart). Economy-safe because the placeholder remains non-openable.
|
||||||
|
- If C′ still fails, escalate to read-only RE of the availability predicate / the
|
||||||
|
count-gated default-tab hypothesis (candidate F) before any further change.
|
||||||
|
|
||||||
|
## 12. Open questions
|
||||||
|
|
||||||
|
1. Exact field that flips the sentinel from "pack not available" to acceptable
|
||||||
|
(`state`? `unopened`? another). UNKNOWN — packed predicate. (Exp C′ targets this.)
|
||||||
|
2. Why does the store resolve/select `mypacks` with zero owned packs? Is there a
|
||||||
|
server-settable count that would stop it? UNKNOWN — Scaleform/packed.
|
||||||
|
3. Does retail FIFA 17 ever present an empty My Packs, and how? No capture on record.
|
||||||
|
4. Is an active-but-non-openable placeholder (C′) accepted by the availability
|
||||||
|
predicate? HYPOTHESIS — untested.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permanent-fix requirements (Phase 11 — REPORT ONLY, not implemented)
|
||||||
|
|
||||||
|
A correct permanent fix MUST satisfy ALL of:
|
||||||
|
- Zero unopened packs must **NOT** grant the user a free pack.
|
||||||
|
- No synthetic **openable** reward may be created (any placeholder must be rejected by
|
||||||
|
`store_buy`/`open_pack`/`consume_unopened_pack`).
|
||||||
|
- Client must **not crash** (a resolvable `mypacks` group must exist, OR the client
|
||||||
|
must be kept from resolving `mypacks` when empty).
|
||||||
|
- Client must **not** show "The pack you've selected is currently not available".
|
||||||
|
- Normal Bronze/Gold/Special store categories must still work unchanged.
|
||||||
|
- When a genuine unopened pack exists, My Packs must continue to work (Exp A).
|
||||||
|
- Profile/economy semantics must remain correct (no coins/nextItemId/inventory drift).
|
||||||
|
|
||||||
|
Nothing implemented. The evidence favours investigating an **economy-safe active
|
||||||
|
placeholder (C′)** and/or the **count-gated My-Packs default (F)**; it explicitly does
|
||||||
|
NOT support "grant pack 70 whenever My Packs is empty".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UPDATE after Experiment C′ (2026-08-13) — active non-openable placeholder tested
|
||||||
|
|
||||||
|
Executed C′: sentinel 65534 `state "inactive"→"active"` only; `unopenedPackIds==[]`;
|
||||||
|
65534 kept out of `PACK_CATALOG`. Full record in `STORE_TILE_6C.md` §16. Capture:
|
||||||
|
`store_purchasegroup_capture_active_placeholder_2026-08-12.json` (C′-vs-baseline JSON
|
||||||
|
diff = only `65534.state`).
|
||||||
|
|
||||||
|
**Resolves §7 (availability predicate), partially:** `state` **DOES participate**
|
||||||
|
(OBSERVED). `state:"active"` removed the "pack not available" dialog while the group's
|
||||||
|
existence still prevented the crash. So the earlier §7 "deciding field UNKNOWN" is
|
||||||
|
updated: **`state` (inactive vs active) is (at least) a deciding field** for the
|
||||||
|
dialog. `unopened` was NOT varied and remains untested. The full predicate may still
|
||||||
|
involve other fields, but `state` alone flips dialog→no-dialog.
|
||||||
|
|
||||||
|
**Updated candidate table verdict:**
|
||||||
|
- **C′ (active placeholder, non-openable): SUPPORTED with UX caveats — best option so
|
||||||
|
far, but NOT adopted.** No crash, no dialog, store usable, and **no automatic
|
||||||
|
transaction/open for 65534** (only a routine boot `TRANSACTIONCANCEL` no-op).
|
||||||
|
Caveats (OBSERVED): (1) the placeholder renders as a **visible empty pack tile**
|
||||||
|
("0 items, 0 bronze, 0 rares", no cover) that a user could try to open (server-safe:
|
||||||
|
opening 65534 → no-op `{}`/stale `last_pack`, no value — §16.1); (2) **navigation
|
||||||
|
gate**: from the Store "Browse Packs" entry the Bronze/Gold/Special categories are
|
||||||
|
not reachable until "My Packs" is opened first (not present with a genuine owned
|
||||||
|
pack, Exp A).
|
||||||
|
- A (real active pack 70): works cleanly but grants a real openable pack → economy
|
||||||
|
risk; rejected as the permanent fix.
|
||||||
|
- Candidate **F (count-gated My-Packs default)** gains weight: C′'s visible-empty-tile
|
||||||
|
and Browse-Packs navigation gate suggest the client is being pushed to resolve/enter
|
||||||
|
My Packs when it should not with zero packs. If a server-settable count (e.g.
|
||||||
|
`userInfo.unopenedPacks` / `ut/v2/store` quantities) suppresses the My-Packs
|
||||||
|
default/tile, that could remove both the crash risk and the empty-tile artifact
|
||||||
|
without any placeholder. UNTESTED.
|
||||||
|
|
||||||
|
**Permanent fix: still NOT established.** Even though C′ is the first
|
||||||
|
crash-free/dialog-free representation, the empty-tile UX + navigation gate + the
|
||||||
|
untested "explicit placeholder selection" behavior bar adoption. Required next steps
|
||||||
|
(design/authorize separately): (a) controlled test of explicitly focusing/opening the
|
||||||
|
active placeholder; (b) investigate candidate F (count-gated My-Packs) to avoid a fake
|
||||||
|
tile entirely. Do NOT adopt `state:"active"` or "grant pack 70" as the fix on current
|
||||||
|
evidence.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Candidate F — Count-Gated My Packs Navigation (READ-ONLY investigation, 2026-08-13)
|
||||||
|
|
||||||
|
Question: can the server make FIFA decide **not** to resolve/default into My Packs
|
||||||
|
when the account owns zero unopened packs (avoiding any placeholder)?
|
||||||
|
|
||||||
|
## 1. Server-sent unopened-pack signals (inventory, OBSERVED code)
|
||||||
|
| field / endpoint | source | value source | when sent | client consumer | conf |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `userInfo.unopenedPacks.recoveredPacks` (via `userMassInfo`) | `utas_server.py:409-414` | `len(unopenedPackIds)` | boot massinfo; only if count>0 **or** `_UI∈{packs,full}` (default `_UI=roster` → omitted at 0) | hub unopened-pack model / My Packs badge (`FUN…vtbl[0x4e0]`, pack-opening RE) | OBSERVED (code) |
|
||||||
|
| `/user/credits` `.unopenedPacks.recoveredPacks` | `utas_server.py:3542-3545` | `len(unopenedPackIds)` | on credits fetch; **only if count>0** | My Packs badge / CentralUnclaimedPack hub tile | OBSERVED (code+capture) |
|
||||||
|
| `/hub` body | `utas_server.py:1449-1453` | — | hub load | — (**no pack count present**) | OBSERVED |
|
||||||
|
| profile `unopenedPackIds` | `fut_store.py` | account state | internal | not wire-visible directly | OBSERVED |
|
||||||
|
`pileSize`/`store quantities` (`ut/v2/store` `FutStorePackQuantities`) exist as an
|
||||||
|
eligibility gate (`ENDPOINT_MAP.md:60`) but were **never requested** in any capture
|
||||||
|
(8h logs); they carry a store-open `result`, not a My-Packs count.
|
||||||
|
|
||||||
|
## 2. Pre-store request sequence (OBSERVED, captures)
|
||||||
|
Boot → `accountinfo → /ut/auth → settings → phishing → match/reset → userMassInfo →
|
||||||
|
PUT store/transaction/0 (TRANSACTIONCANCEL→{}) → /hub → clientdata → /user/credits →
|
||||||
|
GET /store/purchasegroup/all`. The only pack-count-bearing responses **before**
|
||||||
|
`purchasegroup` are `userMassInfo`(userInfo) and `/user/credits`.
|
||||||
|
|
||||||
|
## 3. Baseline([]) vs Experiment A([70]) pre-store diff (OBSERVED, captures)
|
||||||
|
The single profile change `[] → [70]` altered exactly one pre-store wire signal:
|
||||||
|
- `/user/credits`: **`[]` → no `unopenedPacks` member** (C′ capture, all 3 fetches:
|
||||||
|
`{"credits":…,"currencies":[…]}`); **`[70]` → `"unopenedPacks":{"preOrderPacks":0,
|
||||||
|
"recoveredPacks":1}`** (A capture line 25). OBSERVED.
|
||||||
|
- `userInfo.unopenedPacks`: same pattern (present at `[70]`, omitted at `[]` with
|
||||||
|
`_UI=roster`). INFERRED from code; A-capture credits corroborates.
|
||||||
|
- **No other pre-store field changed.**
|
||||||
|
|
||||||
|
Candidate signal:
|
||||||
|
```
|
||||||
|
Candidate: unopenedPacks.recoveredPacks (count)
|
||||||
|
Endpoint: /user/credits and userMassInfo(userInfo)
|
||||||
|
Baseline([]) value: ABSENT (i.e. zero)
|
||||||
|
Experiment A([70]) value: {preOrderPacks:0, recoveredPacks:1}
|
||||||
|
Source: utas_server.py:3542-3545 / :409-414 (= len(unopenedPackIds))
|
||||||
|
Client-visible before purchasegroup?: YES
|
||||||
|
Confidence: OBSERVED that it differs; its CONTROL over My-Packs nav = see §9
|
||||||
|
```
|
||||||
|
**Key point: this count is already CORRECT** — it reports zero (absent) when the
|
||||||
|
account is empty. OpenFUT is **not** misreporting a nonzero pack count.
|
||||||
|
|
||||||
|
## 4. Navigation / client call path (OBSERVED, decompiled RE)
|
||||||
|
`FUN_18007dab0 → FUN_1800147f0(model, screen+0x290, …)`. `screen+0x290==0` lists
|
||||||
|
group tiles; else `FUN_180014420` exact-matches the group ordinal (NULL on miss →
|
||||||
|
`[NULL+0x48]` crash). `screen+0x290` is written in exactly two CardsDLL sites: the
|
||||||
|
screen ctor `FUN_18007d1a0` writes `0`, and **`FUN_18007e7f0` case `0x7551` copies
|
||||||
|
the Flash movie message field `CATEGORY_ID` verbatim** into it
|
||||||
|
(store-subsystem :172-175, :1055-1056). So the resolved category is chosen by the
|
||||||
|
**Scaleform movie**, not by any server response field.
|
||||||
|
|
||||||
|
## 5. `GOTO_STORE_MYPACK` analysis (OBSERVED, RE)
|
||||||
|
`GOTO_STORE_MYPACK` is the **destination of the hub `CentralUnclaimedPack` tile**
|
||||||
|
(tile type 0x1c); "Nothing in the chain issues a request, and no request could
|
||||||
|
exist" (pack-opening :888-890). Whether that HUB tile appears is gated by the
|
||||||
|
unopened-pack count in the hub model (`model+0x20950`) — i.e. the count DOES control
|
||||||
|
the *hub unclaimed-pack tile*, but the operator reached the store via **Browse Packs**
|
||||||
|
/ the store screen, whose category resolution is the movie-driven `CATEGORY_ID` path
|
||||||
|
(§4), not `GOTO_STORE_MYPACK`. `GOTO_STORE_MYPACK` is a UI navigation command,
|
||||||
|
**not** a server-state-gated store-category selector.
|
||||||
|
|
||||||
|
## 6. Candidate count/flag fields — verdict per field
|
||||||
|
- `unopenedPacks.recoveredPacks`: correct at 0 when empty; controls the hub badge /
|
||||||
|
CentralUnclaimedPack tile, **not** the store's category resolver. Not a viable gate
|
||||||
|
for the store My-Packs entry.
|
||||||
|
- No other server field feeds `screen+0x290` (RE §4: only ctor-0 and movie
|
||||||
|
`CATEGORY_ID`).
|
||||||
|
|
||||||
|
## 7. EA-capture evidence
|
||||||
|
No EA-origin `purchasegroup`/`credits` capture for a zero-unopened-packs account
|
||||||
|
exists in the repo (all captures are OpenFUT-generated). EA count semantics for empty
|
||||||
|
My Packs remain **UNKNOWN**.
|
||||||
|
|
||||||
|
## 8. Where My Packs selection occurs (OBSERVED)
|
||||||
|
**Before** `purchasegroup` parsing decides content, the **Scaleform movie** decides
|
||||||
|
which category to resolve and writes it to `screen+0x290` (§4). The server's role is
|
||||||
|
limited to which groups EXIST in `purchase[]`. Therefore the sentinel is compensating
|
||||||
|
for a **movie-side** decision to resolve My Packs; it is not fixing an incorrect
|
||||||
|
server count (the count is already correct).
|
||||||
|
|
||||||
|
## 9. Candidate F verdict — **F3 (CONTRADICTED)** (with an F4 residue)
|
||||||
|
My Packs selection is **not** controlled by server-sent unopened-pack state:
|
||||||
|
- OBSERVED: the server count is correctly zero/absent when empty, yet the store still
|
||||||
|
resolved My Packs (baseline dialog, Exp-B crash). A correct zero signal did not stop
|
||||||
|
it.
|
||||||
|
- OBSERVED (RE): `screen+0x290` (the resolved category) comes from the movie's
|
||||||
|
`CATEGORY_ID`, with no server-field input; default is 0.
|
||||||
|
Residue (F4): the movie's internal logic for *why* it asks for My Packs on store open
|
||||||
|
is in packed Scaleform and is not statically readable — but no server lever into it
|
||||||
|
has been found. **Conclusion: there is no server-controlled count/flag that makes FIFA
|
||||||
|
skip resolving My Packs; the server can only ensure the `mypacks` group exists.** The
|
||||||
|
"clean count-gated fix" is therefore **not achievable server-side**.
|
||||||
|
|
||||||
|
## 10. Proposed next experiment
|
||||||
|
Because F is contradicted, a count experiment is NOT recommended (the count is already
|
||||||
|
correct and does not gate the store). No single-variable server signal will make FIFA
|
||||||
|
enter Browse Packs instead of My Packs. The realistic next step is the previously
|
||||||
|
deferred **explicit active-placeholder selection test**: with the C′ active
|
||||||
|
non-openable placeholder in place (sentinel 65534 at baseline otherwise), have the
|
||||||
|
operator explicitly focus/open the empty My-Packs tile and observe (server-safe per
|
||||||
|
§16.1 — opening 65534 is a no-op — but UX/navigation behavior unknown). That
|
||||||
|
characterizes the best available server-side option (C′) before any adoption.
|
||||||
|
- Would it change profile state? No (`unopenedPackIds=[]`).
|
||||||
|
- Would it change purchasegroup/sentinel behavior? Only `state:"active"` (as C′),
|
||||||
|
reverted after.
|
||||||
|
- Code change? Yes (same one-line C′ patch). Restart? Yes. (Not authorized here.)
|
||||||
|
If explicit selection proves unsafe/ugly, the remaining options are all **client-side
|
||||||
|
/ out-of-scope** (the decision is in the Scaleform movie), or accepting C′ with its
|
||||||
|
documented UX artifacts.
|
||||||
|
|
||||||
|
**Candidate F does NOT provide the hoped-for clean fix. The active non-openable
|
||||||
|
placeholder (C′) remains the best server-side representation; its empty-tile and
|
||||||
|
Browse-Packs navigation artifacts are movie-driven and not server-fixable.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Explicit Active-Placeholder Selection Test — FINAL backend-side result (2026-08-13)
|
||||||
|
|
||||||
|
With the C′ active placeholder in place (`unopenedPackIds=[]`, 65534 active/mypacks/
|
||||||
|
∉PACK_CATALOG), the operator explicitly opened the empty My-Packs tile once. Full
|
||||||
|
record in `STORE_TILE_6C.md` §17.
|
||||||
|
- **Outcome: S1 — pure client-side rejection.** Dialog **"This pack is no longer
|
||||||
|
available"** → back to My Packs → Hub; **no crash**, navigation stays usable.
|
||||||
|
- **No server request** on selection (no `/store/transaction`, no `/purchased/items`,
|
||||||
|
no 65534 reference); the verdict is client-side. (OBSERVED)
|
||||||
|
- **Zero economy/profile mutation:** coins/items/nextItemId/`unopenedPackIds`/
|
||||||
|
`last_pack` all unchanged; profile byte-identical (`39bb3e83…`); 65534 not
|
||||||
|
persisted. (OBSERVED)
|
||||||
|
|
||||||
|
**Active-placeholder verdict: MARGINALLY ACCEPTABLE** — crash-safe + economy-safe +
|
||||||
|
navigable, but with user-visible defects (empty fake tile; "no longer available" on
|
||||||
|
explicit click; Browse-Packs nav gate). It is a *strict improvement* over the current
|
||||||
|
inactive-sentinel baseline (which errors on store OPEN and bounces to Hub).
|
||||||
|
|
||||||
|
**Backend-side question is now fully answered.** The complete zero-unopened-packs
|
||||||
|
ladder:
|
||||||
|
```
|
||||||
|
no mypacks group -> CardsDLL null-deref CRASH (unsafe)
|
||||||
|
inactive placeholder -> "pack not available" on store open -> Hub (baseline)
|
||||||
|
active placeholder -> store loads; empty tile; "no longer available" only on
|
||||||
|
explicit click; recoverable; economy-safe (best backend option)
|
||||||
|
real active owned pack -> fully correct UI (but grants a real openable pack — economy risk)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Permanent-fix recommendation: P2.** The active non-openable placeholder is the best
|
||||||
|
*safe* backend-only option, but a fully *clean* zero-pack experience is **not**
|
||||||
|
achievable server-side (Candidate F CONTRADICTED — the My-Packs resolution is
|
||||||
|
Scaleform/movie-driven). Recommend: adopt the active placeholder as an optional
|
||||||
|
backend compatibility mode (safe, strictly better than baseline) AND pursue a
|
||||||
|
client-side fix (hide the fake tile / stop the forced My-Packs resolution) for the
|
||||||
|
fully clean result. **Not implemented.** Do NOT grant a real pack.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Client resolver-guard experiment (2026-08-13) — RESULT F3 (crash, confounded)
|
||||||
|
|
||||||
|
A client-side `autopatch.py` memory guard (CardsDLL `0x180014858` `JNZ`→`JG`, routing
|
||||||
|
category `<0` to list-all/Browse) was tested against the exact no-sentinel server condition
|
||||||
|
(sentinel 65534 suppressed; `GET /store/purchasegroup` ids `[1,5,6,7]`, no mypacks group).
|
||||||
|
The client **crashed at the identical resolver site `0x180014882`** (`[NULL+0x48]`), because
|
||||||
|
it presented a **positive** My-Packs ordinal (crash is in the `>0` resolve branch), not the
|
||||||
|
`-1` the guard diverts. **Confound:** FIFA was not relaunched after the backend flip, so it
|
||||||
|
reused stale (sentinel-present) tab state. So the negative-only guard is **insufficient for a
|
||||||
|
positive stale/invalid ordinal**, and the fresh-client case is **not yet decided** (needs a
|
||||||
|
clean re-test: fresh launch with backend already no-sentinel). Backend P2 sentinel was
|
||||||
|
restored immediately (mandatory rollback). Full record + candidate stronger guard:
|
||||||
|
`docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md` PART III.
|
||||||
|
|
||||||
|
## Fresh-process no-sentinel retest (2026-08-13) — RESULT R1 (SUCCESS)
|
||||||
|
|
||||||
|
Re-ran the above cleanly: backend entered no-sentinel mode **while FIFA was closed**, then a
|
||||||
|
**fresh** FIFA (pid 553220, new autopatch 552999, guard `85 ff 7f 0f` enforced) launched and
|
||||||
|
opened the Store. The genuine no-sentinel `/store/purchasegroup` (ids `[1,5,6,7]`, no 65534/
|
||||||
|
mypacks) is **byte-identical** to the F3 capture, so the only changed variable is client
|
||||||
|
process lifetime. Outcome: **no crash, no dialog, Store opens on Browse Packs, packs
|
||||||
|
navigable** (cosmetics only: no tabs / no cover art / "0 items" — pre-existing). A fresh
|
||||||
|
client publishes category `-1` for the absent group, which `JNZ→JG` routes to Browse/list-all
|
||||||
|
with no NULL deref. **This confirms F3 was stale-positive-ordinal contamination, and proves
|
||||||
|
Strategy A (resolver guard) on the tested build.** Backend P2 sentinel restored immediately
|
||||||
|
(`f416e71e…`, `state=active`) and remains production default. Full record:
|
||||||
|
`docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md` PART IV.
|
||||||
@@ -0,0 +1,924 @@
|
|||||||
|
# Store Tile Investigation (bug 6c)
|
||||||
|
|
||||||
|
Status: **ROOT CAUSE ESTABLISHED — P2 compatibility workaround IMPLEMENTED**
|
||||||
|
(2026-08-13). Full investigation complete (§1-§17); backend fix landed in
|
||||||
|
`fifa17-recon/tools/utas_server.py` `store_catalog` with regression tests in
|
||||||
|
`fifa17-recon/tools/test_empty_mypacks.py`. The store-tiles flags are unchanged
|
||||||
|
(`FUT_STORE_DISPLAYGROUP=ON`, `FUT_STORE_GROUPID=OFF`) and the profile is unchanged.
|
||||||
|
|
||||||
|
## RESOLUTION (2026-08-13)
|
||||||
|
|
||||||
|
**ROOT CAUSE (bug 6c):** FIFA 17's Store/Scaleform path RESOLVES the `mypacks`
|
||||||
|
category even when the account owns zero unopened packs (the category is chosen
|
||||||
|
client-side from the movie's `CATEGORY_ID` → `screen+0x290`; no server field gates
|
||||||
|
it — Candidate F CONTRADICTED). CardsDLL `FUN_1800147f0` then dereferences the
|
||||||
|
resolved group with NO null guard, so an absent `mypacks` group crashes the client
|
||||||
|
(`CardsDLL_Win64_retail.dll+0x14882`, `[NULL+0x48]` — minidump-confirmed, §8).
|
||||||
|
|
||||||
|
**BACKEND RESULT / DECISION — P2 (compatibility workaround):** emit a synthetic,
|
||||||
|
**active**, non-openable `mypacks` placeholder (id 65534, absent from `PACK_CATALOG`)
|
||||||
|
only when `unopenedPackIds == []`. This is crash-safe AND economy-safe (explicit
|
||||||
|
selection is rejected client-side with "This pack is no longer available", sends no
|
||||||
|
backend request, and mutates nothing — §17). It is a FIFA-17 client-compatibility
|
||||||
|
shim, **NOT** an EA-authentic empty-My-Packs representation, and is confined to the
|
||||||
|
FIFA-17 adapter/backend layer (NOT OpenFUT Core).
|
||||||
|
|
||||||
|
**KNOWN UX LIMITATIONS (unfixable server-side):** a fake empty "0 items" tile; an
|
||||||
|
explicit-selection dialog "This pack is no longer available"; and a Browse-Packs →
|
||||||
|
My-Packs navigation quirk. These are Scaleform/movie-driven.
|
||||||
|
|
||||||
|
**CLEAN CLIENT FIX:** still unresolved; belongs to client-side work —
|
||||||
|
`docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md`.
|
||||||
|
|
||||||
|
Evidence labels: **OBSERVED** (running code / `docker inspect` / live log / minidump /
|
||||||
|
table), **INFERRED**, **HYPOTHESIS**, **UNKNOWN**.
|
||||||
|
|
||||||
|
### Hypotheses
|
||||||
|
|
||||||
|
- **H1 (original — subtype/definition):** *Store tiles render "unknown" because the
|
||||||
|
server emits definitions whose type/subtype the client cannot map (prime suspects
|
||||||
|
5004xxx misc {231,232,233,236}, 8010xxx league logos, FCC↔wire subtype gaps).*
|
||||||
|
**Verdict: CONTRADICTED by static store-handler evidence.** The `/store/purchasegroup`
|
||||||
|
handler emits PACK definitions only — no `cardsubtypeid`/`carddbid`/`cardassetid`
|
||||||
|
anywhere in the response (§2). Those subtype families belong to the separate
|
||||||
|
**club-item / consumable / equippable** paths (`fut_clubitems.py`, `fut_consumables`,
|
||||||
|
`/club?type=`), which are OUT OF SCOPE for this store-tile task. History preserved
|
||||||
|
in §3.3 and §8; not investigated further here.
|
||||||
|
- **H2 (revised — displayGroup token):** **HYPOTHESIS (under runtime test).** *The
|
||||||
|
"unknown" FUT Store tile is caused by one or more pack entries whose
|
||||||
|
`displayGroup.value` token is not one of the six categories FIFA 17 can render:*
|
||||||
|
`mypacks, points, bronze, silver, gold, special`. Tested against a real capture in
|
||||||
|
§4-§8.
|
||||||
|
|
||||||
|
---
|
||||||
|
## Runtime capture plan (Phase 2) — OBSERVED
|
||||||
|
|
||||||
|
- **Backend container:** `openfut-fut-backend` (logs on stdout via `log()`,
|
||||||
|
`utas_server.py:59-62`; each request logs `"<VERB> <path>"` at `:3732`, and the
|
||||||
|
response line `" -> <code> <body[:200]>"` at `:3754` — **response body truncated
|
||||||
|
to 200 bytes**, so the full JSON body is NOT in the log).
|
||||||
|
- **Endpoint / path matcher:** `GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true`
|
||||||
|
(OBSERVED historically in the log; route regex `/store/purchasegroup`,
|
||||||
|
`utas_server.py:1215`).
|
||||||
|
- **Capture marker (UTC, set immediately before the manual test):**
|
||||||
|
`2026-08-12T23:54:01Z` (saved to `/tmp/store_capture_marker.txt`; 0 log lines
|
||||||
|
after it at set-time → clean boundary).
|
||||||
|
- **Log command to isolate the manual action:**
|
||||||
|
`docker logs --since 2026-08-12T23:54:01Z --timestamps openfut-fut-backend`
|
||||||
|
then locate the first `GET .../store/purchasegroup` line after the marker plus its
|
||||||
|
following headers and `-> 200 {"purchase"...` line.
|
||||||
|
- **Full-body recovery (because the log truncates at 200 bytes):** the response is a
|
||||||
|
deterministic pure function — `store_catalog()` (`:3408`) over static `PACK_CATALOG`
|
||||||
|
(`fut_store.py:820`) + live `STORE.unopened_packs()` (from `/state` profile) under
|
||||||
|
fixed flags (`STORE_DISPLAYGROUP=ON`, `STORE_GROUPID=OFF`, `FUT_PRICE_PROBE=OFF`).
|
||||||
|
Plan: reconstruct the exact body from the running `/app` code + live `/state`
|
||||||
|
profile, then **verify** its `json.dumps(...)[:200]` byte-for-byte equals the genuine
|
||||||
|
logged 200-byte prefix. Match ⇒ the reconstruction IS the sent body. No request is
|
||||||
|
synthesized, replayed, or curl'd; the genuine log line is the ground-truth anchor.
|
||||||
|
|
||||||
|
|
||||||
|
## 1. Method
|
||||||
|
|
||||||
|
### Environment (OBSERVED)
|
||||||
|
- Running on `10.10.0.120` (dev-lxc). Client is `10.10.0.105`.
|
||||||
|
- Backend under test: Docker container `openfut-fut-backend`
|
||||||
|
(image `openfut-fut-backend:dev`), `Up 7 hours`, entrypoint `/app/entrypoint.sh`.
|
||||||
|
- `docker inspect openfut-fut-backend`: only mount is
|
||||||
|
`/home/alex/OpenFUT/fifa17-recon/docker/state -> /state (rw)`; container ENV
|
||||||
|
contains **no `FUT_STORE_*`** vars.
|
||||||
|
- `entrypoint.sh` launches `python3 -u utas_server.py` from `/app/tools` with extra
|
||||||
|
env `FUT_TRADING=1 FUT_PILESIZES=1 FUT_TRADEABLE=1 FUT_DISCARD_TABLE=1
|
||||||
|
FUT_DISCARD_SEND=1` — again **no `FUT_STORE_*`**.
|
||||||
|
- The running store code is `/app/tools/utas_server.py`. Extracted read-only via
|
||||||
|
`docker cp openfut-fut-backend:/app/tools /tmp/app-tools` and confirmed
|
||||||
|
**byte-identical** (`sha256`) to the repo copy
|
||||||
|
`fifa17-recon/tools/utas_server.py` (both 3765 lines) and
|
||||||
|
`fifa17-recon/tools/fut_clubitems.py`. All line references below are to the repo
|
||||||
|
paths and equal the running code.
|
||||||
|
|
||||||
|
### Commands (exact)
|
||||||
|
```
|
||||||
|
docker ps --format '{{.Names}}\t{{.Image}}\t{{.Command}}\t{{.Status}}'
|
||||||
|
docker inspect openfut-fut-backend --format '...CMD/ENTRYPOINT/MOUNTS/ENV...'
|
||||||
|
docker exec openfut-fut-backend cat /app/entrypoint.sh
|
||||||
|
docker cp openfut-fut-backend:/app/tools /tmp/app-tools
|
||||||
|
docker cp openfut-fut-backend:/app/data /tmp/app-data
|
||||||
|
diff /tmp/app-tools/utas_server.py fifa17-recon/tools/utas_server.py # identical
|
||||||
|
diff /tmp/app-tools/fut_clubitems.py fifa17-recon/tools/fut_clubitems.py # identical
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual test sequence
|
||||||
|
NOT executed. The manual FIFA-client store capture (2D/2E) was never reached
|
||||||
|
because the investigation blocked at 2C before any flag could be enabled. No
|
||||||
|
request was synthesized, replayed, or simulated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Store handler code path (2A) — OBSERVED
|
||||||
|
|
||||||
|
Request → response for the store tile screen:
|
||||||
|
|
||||||
|
1. **Endpoint.** `GET ut/<sku>/store/purchasegroup/...` — `FutStoreGetPackTypes`
|
||||||
|
(client deser root `0x1801234e0`). (`utas_server.py:3408-3409` docstring.)
|
||||||
|
- Route table entry: `(re.compile(r"/store/purchasegroup"), lambda m,h:
|
||||||
|
store_catalog(h))` at `utas_server.py:1215`.
|
||||||
|
- Sibling store routes: `/store/transaction -> store_buy(h)` (`:1216`, the BUY);
|
||||||
|
bare `/store(\?|$) -> (200,{"result":"SUCCESS"})` eligibility gate (`:1221`).
|
||||||
|
2. **Handler.** `store_catalog(h)` (`utas_server.py:3408-3447`).
|
||||||
|
- Iterates `PACK_CATALOG` (non-`ownedOnly` packs) → `_pack_body(p, idx)`.
|
||||||
|
- Appends owned unopened packs from `visible_unopened_packs()` →
|
||||||
|
`STORE.unopened_packs()` + `_OPENED_PACK_GRACE` (`:51-52`, `:3423-3427`).
|
||||||
|
- If no owned packs, appends one inactive `mypacks` sentinel (id 65534) so
|
||||||
|
`GOTO_STORE_MYPACK` resolves (`:3428-3446`).
|
||||||
|
- Returns `200, {"purchase": [<pack bodies>], "timestamp": 1596326400}`
|
||||||
|
(`:3447`).
|
||||||
|
3. **Definition construction.** `_pack_body(p, idx, owned=False)`
|
||||||
|
(`utas_server.py:3268-3405`). Emitted keys (OBSERVED, `:3293-3328`):
|
||||||
|
`assetId`(=`p["id"]`), `id`(=`p["id"]`), `packType`, `description`(=`p["name"]`),
|
||||||
|
`state`, `saleType`, `limitType`, `quantity`, `purchaseLimit`, `purchaseCount`,
|
||||||
|
`isPremium`, `sortPriority`, `currencies`, `extPrice`, `packContentInfo`
|
||||||
|
(`bronze/silver/gold/rare/itemQuantity`), `unopened`, and one of:
|
||||||
|
- owned pack → `displayGroup = {"value":"mypacks","priority":idx}` (`:3336`);
|
||||||
|
- else if `STORE_DISPLAYGROUP` → `displayGroup = {"value": category}` where
|
||||||
|
`category ∈ {special, gold, silver, bronze}` chosen from
|
||||||
|
`p["specialChance"]`/`p["gold"]` (`:3337`,`:3373-3379`), and if `STORE_GROUPID`
|
||||||
|
also `displayGroupAssetId = p["id"]` (`:3403-3404`).
|
||||||
|
4. **Data source(s).**
|
||||||
|
- `PACK_CATALOG` — a hardcoded list of 3 pack dicts
|
||||||
|
(`{id,name,price,count,gold,tiers,specialChance}`) at `fut_store.py:820-841`.
|
||||||
|
There is NO card table read in this path.
|
||||||
|
- `STORE = Store()` (`fut_store.py:843`), backed by the profile JSON
|
||||||
|
(`unopened_packs()` reads `unopenedPackIds`, `fut_store.py:619-621`).
|
||||||
|
5. **Serialization → HTTP.** The dict is JSON-encoded by the server's response
|
||||||
|
writer and returned as the HTTP body.
|
||||||
|
|
||||||
|
### Fields 5 (`cardsubtypeid`/`carddbid`/`cardassetid`) — OBSERVED
|
||||||
|
**None of `cardsubtypeid`, `carddbid`, or `cardassetid` appear anywhere in the
|
||||||
|
store-tile (`purchasegroup`) response.** `_pack_body` (`:3293-3405`) emits only the
|
||||||
|
pack keys listed above; `assetId`/`id` are the PACK id `p["id"]` (e.g. 1, 5), not a
|
||||||
|
card asset id. The store catalog emits PACKS, never card definitions. (Grep of
|
||||||
|
`_pack_body` and `store_catalog` for those three field names returns zero hits.)
|
||||||
|
|
||||||
|
### Families the store handler can emit
|
||||||
|
Only **packs** (`PACK_CATALOG`: "Bronze Pack" id 1, "Gold Pack" id 5, and the third
|
||||||
|
catalog entry) plus owned reward packs and the `mypacks` sentinel. It cannot emit
|
||||||
|
any card family (players, staff, consumables, club items).
|
||||||
|
|
||||||
|
### Filtering / transformation
|
||||||
|
- `normal = [p for p in PACK_CATALOG if not p.get("ownedOnly")]` (`:3421`).
|
||||||
|
- `_pack_body` maps a pack to a category token via `specialChance>=1.0 -> special`,
|
||||||
|
else `gold -> gold`, `p.get("silver") -> silver`, else `bronze` (`:3373-3379`).
|
||||||
|
- The tile caption/category is `displayGroup.value`; `description` carries the
|
||||||
|
per-pack title.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Wire subtype coverage (2B)
|
||||||
|
|
||||||
|
### 3.1 Store path
|
||||||
|
The store-tile path emits **no `cardsubtypeid`** at all (see §2). So for the store
|
||||||
|
tile, "is `cardsubtypeid` the raw FCC subtype, the wire category, transformed, or
|
||||||
|
something else?" → **not present** (N/A). The store tile is selected by
|
||||||
|
`displayGroup.value` (a category-token STRING), not by any card subtype.
|
||||||
|
|
||||||
|
Per `_pack_body:3367-3372` (citing `FUN_180014580`/`FUN_180014df0`): FIFA 17's
|
||||||
|
StoreFront resolves exactly **six hard-coded category tokens** —
|
||||||
|
`mypacks, points, bronze, silver, gold, special`. Any other `displayGroup.value`
|
||||||
|
(e.g. a raw pack title) creates an "unsupported pseudo-category". The documented
|
||||||
|
cause of "unknown" store tiles is therefore a **`displayGroup` category-token**
|
||||||
|
issue on packs (absent group, or a non-canonical token), NOT a card type/subtype.
|
||||||
|
|
||||||
|
### 3.2 Club-item path (the only place wire subtypes live) — `fut_clubitems.py`
|
||||||
|
Club items are served on the **`/club?type=` route** (`utas_server.py:2250`,
|
||||||
|
`handle_club`), gated by `FUT_CLUBITEMS`, NOT by the store route. Current `FAMILIES`
|
||||||
|
(`fut_clubitems.py:61-67`), format `(family, table, art id, stat id, stat name,
|
||||||
|
UNVERIFIED cardsubtypeid)`:
|
||||||
|
|
||||||
|
| wire subtype | mapped family | table | art id | source | confidence |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| 9 | kits | fcc_kitcards.json | 35 | `fut_clubitems.py:65` | HYPOTHESIS (marked "UNVERIFIED", `:49`,`:30-32`) |
|
||||||
|
| 10 | stadia | fcc_stadium.json | 36 | `fut_clubitems.py:63` | HYPOTHESIS ("UNVERIFIED") |
|
||||||
|
| 11 | badges | fcc_badgecards.json| 39 | `fut_clubitems.py:64` | HYPOTHESIS ("UNVERIFIED") |
|
||||||
|
| 30 | balls | fcc_balls.json | 37 | `fut_clubitems.py:62` | HYPOTHESIS ("UNVERIFIED") |
|
||||||
|
| 31 | leaguelogos | fcc_leaguelogos.json| 40| `fut_clubitems.py:66` | HYPOTHESIS ("UNVERIFIED") |
|
||||||
|
|
||||||
|
- The prior recorded mapping (9=kits,10=stadia,11=badges,30=balls,31=logos) is
|
||||||
|
**confirmed present in current code** — but the code itself marks every one
|
||||||
|
"UNVERIFIED cardsubtypeid" and states the binary assigns family↔subtype **nowhere**
|
||||||
|
in the 149 dumped tables; cardtype-9 admits the set `{30,31,145,146,147,148,149,150}`
|
||||||
|
(`fut_clubitems.py:26-28`, `:73`). So these are server-chosen HYPOTHESIS values.
|
||||||
|
- In the club-item wire item (`_item:88-119`), `cardsubtypeid` (`:97`) is a
|
||||||
|
**server-assigned wire-category constant** (9/10/11/30/31), NOT the raw FCC
|
||||||
|
subtype: the club-item fcc tables carry **no `cardsubtype` column at all** (Task 1:
|
||||||
|
badges/stadium/kit/logos/balls have empty subtype sets). `resourceId`/`assetId`
|
||||||
|
= `carddbid`, and `cardassetid` = the family ART id (`:94-96`). So for club items:
|
||||||
|
**`cardsubtypeid` = wire category (server constant), `carddbid`/`cardassetid` =
|
||||||
|
real fcc columns.**
|
||||||
|
- By contrast, consumables (`fut_store._item` / `fut_consumables`) DO carry the raw
|
||||||
|
FCC subtype (51..341) as `cardsubtypeid`.
|
||||||
|
|
||||||
|
### 3.3 Task-1 families vs wire-subtype coverage
|
||||||
|
Families that the **store handler** can map to a wire subtype: **none** — the store
|
||||||
|
handler emits packs, which have no card subtype (by design).
|
||||||
|
|
||||||
|
Families for which a **club-item** wire subtype exists (HYPOTHESIS-grade):
|
||||||
|
kits(9), stadia(10), badges(11), balls(30), leaguelogos(31).
|
||||||
|
|
||||||
|
Task-1 card families with **no wire subtype mapping anywhere in the server**:
|
||||||
|
- **5001xxx contracts, 5002xxx fitness/healing, 5003xxx training** — served as
|
||||||
|
consumables carrying their raw FCC subtype; these are consumable overlays, not
|
||||||
|
store tiles, and not part of any store/club-item wire-category map.
|
||||||
|
- **5004xxx misc {231,232,233,236}** — **no wire subtype mapping found** in
|
||||||
|
`fut_clubitems.py` or the store path. (Grep: no `misc` family, no {231,232,233,236}
|
||||||
|
wire assignment.) They exist only as raw FCC subtypes in consumable data.
|
||||||
|
- **8010xxx league logos/stickers** — mapped in the **club-item** path as wire
|
||||||
|
subtype **31** (`fut_clubitems.py:66`), HYPOTHESIS-grade. `fcc_leaguelogostickers`
|
||||||
|
(39 rows) is NOT wired in `FAMILIES` (only `fcc_leaguelogos`, 44 rows).
|
||||||
|
- **6000xxx badges, 6200xxx stadiums, 6300/6400xxx kits, 8120xxx balls** — mapped in
|
||||||
|
the club-item path (11/10/9/30), HYPOTHESIS-grade.
|
||||||
|
- **staff (managers/coaches, subtypes 4-8)** — served by `fut_staff` on `/club?type=
|
||||||
|
manager`, not a store tile.
|
||||||
|
|
||||||
|
Note none of these belong to the **store-tile** (`purchasegroup`) response.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2C flag investigation — BLOCKED
|
||||||
|
|
||||||
|
### Store-tiles flags located (OBSERVED)
|
||||||
|
Two, both in `utas_server.py`, both module-level constants read **once at import**:
|
||||||
|
|
||||||
|
| flag | env var | line (read) | consumed | default | current running value |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `STORE_DISPLAYGROUP` | `FUT_STORE_DISPLAYGROUP` | `:975` | `_pack_body:3337` | `"1"` → **ON** | **ON** (no env override) |
|
||||||
|
| `STORE_GROUPID` | `FUT_STORE_GROUPID` | `:980` | `_pack_body:3403` | `"0"` → **OFF** | **OFF** (no env override) |
|
||||||
|
|
||||||
|
- `:975` `STORE_DISPLAYGROUP = os.environ.get("FUT_STORE_DISPLAYGROUP", "1") == "1"`
|
||||||
|
- `:980` `STORE_GROUPID = os.environ.get("FUT_STORE_GROUPID", "0") == "1"`
|
||||||
|
|
||||||
|
**Current values (recorded before any change; nothing was changed):**
|
||||||
|
- `FUT_STORE_DISPLAYGROUP`: unset in container env → default `"1"` →
|
||||||
|
`STORE_DISPLAYGROUP = True` (**ON**). (INFERRED from OBSERVED env dump + OBSERVED
|
||||||
|
code default.)
|
||||||
|
- `FUT_STORE_GROUPID`: unset in container env → default `"0"` →
|
||||||
|
`STORE_GROUPID = False` (**OFF**). This is the flag "expected to be OFF".
|
||||||
|
|
||||||
|
The related club-item flag `FUT_CLUBITEMS` (`:1647-1648`) is likewise unset →
|
||||||
|
`CLUBITEMS = False` (club items not currently served), also a module-level import
|
||||||
|
constant.
|
||||||
|
|
||||||
|
### Dynamic evaluation? NO (OBSERVED)
|
||||||
|
- All three flags are top-level `os.environ.get(...)` assignments evaluated at
|
||||||
|
module import (`:975`, `:980`, `:1647`); `_pack_body` reads the resulting module
|
||||||
|
**constants** (`:3337`, `:3403`), never `os.environ` at request time.
|
||||||
|
- Repo-wide there is **no** `importlib.reload`, no `signal`/`SIGHUP` handler, and no
|
||||||
|
per-request environ re-read for these flags. (The only runtime-refreshable feature
|
||||||
|
is `FUT_ID_SWEEP`, which re-reads a *file* `SWEEP_FILE`, `:1965-1966` — unrelated.)
|
||||||
|
- `:3250` documents the intended workflow explicitly:
|
||||||
|
`# Enable for the test with: FUT_PRICE_PROBE=1 ./openfut-fut.sh restart`.
|
||||||
|
|
||||||
|
### Restart conflict → STOP
|
||||||
|
Changing either store flag requires either setting a container env var (→
|
||||||
|
`docker` recreate = restart) or editing the module (→ re-import = restart). Both
|
||||||
|
violate the no-restart / no-redeploy / no-recreate constraint. Therefore:
|
||||||
|
|
||||||
|
```
|
||||||
|
TASK 2 BLOCKED AT 2C:
|
||||||
|
Flag requires restart, conflicting with no-restart constraint.
|
||||||
|
```
|
||||||
|
|
||||||
|
No flag was enabled. 2D/2E (manual client capture + definition analysis) NOT
|
||||||
|
started. No client request generated, replayed, or simulated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Captured request and response (Phase 3) — OBSERVED
|
||||||
|
|
||||||
|
Real client action (operator opened the FUT Store on `.105`, 2026-08-12). Only one
|
||||||
|
`/store/purchasegroup` request occurred after the capture marker
|
||||||
|
`2026-08-12T23:54:01Z`, so attribution is unambiguous (only the operator drives the
|
||||||
|
client). Log via `docker logs --since 2026-08-12T23:54:01Z --timestamps openfut-fut-backend`.
|
||||||
|
|
||||||
|
- **Request:** `23:54:43 GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true`
|
||||||
|
(Host `10.10.0.120:8099`, `User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)`,
|
||||||
|
`X-UT-SID` present). Preceded at 23:54:43 by `GET /ut/game/fifa17/user/credits`
|
||||||
|
→ `200 {"credits": 29876776, ...}`.
|
||||||
|
- **Response:** `200`. The server log truncates the body to 200 bytes
|
||||||
|
(`utas_server.py:3754`, `raw[:200]`), genuine prefix:
|
||||||
|
`{"purchase": [{"assetId": 1, "id": 1, "packType": "BRONZE", "description": "Bronze Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount`
|
||||||
|
- **Full body recovered** by running the exact running-container code
|
||||||
|
(`docker exec openfut-fut-backend python3 -c "import utas_server as u; u.store_catalog(None)"`)
|
||||||
|
over the live `/state` profile, under the live flags (confirmed in-process:
|
||||||
|
`STORE_DISPLAYGROUP=True`, `STORE_GROUPID=False`). Its `json.dumps(...)[:200]`
|
||||||
|
equals the genuine logged 200-byte prefix **byte-for-byte** (verified MATCH), so the
|
||||||
|
reconstruction IS the sent body (deterministic pure function + verified prefix). No
|
||||||
|
request was synthesized, replayed, or curl'd. Full body (2841 bytes) preserved
|
||||||
|
verbatim at `docs/evidence/store_purchasegroup_capture_2026-08-12.json`.
|
||||||
|
- **Operator-reported client behavior:** on opening the store, an error dialog
|
||||||
|
appeared — *"The pack you've selected is currently not available. Please select a
|
||||||
|
different pack or try again later."* — and clicking OK returned to the FUT hub. No
|
||||||
|
tiles were browsable; **no "unknown" tiles were reported**. There was **no**
|
||||||
|
`PUT /store/transaction` (no buy) and **no server error** (server answered `200`).
|
||||||
|
|
||||||
|
## 5. Definition analysis (Phase 4/5) — OBSERVED
|
||||||
|
|
||||||
|
`purchase[]` = 5 entries (`timestamp` 1596326400). No `cardsubtypeid`/`carddbid`/
|
||||||
|
`cardassetid` in any entry (packs, not cards).
|
||||||
|
|
||||||
|
| idx | id | assetId | packType | description | displayGroup.value | priority | state | dgAssetId | classification |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| 0 | 1 | 1 | BRONZE | Bronze Pack | `bronze` | — | active | absent | KNOWN TOKEN |
|
||||||
|
| 1 | 5 | 5 | GOLD | Gold Pack | `gold` | — | active | absent | KNOWN TOKEN |
|
||||||
|
| 2 | 6 | 6 | GOLD | Premium Gold | `gold` | — | active | absent | KNOWN TOKEN |
|
||||||
|
| 3 | 7 | 7 | GOLD | Special Players Pack | `special` | — | active | absent | KNOWN TOKEN |
|
||||||
|
| 4 | 65534 | 65534 | GOLD | "" (empty) | `mypacks` | 1 | **inactive** | absent | KNOWN TOKEN |
|
||||||
|
|
||||||
|
Per-pack fields (idx 0-3 identical shape): `saleType:"promo"`, `limitType:"NONE"`,
|
||||||
|
`quantity:0`, `purchaseLimit:0`, `purchaseCount:0`, `isPremium:false`,
|
||||||
|
`currencies:[{"name":"coins","funds":<price>,"finalFunds":<price>}]`,
|
||||||
|
`extPrice:{finalPrice/originalPrice:{amount:<price/100>,currency:"mtx"}}`,
|
||||||
|
`packContentInfo:{...tier quantities...}`, `unopened:false`. Prices: Bronze 400,
|
||||||
|
Gold 5000, Premium Gold 15000, Special Players 25000. The sentinel (idx 4) drops
|
||||||
|
`currencies`/`extPrice`, has empty description, `state:"inactive"`.
|
||||||
|
|
||||||
|
**Unique `displayGroup.value` set emitted: `{bronze, gold, special, mypacks}`.**
|
||||||
|
Compared to the six client renderer tokens `{mypacks, points, bronze, silver, gold,
|
||||||
|
special}` (`FUN_180014580`/`FUN_180014df0`, `plan-2026-08-05-store-subsystem.md:202-206`):
|
||||||
|
**every emitted token is a KNOWN token; the set outside the renderer categories is
|
||||||
|
EMPTY.**
|
||||||
|
|
||||||
|
## 6. Unmapped definitions / suspect tokens found — OBSERVED
|
||||||
|
|
||||||
|
None. Zero SUSPECT/UNKNOWN `displayGroup.value` tokens; zero card definitions;
|
||||||
|
zero `cardsubtypeid`/`carddbid`/`cardassetid`. The only anomalous element is the
|
||||||
|
**inactive, empty-description `mypacks` sentinel** (idx 4), emitted by
|
||||||
|
`store_catalog:3428-3446` **only when the profile owns zero unopened packs**
|
||||||
|
(OBSERVED: profile `unopenedPackIds == []`).
|
||||||
|
|
||||||
|
## 7. Client-side evidence (Phase 7) — existing RE only
|
||||||
|
|
||||||
|
No new Ghidra run. Existing decompiler evidence already bounds the answer and shows
|
||||||
|
new static analysis would be unproductive:
|
||||||
|
- **The parser is not the gate** (`OPENCODE_ENDPOINT_PROMPT.md:118-120`): per-pack
|
||||||
|
epilogue `0x18013badc` pushes every parsed pack unconditionally — no drop predicate
|
||||||
|
in the parse path. So a `200` with clean JSON cannot be rejected by the deserializer.
|
||||||
|
- **Store-level "not available"** (`FUT_CatalogNotAvailable`, msg `0x7550`) comes from
|
||||||
|
downstream client gates (`OPENCODE_ENDPOINT_PROMPT.md:120-131`): (1) resolution
|
||||||
|
`GetSystemMetrics` ≤1024×768, (2) store-data-model load status `0x180013cf0`,
|
||||||
|
(3) Blaze purchase-config flags `IS_STORE_ENABLED/IS_COIN_PURCHASABLE/...` (these
|
||||||
|
are already served, `blaze_responder_v3b.py:708-719`).
|
||||||
|
- **Per-pack tile-availability predicate is in the PACKED FIFA17.exe and UNREAD**
|
||||||
|
(`plan-2026-08-05-pack-opening.md:553`): fields `state/saleType/quantity/
|
||||||
|
purchaseLimit/purchaseCount/start/end` are parsed and copied to the tile, but the
|
||||||
|
predicate that greys/blocks a tile "is in the packed exe and unread." Whether
|
||||||
|
`purchaseLimit`+`purchaseCount` greys a tile is explicitly an OPEN question
|
||||||
|
(`:644-645`). Static Ghidra on the packed exe cannot read it (decrypts only in live
|
||||||
|
memory); resolving it requires a live-memory experiment on the running FIFA process,
|
||||||
|
which is out of scope (must not touch FIFA).
|
||||||
|
- The exact operator string *"The pack you've selected is currently not available"*
|
||||||
|
is **not present** anywhere in the recon corpus (docs/tools); the documented
|
||||||
|
store/pack error strings are `FUT_CatalogNotAvailable` and
|
||||||
|
`CARDS_CB_ERR_PACK_NOT_IN_DIME` (a server-returnable code). UNKNOWN loc key.
|
||||||
|
|
||||||
|
## 8. Assessment (Phase 8) — runtime verdict
|
||||||
|
|
||||||
|
- **H1 (subtype/definition): CONTRADICTED.** The captured response contains no card
|
||||||
|
definitions and no `cardsubtypeid`/`carddbid`/`cardassetid` (OBSERVED §4-§5).
|
||||||
|
- **H2 (unknown `displayGroup.value` token): CONTRADICTED.** Every emitted token is a
|
||||||
|
KNOWN renderer category (`bronze/gold/special/mypacks`); the unsupported-token set
|
||||||
|
is EMPTY (OBSERVED §5). The store-tile "unknown" mechanism is NOT reproduced under
|
||||||
|
the current config (`STORE_DISPLAYGROUP=ON`).
|
||||||
|
|
||||||
|
Answering the Phase-8 questions:
|
||||||
|
1. **Unsupported `displayGroup.value` in the response?** No — all four tokens
|
||||||
|
(`bronze/gold/special/mypacks`) are recognized. (OBSERVED)
|
||||||
|
2. **Which pack got it?** None. (OBSERVED)
|
||||||
|
3. **Correspond to an unknown tile in FIFA?** No unknown tile was reported; the
|
||||||
|
observed symptom was a *"pack not available"* dialog, not an unknown tile. (OBSERVED)
|
||||||
|
4. **Where does the backend assign the token?** `_pack_body:3373-3379` maps each pack
|
||||||
|
to `special/gold/silver/bronze` from `specialChance`/`gold`; owned/sentinel →
|
||||||
|
`mypacks` (`:3336`). All canonical. (OBSERVED)
|
||||||
|
5. **Is `displayGroup.value` sufficient to explain the bug?** No. The response is
|
||||||
|
clean; the failure is a downstream client/packed-exe gate, not a token. (INFERRED)
|
||||||
|
6. **Is the server emitting a value FIFA demonstrably cannot understand?** No.
|
||||||
|
(OBSERVED)
|
||||||
|
7. **Narrowest likely fix (REPORT ONLY — not implemented):** The single anomalous,
|
||||||
|
server-controllable element is the **inactive empty `mypacks` sentinel** emitted
|
||||||
|
when `unopenedPackIds == []` (`store_catalog:3428-3446`). Leading HYPOTHESIS: with
|
||||||
|
no owned packs, the store's My-Packs group contains only this inactive pack, and
|
||||||
|
the client's (packed-exe) selection/availability path lands on it →
|
||||||
|
*"the pack you've selected is currently not available"* → back to hub. Narrowest
|
||||||
|
candidate fixes to TEST (each needs a controlled change, hence a future
|
||||||
|
restart-gated experiment — do NOT implement now):
|
||||||
|
(a) suppress the sentinel when there are no owned packs and instead let the store
|
||||||
|
land on a real active category (bronze/gold/special); or
|
||||||
|
(b) if My-Packs must resolve, make the sentinel non-selectable rather than an
|
||||||
|
`inactive` pack in the group.
|
||||||
|
Cheaper-to-eliminate CLIENT-side cause to check first (existing RE, no server
|
||||||
|
change): FIFA display resolution must be **>1024×768** on `.105`
|
||||||
|
(`OPENCODE_ENDPOINT_PROMPT.md:122-124`).
|
||||||
|
|
||||||
|
**Overall runtime verdict: CONTRADICTED** — neither H1 nor H2 reproduces; the
|
||||||
|
"unknown store tile" hypothesis is not the live failure. The live failure is a
|
||||||
|
distinct *pack-availability* error whose trigger is in the packed FIFA17.exe and
|
||||||
|
cannot be pinned from the (clean) server response alone.
|
||||||
|
|
||||||
|
## 9. Open questions
|
||||||
|
1. What exactly raises *"The pack you've selected is currently not available"*? The
|
||||||
|
loc key is unknown and the predicate is in the packed exe (unread). Resolving it
|
||||||
|
needs a controlled field/flag experiment or live-memory RE (both currently gated).
|
||||||
|
2. Does the store, with `unopenedPackIds == []`, land on / auto-select the inactive
|
||||||
|
`mypacks` sentinel? (HYPOTHESIS §8.7; unproven without client-side observation.)
|
||||||
|
3. Did the store render tiles successfully in earlier sessions when the profile
|
||||||
|
owned unopened packs (e.g. the 21:54-21:57 pack-opening burst)? If so, the
|
||||||
|
presence/absence of owned packs (sentinel) is implicated. (UNKNOWN — earlier logs
|
||||||
|
truncate the body; not proven.)
|
||||||
|
4. Does `purchaseLimit:0`/`purchaseCount:0` grey a tile? Existing RE lists this as an
|
||||||
|
OPEN question; would need a controlled experiment. (UNKNOWN)
|
||||||
|
5. Is bug 6c ("unknown tile") a stale symptom from before `STORE_DISPLAYGROUP` became
|
||||||
|
the default `ON`? Under the current config no unknown tile reproduces.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## H3 — EMPTY MY-PACKS SENTINEL (HYPOTHESIS)
|
||||||
|
|
||||||
|
When the profile owns zero unopened packs (`unopenedPackIds == []`), OpenFUT emits
|
||||||
|
synthetic **inactive** pack id **65534** in the `mypacks` display group
|
||||||
|
(`store_catalog:3428-3446`). The FIFA 17 store may treat this object as a selectable
|
||||||
|
pack whose availability predicate fails, producing *"The pack you've selected is
|
||||||
|
currently not available"* and returning the user to the FUT Hub before any
|
||||||
|
`/store/transaction`. **Status: HYPOTHESIS (untested).**
|
||||||
|
|
||||||
|
## 10. Resolution check (Phase 1) — OBSERVED
|
||||||
|
|
||||||
|
Read-only inspection of `.105` (FIFA pid 529227, not touched):
|
||||||
|
- Desktop/monitor: **2560x1440** — DRM connectors `card1-DP-2` and `card1-HDMI-A-1`
|
||||||
|
both `connected`, native mode 2560x1440; compositor KDE `kwin_wayland` (no gamescope).
|
||||||
|
- FIFA render config: `…/Games/umu/fifa17/pfx/drive_c/users/steamuser/Documents/FIFA 17/settings/overrideAutodetect.lua`
|
||||||
|
→ `ResolutionWidth = 1280`, `ResolutionHeight = 720`, `FullscreenEnabled = 0` (windowed).
|
||||||
|
- Session: Wayland (`WAYLAND_DISPLAY=wayland-0`), FIFA via XWayland (`DISPLAY=:0`,
|
||||||
|
`XAUTHORITY=/run/pressure-vessel/Xauthority` inside the game namespace).
|
||||||
|
|
||||||
|
**Is FIFA rendering above 1024x768? YES (1280x720).**
|
||||||
|
**Resolution hypothesis eliminated for this reproduction.**
|
||||||
|
|
||||||
|
## 11. Sentinel 65534 provenance (Phase 3)
|
||||||
|
|
||||||
|
1. **Basis:** **OpenFUT INVENTION** (OBSERVED). `65534` (0xFFFE) appears nowhere in
|
||||||
|
any EA capture or recon note — repo-wide it exists only in `utas_server.py`
|
||||||
|
(`store_catalog:3435-3446`) and this evidence set. The author's comment
|
||||||
|
(`:3428-3434`) states it is a workaround: "Retain an inactive zero-item sentinel so
|
||||||
|
the [`mypacks`] destination resolves… Its id is deliberately absent from
|
||||||
|
PACK_CATALOG." It is a compatibility guess, not captured behavior.
|
||||||
|
2. **Known-good EA capture of EMPTY My Packs:** **UNKNOWN** — none found. All recon
|
||||||
|
My-Packs analysis is client-side RE (`FUN_1800150d0` filters
|
||||||
|
`displayGroup.value=="mypacks"`, `plan-2026-08-05-pack-opening.md:34-36,893-895`);
|
||||||
|
no EA server response for an empty My Packs state is on record.
|
||||||
|
3. **Known-good response with ≥1 unopened pack:** **UNKNOWN** for EA. OpenFUT's own
|
||||||
|
seed grants reward pack id 70 (`_new_profile` `unopenedPackIds:[70]`,
|
||||||
|
`fut_store.py:360`), but that is an OpenFUT synthetic grant, not an EA capture.
|
||||||
|
4. **Evidence FIFA EXPECTS a sentinel/placeholder:** **NO / UNKNOWN.** Recon shows My
|
||||||
|
Packs is a client-side filter over the ordinary catalogue; the "empty-category
|
||||||
|
dialog over the wrong tab" concern behind the sentinel is the author's HYPOTHESIS,
|
||||||
|
not decompiler-confirmed. No evidence FIFA requires a placeholder object.
|
||||||
|
5. **Evidence for the `inactive` representation:** **UNKNOWN / HYPOTHESIS.** The claim
|
||||||
|
"state != active keeps it out of the visible row list" (`:3432`) is an unverified
|
||||||
|
author assumption; no RE shows `state:"inactive"` hides a pack from selection. If
|
||||||
|
FIFA does NOT hide it, the sole `mypacks` entry is a selectable inactive pack —
|
||||||
|
exactly the H3 failure mode.
|
||||||
|
|
||||||
|
## 12. Empty vs non-empty My Packs behavior (Phase 2) — OBSERVED (code) / captured
|
||||||
|
|
||||||
|
`store_catalog(h)` (`utas_server.py:3421-3447`):
|
||||||
|
- Always emits the 4 non-`ownedOnly` catalogue packs (ids 1,5,6,7) via `_pack_body`.
|
||||||
|
- For each id in `visible_unopened_packs()` (= `STORE.unopened_packs()` +
|
||||||
|
`_OPENED_PACK_GRACE`) that resolves in `PACK_CATALOG`, appends `_pack_body(owned,
|
||||||
|
idx, owned=True)`.
|
||||||
|
- **Only when `unopened_packs()` is empty (`if not owned_ids`)** appends the inactive
|
||||||
|
sentinel (`:3428-3446`).
|
||||||
|
|
||||||
|
Sentinel 65534 vs a normal active pack (Bronze, idx 0), field-by-field (from the
|
||||||
|
captured body):
|
||||||
|
|
||||||
|
| field | sentinel 65534 | Bronze Pack (active) |
|
||||||
|
|---|---|---|
|
||||||
|
| id | 65534 | 1 |
|
||||||
|
| assetId | 65534 | 1 |
|
||||||
|
| packType | GOLD | BRONZE |
|
||||||
|
| description | "" (empty) | "Bronze Pack" |
|
||||||
|
| state | **inactive** | active |
|
||||||
|
| saleType | promo | promo |
|
||||||
|
| limitType | NONE | NONE |
|
||||||
|
| quantity | 0 | 0 |
|
||||||
|
| purchaseLimit | 0 | 0 |
|
||||||
|
| purchaseCount | 0 | 0 |
|
||||||
|
| isPremium | false | false |
|
||||||
|
| sortPriority | 1 | 1 |
|
||||||
|
| currencies | **ABSENT** (popped) | `[{coins,400,400}]` |
|
||||||
|
| extPrice | **ABSENT** (popped) | `{mtx 4/4}` |
|
||||||
|
| packContentInfo | all-zero quantities | bronze 5 / item 5 |
|
||||||
|
| unopened | false | false |
|
||||||
|
| displayGroup.value | **mypacks** | bronze |
|
||||||
|
| displayGroup.priority | 1 | ABSENT |
|
||||||
|
| displayGroupAssetId | ABSENT | ABSENT |
|
||||||
|
|
||||||
|
**What changes when `unopenedPackIds` is non-empty (e.g. `[70]`):** the sentinel is
|
||||||
|
NOT emitted; instead pack 70 (Reward Special Players Pack, `ownedOnly`) appears via
|
||||||
|
the owned branch of `_pack_body` (`:3335-3336`): `displayGroup={"value":"mypacks",
|
||||||
|
"priority":idx}`, `state:"active"` (default), `unopened:true`, `currencies`/`extPrice`
|
||||||
|
popped. i.e. the `mypacks` group would hold a genuine **active** owned pack instead of
|
||||||
|
the inactive sentinel.
|
||||||
|
|
||||||
|
## 13. Proposed controlled experiments (Phase 5) — DESIGN ONLY, NOT EXECUTED
|
||||||
|
|
||||||
|
### Existing grant mechanism (Phase 4)
|
||||||
|
- **Supported profile-only method: YES** — `Store.grant_unopened_pack(pack_id)`
|
||||||
|
(`fut_store.py:635-643`): validates the id is in `PACK_CATALOG`, appends to
|
||||||
|
`unopenedPackIds`, persists; reverts via `consume_unopened_pack` (`:623-633`).
|
||||||
|
Modifies **only** profile state; cleanly reversible.
|
||||||
|
- **BUT restart IS required to take effect.** `Store.load()` caches `self._p`
|
||||||
|
(`:370-372`); **no route calls `grant_unopened_pack` or `select_account`**, and
|
||||||
|
`select_account` only re-reads on a persona *change*. So an out-of-process grant or
|
||||||
|
a raw profile-file edit writes disk but the **running server keeps serving its
|
||||||
|
cached `unopenedPackIds`** until the process reloads. There is no SIGHUP/reload
|
||||||
|
endpoint. Therefore any profile change needs a container restart to be observed.
|
||||||
|
|
||||||
|
### Experiment A — Non-empty My Packs (PREFERRED; least invasive)
|
||||||
|
- **State change:** set the profile's `unopenedPackIds` to `[70]` (edit
|
||||||
|
`…/state/accounts/33068179/fifa17_profile.json`, or call
|
||||||
|
`STORE.grant_unopened_pack(70)`), **store code/config unchanged**.
|
||||||
|
- **Restart required?** **YES** — profile cache (above). Profile-only; no code edit.
|
||||||
|
- **Rollback:** set `unopenedPackIds` back to `[]` (or `consume_unopened_pack(70)`),
|
||||||
|
restart. (Also restore `nextItemId`/coins only if a pack is actually opened — the
|
||||||
|
grant alone touches only `unopenedPackIds`.)
|
||||||
|
- **Expected `purchasegroup` difference:** sentinel 65534 GONE; instead one active
|
||||||
|
pack id 70 in the `mypacks` group (`state:active`, `unopened:true`); hub/credits
|
||||||
|
report `recoveredPacks:1`.
|
||||||
|
- **Expected client observation:** if H3 is correct, the immediate *"pack not
|
||||||
|
available"* dialog should NOT fire (or behavior changes) and My Packs should show a
|
||||||
|
real pack. If the dialog still fires identically, H3 is weakened and the cause is
|
||||||
|
elsewhere (packed-exe predicate / another field).
|
||||||
|
|
||||||
|
### Experiment B — Suppress the empty sentinel (only if A is inconclusive)
|
||||||
|
- **Change:** in `store_catalog` (`:3428-3446`), when `unopened_packs()` is empty, do
|
||||||
|
NOT append pack 65534 (emit no `mypacks` entry). **Code change ⇒ restart. NOT
|
||||||
|
authorized.** Narrowest patch: guard/remove the `if not owned_ids:` sentinel block.
|
||||||
|
- **Purpose:** distinguishes "the inactive sentinel is selected and fails" (A already
|
||||||
|
tests the inverse) from "an absent `mypacks` group causes a different failure"
|
||||||
|
(the original author's stated fear at `:3429-3431`).
|
||||||
|
|
||||||
|
### Experiment C — Alternate sentinel representation (design only)
|
||||||
|
- If evidence later shows FIFA expects an empty `mypacks` group represented
|
||||||
|
differently (e.g. present-but-not-a-pack, or `state` other than `inactive`), adjust
|
||||||
|
the sentinel shape. **DESIGN ONLY; DO NOT IMPLEMENT.** No current evidence specifies
|
||||||
|
the correct empty-group representation (see §11.4-11.5).
|
||||||
|
|
||||||
|
Note: both A and B require a restart (A for the profile cache, B for the code). A is
|
||||||
|
strictly less invasive (profile-only, clean rollback, no code change) and is the
|
||||||
|
preferred next experiment. Neither is authorized yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Experiment A — Non-empty My Packs (EXECUTED 2026-08-13) — OBSERVED
|
||||||
|
|
||||||
|
Authorized controlled test of H3: change ONLY the profile's `unopenedPackIds`
|
||||||
|
(`[] → [70]`), restart the FUT backend once, capture a genuine FIFA store request,
|
||||||
|
then roll back. No store code/config/flags/PACK_CATALOG/pack-70/sentinel-code
|
||||||
|
changed; FIFA not modified/restarted; operator drove the client.
|
||||||
|
|
||||||
|
### 14.1 Baseline profile state
|
||||||
|
- Path: `fifa17-recon/docker/state/accounts/33068179/fifa17_profile.json` (persona
|
||||||
|
33068179/CAGE; proven live: `STORE.path` in-process = `/state/accounts/33068179/
|
||||||
|
fifa17_profile.json`, coins 29876776 matching the live `/user/credits`).
|
||||||
|
- `unopenedPackIds == []`. Original SHA-256
|
||||||
|
`39bb3e833fa55287d8516815ba3a717b41c0f0c7a7c41d20503f3a55c65cc6e7`. Backup:
|
||||||
|
`/tmp/fifa17_profile.33068179.ORIG.20260813T001228Z.json` (same hash).
|
||||||
|
|
||||||
|
### 14.2 State change
|
||||||
|
- Narrow anchored edit of line 97070 only: ` "unopenedPackIds": [],` →
|
||||||
|
` "unopenedPackIds": [70],`. Diff vs backup = exactly one line; semantic diff =
|
||||||
|
only key `unopenedPackIds` (`[] → [70]`), all other 24 keys identical. Modified
|
||||||
|
SHA-256 `2b5760baa265f320904de2d23fd6ab374733efe74cb0756c3be39c083bce4ac8`.
|
||||||
|
(Used the direct edit rather than `grant_unopened_pack(70)` to avoid whole-file
|
||||||
|
reserialization; net semantic effect is identical.)
|
||||||
|
|
||||||
|
### 14.3 Restart
|
||||||
|
- `docker restart openfut-fut-backend` (Pid 1398009→1548312, StartedAt
|
||||||
|
2026-08-12T16:40:52Z → 2026-08-13T00:13:54Z). Bridge/Core and Rust hosts untouched.
|
||||||
|
This container bundles blaze/roster/utas/pow (per `entrypoint.sh`); all rebound.
|
||||||
|
- Post-restart in-process check: `STORE.load()['unopenedPackIds'] == [70]`;
|
||||||
|
`store_catalog(None)` → pack 70 present, sentinel 65534 absent.
|
||||||
|
|
||||||
|
### 14.4 Genuine FIFA capture
|
||||||
|
- Marker `2026-08-13T00:14:24Z`. Single request after it (unambiguous):
|
||||||
|
`00:14:58 GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true → 200`.
|
||||||
|
**No `/store/transaction`, no `/purchased/items`** in the window (pack 70 not opened).
|
||||||
|
- Full body recovered from the running code over the live [70] profile; its
|
||||||
|
`[:200]` matches the genuine logged 200-byte prefix byte-for-byte (verified MATCH).
|
||||||
|
Preserved at `docs/evidence/store_purchasegroup_capture_mypacks70_2026-08-12.json`.
|
||||||
|
|
||||||
|
### 14.5 Client-observed behavior (operator report)
|
||||||
|
1. Store remains open: **YES**.
|
||||||
|
2. "The pack you've selected is currently not available": **NO (gone)**.
|
||||||
|
3. My Packs category / unopened reward pack visible: **YES**.
|
||||||
|
4. Returned to FUT Hub: yes (normal navigation; not forced by an error dialog).
|
||||||
|
|
||||||
|
### 14.6 Response diff (baseline 2026-08-12 vs experiment)
|
||||||
|
Only difference across all 5 entries:
|
||||||
|
- **Removed:** id `65534` (`state:inactive`, `displayGroup:mypacks`, `description:""`).
|
||||||
|
- **Added:** id `70` (`state:active`, `displayGroup:mypacks`,
|
||||||
|
`description:"Reward Special Players Pack"`, `unopened:true`).
|
||||||
|
- Packs 1/5/6/7 byte-identical. Classification: **all EXPECTED FROM UNOPENED PACK
|
||||||
|
STATE; nothing UNEXPECTED.**
|
||||||
|
|
||||||
|
### 14.7 H3 assessment — **SUPPORTED**
|
||||||
|
65534 disappeared AND pack 70 replaced it as a real My Packs entry AND the
|
||||||
|
"pack not available" / store-exit behavior disappeared → per the pre-registered
|
||||||
|
criterion, **H3 is strongly SUPPORTED**. The failure is tied to the My Packs group
|
||||||
|
content when the profile owns zero unopened packs.
|
||||||
|
|
||||||
|
Sub-hypothesis resolution:
|
||||||
|
- **H3c (failure unrelated to My Packs): RULED OUT.** A My-Packs-only profile change
|
||||||
|
(no store code/config change) eliminated the failure.
|
||||||
|
- **H3a (the inactive sentinel object itself is the trigger) vs H3b (empty My Packs
|
||||||
|
state generally is the trigger): NOT DISTINGUISHED by Experiment A.** The change
|
||||||
|
simultaneously (i) removed the inactive sentinel and (ii) supplied a real active
|
||||||
|
owned pack. Either "presence of the inactive/empty sentinel" or "absence of any
|
||||||
|
real owned pack" could be the cause. Distinguishing them requires Experiment B
|
||||||
|
(empty `unopenedPackIds` AND suppress the sentinel so `mypacks` has no entry): if
|
||||||
|
that also fixes it → H3a (sentinel object was the problem); if it re-breaks or
|
||||||
|
changes → H3b (empty My Packs itself is the problem). Experiment B is a code change
|
||||||
|
(restart-gated) and remains unauthorized.
|
||||||
|
|
||||||
|
### 14.8 Rollback verification
|
||||||
|
- Profile restored from backup → SHA-256 `39bb3e83…` == original (byte-identical);
|
||||||
|
`unopenedPackIds == []`. Disk was unmutated during the test (still `2b5760ba…`
|
||||||
|
before rollback → store reads don't persist; pack 70 never opened).
|
||||||
|
- `docker restart openfut-fut-backend` (Pid 1549503, StartedAt 2026-08-13T00:16:56Z).
|
||||||
|
Post-restart in-process: `unopenedPackIds == []`, sentinel 65534 present again,
|
||||||
|
pack 70 absent → runtime baseline restored. Bridge/Core untouched.
|
||||||
|
|
||||||
|
**H3 status: SUPPORTED (H3c ruled out; H3a vs H3b open).** No permanent fix
|
||||||
|
implemented.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Experiment B — Empty My Packs Without Sentinel (EXECUTED 2026-08-13) — OBSERVED
|
||||||
|
|
||||||
|
### 15.1 Purpose
|
||||||
|
Distinguish **H3a** (the synthetic inactive sentinel 65534 itself is the trigger)
|
||||||
|
from **H3b** (FIFA cannot tolerate an empty My Packs state even without a sentinel).
|
||||||
|
Hold `unopenedPackIds == []` constant; change ONLY: sentinel 65534 emitted →
|
||||||
|
suppressed. Authorized TEMPORARY code change, reverted after test.
|
||||||
|
|
||||||
|
### 15.2 Baseline
|
||||||
|
Profile `unopenedPackIds == []` (SHA-256 `39bb3e83…`, unchanged throughout). Running
|
||||||
|
code before patch = `c89d43ea…` (host repo == container copy). Sentinel 65534 emitted.
|
||||||
|
|
||||||
|
### 15.3 Temporary patch — **TEMPORARY EXPERIMENT B PATCH, NOT A PERMANENT FIX**
|
||||||
|
Applied to the **container** copy `/app/tools/utas_server.py` only (the container
|
||||||
|
mounts `/state`, not `/app`; host repo `fifa17-recon/tools/utas_server.py` was NOT
|
||||||
|
edited — its git diff stayed empty). Single line, `store_catalog` (line 3428):
|
||||||
|
```
|
||||||
|
- if not owned_ids:
|
||||||
|
+ if False: # TEMP EXPERIMENT B PATCH -- suppress synthetic sentinel 65534 (NOT A PERMANENT FIX)
|
||||||
|
```
|
||||||
|
Semantic effect: when `unopened_packs()` is empty, append nothing (no sentinel, no
|
||||||
|
replacement object). Normal packs 1/5/6/7 (appended earlier) unchanged. Diff vs the
|
||||||
|
backed-up original = exactly this one line. Patched code SHA-256 `5bb8fca9…`.
|
||||||
|
|
||||||
|
### 15.4 Restart verification
|
||||||
|
`docker restart openfut-fut-backend` (Pid 1549503→1551026, StartedAt
|
||||||
|
2026-08-13T00:20:45Z). The writable-layer edit survived the restart; running
|
||||||
|
`/app/tools/utas_server.py` = `5bb8fca9…` (patched). In-process: `unopenedPackIds ==
|
||||||
|
[]`; `store_catalog` → 4 packs {1,5,6,7}, **no 65534, no 70, no `mypacks` entry**.
|
||||||
|
Flags unchanged (`DISPLAYGROUP=ON`, `GROUPID=OFF`). Bridge/Core/Rust untouched.
|
||||||
|
|
||||||
|
### 15.5 Genuine FIFA capture
|
||||||
|
Marker `2026-08-13T00:20:55Z`. Request sequence (operator opened the store):
|
||||||
|
`00:21:05 GET /hub` → `00:21:07 GET /user/credits` → `00:21:07 GET
|
||||||
|
/store/purchasegroup/all?ppInfo=true → 200`. **No `/store/transaction`; no further
|
||||||
|
requests** (client crashed after receiving the store body). Full body recovered from
|
||||||
|
the running patched code; `[:200]` matches the genuine logged prefix byte-for-byte
|
||||||
|
(verified MATCH). Preserved at
|
||||||
|
`docs/evidence/store_purchasegroup_capture_empty_no_sentinel_2026-08-12.json`
|
||||||
|
(4 packs {1,5,6,7}, no `mypacks` group).
|
||||||
|
|
||||||
|
### 15.6 Client behavior (operator report)
|
||||||
|
**The game CRASHED** on opening the store. Not the baseline dialog; a hard crash. No
|
||||||
|
`/store/transaction` was issued.
|
||||||
|
|
||||||
|
### 15.7 Three-way response comparison
|
||||||
|
| capture | ids present | `mypacks` group entry | client outcome |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Baseline (`…_2026-08-12.json`) | 1,5,6,7,**65534** | 65534 `inactive`, desc "" | "pack not available" dialog → Hub |
|
||||||
|
| Exp A (`…_mypacks70_…json`) | 1,5,6,7,**70** | 70 `active`, "Reward Special Players Pack" | **works** — store open, My Packs visible |
|
||||||
|
| Exp B (`…_empty_no_sentinel_…json`) | 1,5,6,7 | **none** | **CRASH** |
|
||||||
|
Normal packs {1,5,6,7} identical across all three.
|
||||||
|
|
||||||
|
### 15.8 H3a / H3b verdict
|
||||||
|
- **H3a (sentinel object itself is the trigger): CONTRADICTED.** Removing the
|
||||||
|
sentinel did NOT restore the store; it produced a *worse* outcome (crash). If the
|
||||||
|
sentinel object were the sole cause, its removal would yield a working store (it
|
||||||
|
did not).
|
||||||
|
- **H3b (FIFA cannot tolerate an empty My Packs state): SUPPORTED.** Only Exp A — a
|
||||||
|
real **active** owned pack in `mypacks` — worked. Both the inactive sentinel
|
||||||
|
(graceful "pack not available" dialog) and the total absence of any `mypacks` entry
|
||||||
|
(crash) fail. The sentinel is a **load-bearing workaround** that *downgrades* the
|
||||||
|
failure from a crash to a dialog but does not fix it.
|
||||||
|
- **Pre-registered-rule nuance:** Exp B produced a *distinct* failure (crash), which
|
||||||
|
the pre-registered rules classify as **B3 (different failure)** rather than the
|
||||||
|
exact B2 dialog. Documented as such: the crash is a THIRD failure mode. It still
|
||||||
|
resolves the question — it rules out H3a and supports H3b — but the specific
|
||||||
|
outcome (crash, not the same dialog) is stronger than B2 anticipated. Not forced
|
||||||
|
into a clean binary beyond what the evidence shows.
|
||||||
|
|
||||||
|
**Does FIFA tolerate an empty My Packs without the sentinel? NO — it crashes.**
|
||||||
|
|
||||||
|
### 15.9 Rollback verification
|
||||||
|
- Container `/app/tools/utas_server.py` restored from backup → SHA-256 `c89d43ea…`
|
||||||
|
== pre-experiment (byte-identical); the `if False:` patch fully removed.
|
||||||
|
- `docker restart openfut-fut-backend` (final Pid 1551901,
|
||||||
|
StartedAt 2026-08-13T00:22:04Z). In-process: `unopenedPackIds == []`, sentinel
|
||||||
|
65534 emitted again, pack 70 absent; `DISPLAYGROUP=ON`, `GROUPID=OFF`.
|
||||||
|
- Host repo `fifa17-recon/tools/utas_server.py` never edited (git diff empty, SHA-256
|
||||||
|
`c89d43ea…`). Profile unchanged (`39bb3e83…`). Bridge/Core/Rust untouched.
|
||||||
|
|
||||||
|
### 15.10 Likely permanent fix (REPORT ONLY — not implemented)
|
||||||
|
Evidence: the store's `mypacks` group must contain a **valid, active, openable owned
|
||||||
|
pack**; both an inactive sentinel and an absent group fail (dialog / crash). The only
|
||||||
|
working configuration observed is a genuine active owned pack (Exp A). Candidate
|
||||||
|
directions (report only, each needs design + authorization):
|
||||||
|
1. Ensure the profile always owns ≥1 legitimate active unopened pack while the store
|
||||||
|
is shown (e.g. keep a real reward pack such as id 70 granted), so `mypacks` is
|
||||||
|
never empty — this matches the only known-working state but changes economy state
|
||||||
|
and needs a lifecycle policy (what happens after the user opens it).
|
||||||
|
2. Change what `store_catalog` advertises so FIFA never lands on / requires a
|
||||||
|
`mypacks` group when there are zero owned packs (client-compatible empty-store
|
||||||
|
representation) — the correct representation is UNKNOWN; neither current option
|
||||||
|
(inactive sentinel / no group) is it, so this needs new client-side RE before
|
||||||
|
implementation.
|
||||||
|
Recommendation: do NOT simply delete the sentinel (Exp B proves that crashes). No fix
|
||||||
|
implemented.
|
||||||
|
|
||||||
|
**H3 status: SUPPORTED. H3a CONTRADICTED, H3b SUPPORTED (Exp B crash = third failure
|
||||||
|
mode; empty My Packs is the root problem). Sentinel is a load-bearing workaround.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Experiment C′ — Active Non-Openable Placeholder (EXECUTED 2026-08-13) — OBSERVED
|
||||||
|
|
||||||
|
Question: can the required `mypacks` group be kept structurally valid with an
|
||||||
|
**active** placeholder that stays impossible to open/purchase? Change exactly one
|
||||||
|
field of sentinel 65534: `state "inactive" → "active"`. Profile untouched
|
||||||
|
(`unopenedPackIds==[]`); 65534 kept absent from `PACK_CATALOG`.
|
||||||
|
|
||||||
|
### 16.1 Server-side safety proof (OBSERVED, code)
|
||||||
|
65534 cannot grant value regardless of `state` — pack resolution is by
|
||||||
|
`pack_by_id(id)` over `PACK_CATALOG` (ids 1,5,6,7,70), independent of the display
|
||||||
|
`state`:
|
||||||
|
- `store_buy` (PUT `/store/transaction`, `:3460-3465`): `pack_by_id(65534)=None` →
|
||||||
|
`if not pack: return 200, {}` (no `open_pack`, no coin change).
|
||||||
|
- `purchased_items` (POST, `:3494-3496`): `pack_by_id(65534)=None` →
|
||||||
|
`return 200, {"itemData": STORE.last_pack()}` (stale prior items only; no new
|
||||||
|
grant, no `open_pack`, no `consume_unopened_pack`).
|
||||||
|
- `open_pack`/`consume_unopened_pack` are unreachable for 65534 (pack resolves to
|
||||||
|
None first). Precondition PASSED.
|
||||||
|
|
||||||
|
### 16.2 Baseline / patch
|
||||||
|
Baseline: profile `39bb3e83…`, code `c89d43ea…` (host==container), sentinel
|
||||||
|
`inactive`/`mypacks`, 65534∉catalog, flags `DISPLAYGROUP=ON`/`GROUPID=OFF`.
|
||||||
|
Temporary container-only patch (host repo untouched), line 3444:
|
||||||
|
`empty["state"] = "inactive"` → `empty["state"] = "active"`. Diff vs original = this
|
||||||
|
one line; patched code `e1a4e1dc…`. **TEMPORARY EXPERIMENT C′ PATCH — NOT A PERMANENT
|
||||||
|
FIX.**
|
||||||
|
|
||||||
|
### 16.3 Restart / runtime
|
||||||
|
`docker restart openfut-fut-backend` (Pid 1556305, StartedAt 00:38:51Z). Running code
|
||||||
|
`e1a4e1dc…`; `unopenedPackIds==[]`; sentinel `65534 state=active unopened=False
|
||||||
|
dg=mypacks desc=""`; 65534∉catalog; normal packs 1/5/6/7 active; 70 absent; flags
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
### 16.4 Genuine FIFA capture
|
||||||
|
Marker `2026-08-13T00:38:53Z`. FIFA relaunched (Exp-B crash had closed it) → booted to
|
||||||
|
hub. Three genuine store fetches: `00:39:44`, `00:40:26`, `00:41:23`
|
||||||
|
(`GET /store/purchasegroup/all?ppInfo=true → 200`), reconstructed body prefix-matches
|
||||||
|
the logged 200-byte prefix (verified). Saved
|
||||||
|
`docs/evidence/store_purchasegroup_capture_active_placeholder_2026-08-12.json`.
|
||||||
|
C′-vs-baseline full JSON diff = **only** `65534.state: "inactive" → "active"`.
|
||||||
|
|
||||||
|
### 16.5 UI observation (operator report)
|
||||||
|
1. **No crash** (game launched to hub).
|
||||||
|
2. **No "pack not available" dialog.**
|
||||||
|
3. Store remains open.
|
||||||
|
4. My Packs tab **not shown while inside the Store (Browse Packs)**.
|
||||||
|
5. Via the FUT-hub **My Packs** menu: **one pack tile with no cover, "0 items, 0
|
||||||
|
bronze, 0 rares"** (the placeholder renders as a visible empty pack).
|
||||||
|
6. Navigation: from the hub **My Packs** menu → Bronze/Gold/Special reachable; but
|
||||||
|
from the **Browse Packs** (Store) entry, Bronze/Gold/Special are **not reachable
|
||||||
|
until My Packs is opened first**.
|
||||||
|
|
||||||
|
### 16.6 Passive request sequence (OBSERVED)
|
||||||
|
Boot: `.../accountinfo → /ut/auth → settings → phishing → match/reset → userMassInfo
|
||||||
|
→ PUT store/transaction/0 → hub …`. The single `/store/transaction` is the routine
|
||||||
|
**boot** call with body `{"state":"TRANSACTIONCANCEL"}` → `200 {}` (no packId), fired
|
||||||
|
at 00:39:39 **before** any store fetch. Across all three store opens: **no
|
||||||
|
`/store/transaction`, no `/purchased`, and no request referencing 65534.** The active
|
||||||
|
placeholder did NOT cause FIFA to auto-submit any transaction/open.
|
||||||
|
|
||||||
|
### 16.7 Availability result / verdict
|
||||||
|
**Result C1 (strong positive) — ACTIVE PLACEHOLDER HYPOTHESIS SUPPORTED, with UX
|
||||||
|
caveats.** `state` participates materially: with `state:"active"` the group exists
|
||||||
|
(no crash, as in baseline) AND the availability path is satisfied (no
|
||||||
|
"pack not available" dialog, unlike baseline). So **C2 is refuted** — `state` is a
|
||||||
|
deciding field for the dialog. But it is **not a clean permanent fix**:
|
||||||
|
- the placeholder renders as a **visible empty pack tile** ("0 items"), i.e. a fake
|
||||||
|
pack a user could try to open (server-safe: opening → no-op `{}` / stale
|
||||||
|
`last_pack`, but confusing UX);
|
||||||
|
- **navigation caveat #6**: from Browse Packs the other categories are gated behind
|
||||||
|
opening My Packs first — an UNEXPECTED behavior not present with a genuine owned
|
||||||
|
pack (Exp A).
|
||||||
|
|
||||||
|
### 16.8 Rollback verification
|
||||||
|
Container code restored from backup → `c89d43ea…` (== host, == pre-experiment); the
|
||||||
|
`state` change removed. `docker restart` (Pid 1557831, StartedAt 00:43:04Z).
|
||||||
|
In-process: `unopenedPackIds==[]`, sentinel `state=inactive`, 65534∉catalog, 70
|
||||||
|
absent, flags `DISPLAYGROUP=ON`/`GROUPID=OFF`. Host repo `utas_server.py` never edited
|
||||||
|
(`c89d43ea…`, git diff empty). Profile `39bb3e83…` unchanged. Bridge/Core untouched.
|
||||||
|
|
||||||
|
### 16.9 Implications for the client contract
|
||||||
|
`state:"active"` satisfies the pack-availability predicate (no dialog) while the
|
||||||
|
group's existence prevents the crash — so an active non-openable placeholder is the
|
||||||
|
first representation that neither crashes nor shows the dialog. However it exposes a
|
||||||
|
**visible empty "pack"** and a **Browse-Packs navigation gate** (#5/#6), so it is NOT
|
||||||
|
adopted. **Permanent fix NOT established.** Open follow-ups: (a) what happens if the
|
||||||
|
user explicitly selects/opens the active placeholder (a later controlled test —
|
||||||
|
server-safe per §16.1 but UX-unknown); (b) whether a count-gated My-Packs default or a
|
||||||
|
representation that avoids rendering a fake tile can remove the empty-tile/navigation
|
||||||
|
artifacts. Do NOT adopt `state:"active"` as the fix on this evidence alone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. Explicit Active-Placeholder Selection Test (EXECUTED 2026-08-13) — OBSERVED
|
||||||
|
|
||||||
|
**Purpose:** with the C′ active placeholder in place, characterize what happens when
|
||||||
|
the user *explicitly opens* the empty 65534 My-Packs tile (the last open backend-side
|
||||||
|
question).
|
||||||
|
|
||||||
|
**Server safety proof (re-confirmed, code `c89d43ea`):** `pack_by_id(65534)=None`;
|
||||||
|
65534∉PACK_CATALOG∉unopenedPackIds. `store_buy`→`200 {}`; `purchased_items`→
|
||||||
|
`200 {"itemData": last_pack}` (stale). No inventory/coin/profile mutation possible.
|
||||||
|
|
||||||
|
**Temporary C′ state:** container-only one-line patch `65534.state "inactive"→"active"`
|
||||||
|
(patched `e1a4e1dc…`), restart (Pid 1560774). `unopenedPackIds=[]`, 65534
|
||||||
|
active/mypacks/∉catalog, normal packs unchanged, flags ON/OFF, host code + profile
|
||||||
|
unchanged. Marker `2026-08-13T00:53:09Z`.
|
||||||
|
|
||||||
|
**Manual selection behavior (operator report):** opened FUT → My Packs → the empty
|
||||||
|
placeholder tile visible → selected/opened it ONCE:
|
||||||
|
1. Dialog: **YES**. 2. Exact text: **"This pack is no longer available"**.
|
||||||
|
3. Stays in My Packs: yes. 4. After closing the dialog → returns to My Packs.
|
||||||
|
5. Then navigates back to the FUT Hub successfully. 6. **No crash.** 7. No spinner.
|
||||||
|
8. **Navigation remains fully usable afterward.**
|
||||||
|
|
||||||
|
**Genuine request sequence (OBSERVED, marker `00:53:09Z`):** boot (`…/auth →
|
||||||
|
userMassInfo → PUT store/transaction/0 {"state":"TRANSACTIONCANCEL"}→200 {} → hub →
|
||||||
|
credits → purchasegroup`) then navigation (`hub→credits→purchasegroup` ×2 for the
|
||||||
|
store/My-Packs views). **The explicit tile selection generated NO server request** —
|
||||||
|
no `/store/transaction`, no `/purchased/items`, and NO reference to 65534 anywhere.
|
||||||
|
The "no longer available" verdict is rendered **client-side**.
|
||||||
|
|
||||||
|
**Result class: S1 — pure client-side rejection.**
|
||||||
|
|
||||||
|
**Post-test profile/economy integrity (OBSERVED):** coins 29876776, nextItemId
|
||||||
|
100004837, items 1995, purchased 0, `unopenedPackIds` `[]`, `last_pack` empty — ALL
|
||||||
|
unchanged vs pre-test; 65534 not persisted in items or unopenedPackIds; profile
|
||||||
|
SHA-256 `39bb3e83…` byte-identical. **Zero mutation.**
|
||||||
|
|
||||||
|
**UX assessment:** crash-safe ✓, economy-safe ✓ (no request even sent), navigation
|
||||||
|
recoverable ✓. Blemishes: a **visible empty "0 items" tile**, a **"This pack is no
|
||||||
|
longer available" dialog on explicit click**, and (from §16.5) the **Browse-Packs
|
||||||
|
navigation gate** (must open My Packs first). Notably this is a *strict improvement*
|
||||||
|
over the inactive-sentinel baseline, which throws "pack not available" immediately on
|
||||||
|
STORE OPEN and bounces to the Hub; the active placeholder only errors if the user
|
||||||
|
deliberately clicks the empty tile, and recovers cleanly.
|
||||||
|
|
||||||
|
**Active-placeholder verdict: MARGINALLY ACCEPTABLE.** Safe (crash + economy) and
|
||||||
|
usable, but visibly imperfect (fake tile + click-dialog + browse nav gate). Not
|
||||||
|
UNACCEPTABLE (no crash/economy risk, recoverable); not fully ACCEPTABLE (user-visible
|
||||||
|
defects).
|
||||||
|
|
||||||
|
**Permanent-fix decision: P2.** The active sentinel technically works and is safe, but
|
||||||
|
its UX is poor and — per Candidate F (CONTRADICTED) — **no backend-only *clean*
|
||||||
|
solution exists** (the store's My-Packs resolution is Scaleform/movie-driven, not
|
||||||
|
server-gated). Recommendation: keep the active placeholder as an optional/temporary
|
||||||
|
backend compatibility mode (strictly better than the current inactive-sentinel
|
||||||
|
baseline) and pursue a **client-side** fix for a fully clean zero-pack experience
|
||||||
|
(hiding the fake tile / suppressing the forced My-Packs resolution). NOT implemented.
|
||||||
|
|
||||||
|
**Rollback verification:** container code restored to `c89d43ea…` (== host, ==
|
||||||
|
pre-experiment), sentinel back to `inactive`; `docker restart` (Pid 1562665, StartedAt
|
||||||
|
00:59:04Z); `unopenedPackIds=[]`, 65534 inactive/∉catalog, 70 absent, flags ON/OFF;
|
||||||
|
host `utas_server.py` never edited; profile `39bb3e83…` unchanged. Bridge/Core
|
||||||
|
untouched.
|
||||||
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# FIFA 17 card-table provenance manifest
|
||||||
|
# Source (authoritative): 10.10.0.105:/home/alex/Documents/OpenFUT/fifa17-recon/data/tables/
|
||||||
|
# Dest (this repo): fifa17-recon/data/tables/
|
||||||
|
# Verified 2026-08-12: source and dest byte-identical (sha256), order-independent.
|
||||||
|
# Combined hash-of-hashes: 10f239add919089354d8dbff873fc9737b0a0f80f6ac41b1aa2a096c0ec8d331
|
||||||
|
# 31 fcc_*.json + 5 staff tables = 36 files. Files are DECODED tables:
|
||||||
|
# each carries {table, source, rowcount, schema[], rows[]}; card instances live in rows[].
|
||||||
|
#
|
||||||
|
0d1c9af7ae654c3e4363f18bb89bad03a0631056d36425c84b9a680fa989618c fcc_managerbonusvalues.json
|
||||||
|
0e5d5309cd1d9322476f8047fc6eaf4a88f4f19211b8ea01fe4d28ddd3733134 fcc_healingcards.json
|
||||||
|
1da80e390169ebb8ee8a6543e14b1191d9151f675797f01e23628f6c24d434c5 fcc_misccards.json
|
||||||
|
2212b0ee8d962f0fb6bd346bee35fff6566e22539e397cc2e42bb6efd4dc3ad3 fcc_textposvalues_hd.json
|
||||||
|
2a8e22ddb000b2c08f1a3e5eb47bc56ecf43f733ce6e7519498d332e4f439746 headcoachcards.json
|
||||||
|
3a323e1c0688a4068ccd21be0c9d8e88a875ab10ce2158d3aed79fc14e65f0fb fcc_chemlinkcalc.json
|
||||||
|
4a60bc4c0d8cb8d2b903e152a3dd5302348753568630818fde059b2de41f82ab fcc_leaguelogos.json
|
||||||
|
5304114078200da4564d33612c955598f12a44bdf52f18274184b98922229b8b fcc_GrandStandPlayers.json
|
||||||
|
5503291e381fee5008120615cd6d30a732b97636d4694a88f943eb14cb992741 fcc_leaguelogostickers.json
|
||||||
|
550739c124ca915fb294954afe3d9d04fb7d1faf2b0c96930b2dcdb1bfe7ac8f fcc_formationcardspositions_kc.json
|
||||||
|
5a5aabec1d40ffa21b8effb84f79e4b788cb42352aa592d71d95eba1db0d209e fcc_trainingcards.json
|
||||||
|
6476e396f166905857d2ada4f12cc37645ca42efdca121e8e74270fbf7422336 managercards.json
|
||||||
|
6546f602024973e20d04522a857c6c243473a177cab5b3be8400390651a42fab fcc_navcoords_hd.json
|
||||||
|
6b0209647383e4e940d2af2c3bbb2185a4aac7ac0e799fe6b50ae52e7625710d fcc_contractcards.json
|
||||||
|
6c52c83aafd9d9d3406e21c656762ac5cc0ba4522002f424e5593430bc5190f5 physiocards.json
|
||||||
|
765687d7f1e5c6c989adf45b174a0fdab0f65597c83132304b53b9f859c02586 fcc_stadium.json
|
||||||
|
79e50b07eecc47a0edf4d2a87782e904785e653937698cc712258a82fdf8b079 fcc_formationcardspositions_hd.json
|
||||||
|
7d36e0fbb9349eabd4267215bacbe29d78ff621deeb8cab3380dcac72c535eb9 fcc_preferredformationcalcmid.json
|
||||||
|
8122a4070901662fac97f675a3e4194dd5fd7e02194fdab204989af42676e268 fitnesscoachcards.json
|
||||||
|
828f8b90241672b9f62a9bbd3cb219a1d3bd8856bd160cc46284f2958e08f7d4 fcc_discardcoins.json
|
||||||
|
852bb82ed373881373d8610e6f4f2ca4406bac13da4bda9d6abba693d7cafc56 fcc_myclubscategories.json
|
||||||
|
974dcbbe6a46c02dc97c77df6c270c9a7f09ba23bee23005ecf95fd114ee66a7 gkcoachcards.json
|
||||||
|
9aa4b3b3f226202b21d2e9f96a1508ecce56abba64372099e090df101fce5eeb fcc_nationcalc.json
|
||||||
|
9b6797991520c05f7448b28e66d160f96afc73eefd661ed8272b0a093b6ba89f fcc_kitcards.json
|
||||||
|
9e8e6595fa8d3bcf963eb25bd9f9ea5d131aa92ed0e7e4ae3089adf5e1d55927 fcc_preferredformationcalcst.json
|
||||||
|
a4e8ac2ba0a6db45f1f59fe384fbd39a8cee8fb72a72f846e52daca44f1c7ff9 fcc_myclubs.json
|
||||||
|
bbf405e3a63b6fd03da1237b8b118764c57a40c797faf85d1e4691a1c95a840e fcc_balls.json
|
||||||
|
ca1184bd85cff3308af0104077feda4357ef483fae6335fa964922eea4e94330 fcc_navcoords_kc.json
|
||||||
|
ca3e4ab0f7892aac7473b774de4699c067c54647ca4a82cdd5fd1108c56ed894 fcc_badgecards.json
|
||||||
|
ccdda8ad0a15f73a056fa336abde8739b346d12b78cb8adbea0f0677487bb598 fcc_preferredpositioncalc.json
|
||||||
|
dbf95bddd456137e4b90a44bdd1f637458f4846ecd5c3ba6747d3f069c3f590f fcc_textposvalues_kc.json
|
||||||
|
dd8c2c860b18d999877c37e0f63dac64ef7bb57bff5a2173960cde71242f9a34 fcc_bonusvalues.json
|
||||||
|
df5b997b153941a5bb760ad5bb0fb23dc16cf8612b300559be23ac929b55eec6 fcc_leagues.json
|
||||||
|
e4619db324a7848639a8ba53f513cf3ea153eeaf698de05d71e56c319b3cc424 fcc_coinrewards.json
|
||||||
|
e6b1ca3ecb7c3923d77e73bda2e6c3f9794b9158354d566112f7979b33b4422c fcc_preferredformationcalcgk.json
|
||||||
|
f54814d61b72dd2b6186e9e414df4fbfbdbea1732da6a4622f001a1ff03bc12a fcc_preferredformationcalcback.json
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Live UTAS wire captures — 2026-08-15
|
||||||
|
|
||||||
|
Fresh sanitised FIFA 17 UTAS wire, captured during the post-P1 staging A/B on a
|
||||||
|
**real FIFA 17 client** (`10.10.0.105`) driving the isolated post-P1 candidate
|
||||||
|
backend (`10.10.0.121`, host bin `fda40d12`, Core `fbb54ea`). Production untouched.
|
||||||
|
|
||||||
|
This rebuilds the primary-capture corpus that was lost to `.gitignore` (Known Issues
|
||||||
|
#200 / "take sanitised captures on the next live FIFA run and commit them").
|
||||||
|
|
||||||
|
## Files
|
||||||
|
- `utas-requests.sanitised.txt` — 10 real client requests (`---`-separated).
|
||||||
|
- `utas-responses.sanitised.txt` — 12 responses.
|
||||||
|
|
||||||
|
Captured with `tcpdump` on the container netns (UTAS is plain HTTP on `:8099`),
|
||||||
|
reassembled + split + **sanitised** by `openfut-staging/extract_http.py`. Redacted:
|
||||||
|
`X-UT-SID`/`sid` (`<SID>`), `authCode` (`<AUTH>`), `deviceId` (`<DEV>`), `macAddress`
|
||||||
|
(`<MAC>`), and any 32+char hex token (`<TOKEN>`). The raw `.pcap` is intentionally
|
||||||
|
NOT committed (it is unsanitised).
|
||||||
|
|
||||||
|
## Route contracts captured (verbatim shapes)
|
||||||
|
|
||||||
|
### POST /openfut/account/sync (launcher control-plane, Python-served)
|
||||||
|
Request (launcher → backend):
|
||||||
|
```json
|
||||||
|
{"personaId":33068179,"personaName":"CAGE","level":1,"experience":0,
|
||||||
|
"experienceMax":1000,"accountFunds":0,"accountFundsCap":100000}
|
||||||
|
```
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{"account":{"personaId":33068179,"personaName":"CAGE","clubName":"OpenFUT",
|
||||||
|
"clubAbbr":"OFC","level":1,"experience":0,"experienceMax":1000,"accountFunds":0,
|
||||||
|
"accountFundsCap":100000,"profilePath":"accounts/33068179/fifa17_profile.json",
|
||||||
|
"coins":29826776,"unopenedPacks":0},"status":"OK"}
|
||||||
|
```
|
||||||
|
NOTE: `coins` here is Python's STALE profile value (29,826,776) — Core's authoritative
|
||||||
|
balance at capture time was 29,859,876. The launcher's `AccountSummary` parser is
|
||||||
|
lenient and requires only `{personaId,personaName,level,experience,accountFunds,coins,
|
||||||
|
unopenedPacks}` (clubName/clubAbbr/etc. ignored). BLOCKER for a Rust migration is NOT
|
||||||
|
the shape — it is that Python `account/sync` also *selects the active profile* the
|
||||||
|
still-Python-served userMassInfo envelope depends on, so it is coupled to the
|
||||||
|
userMassInfo hybrid and cannot migrate standalone.
|
||||||
|
|
||||||
|
### GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true (empty My-Packs sentinel)
|
||||||
|
The `mypacks` group carries the synthetic sentinel pack when My Packs is empty:
|
||||||
|
```json
|
||||||
|
{"assetId":65534,"displayGroup":{"priority":1,"value":"mypacks"},"id":65534,
|
||||||
|
"packType":"GOLD","packContentInfo":{"goldQuantity":0,"itemQuantity":0,...},
|
||||||
|
"state":"active","unopened":false}
|
||||||
|
```
|
||||||
|
Client renders empty My Packs AS Browse Packs (no crash) — the bug-6c resolver guard.
|
||||||
|
|
||||||
|
### GET /ut/game/fifa17/userMassInfo (RUST_OVERLAY hybrid, full 8 KB envelope captured)
|
||||||
|
The complete envelope is in `utas-responses.sanitised.txt`: root `pileSizeClientData` +
|
||||||
|
`userInfo` (with `clubName`, `clubAbbr`, `established`, `accountCreatedPlatformName`,
|
||||||
|
`currencies` [Core coins overlaid by Rust], `won/draw/loss`, `clubNameChangeAllowed`,
|
||||||
|
`divisionOffline/Online`, `purchased`, `feature`, `reliability`, `bidTokens`, `trophies`,
|
||||||
|
`sessionCoinsBankBalance`, `actives`, `squadList`). Field spec is authoritative in
|
||||||
|
`fifa17-recon/tools/utas_server.py::user_info()`. This is the target shape for a future
|
||||||
|
full-Rust userMassInfo (needs the `clubAbbr`/`established` account triad, which Core lacks).
|
||||||
|
|
||||||
|
### Other captured request lines
|
||||||
|
`POST /ut/auth`, `GET user/accountinfo`, `GET settings`, `GET phishing/trusteddevice`,
|
||||||
|
`PUT match/reset`, `GET userMassInfo`, `PUT clientdata/userHubData`,
|
||||||
|
`PUT /ut/v2/game/fifa17/store/transaction/0`.
|
||||||
|
|
||||||
|
See `openfut-staging/p1p-live-2026-08-15/AB_VERDICT.md` for the full A/B result.
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
POST /openfut/account/sync HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 131
|
||||||
|
Connection: close
|
||||||
|
|
||||||
|
{"personaId":33068179,"personaName":"CAGE","level":1,"experience":0,"experienceMax":1000,"accountFunds":0,"accountFundsCap":100000}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
GET /ut/game/fifa17/user/accountinfo HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
Connection: Close
|
||||||
|
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
Easw-Session-Data-Nucleus-Id: 33068179
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
POST /ut/auth HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
Content-Length: 380
|
||||||
|
Connection: Close
|
||||||
|
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
Hash: <TOKEN>
|
||||||
|
|
||||||
|
{"isReadOnly":false,"priorityLevel":6,"sku":"FFA17PCC","nucleusPersonaPlatform":"pc","clientVersion":3,"nuc":33068179,"nucleusPersonaId":33068179,"nucleusPersonaDisplayName":"CAGE","locale":"en-US","regionCode":"US","deviceId":"<DEV>","macAddress":"<MAC>","method":"authcode","identification":{"authCode":"<AUTH>"}}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
GET /ut/game/fifa17/settings HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
X-UT-SID: <SID>
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
GET /ut/game/fifa17/phishing/trusteddevice?deviceId=<DEV> HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
X-UT-SID: <SID>
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
PUT /ut/game/fifa17/match/reset HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
Content-Length: 0
|
||||||
|
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
X-UT-SID: <SID>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
GET /ut/game/fifa17/userMassInfo HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
X-UT-SID: <SID>
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
PUT /ut/v2/game/fifa17/store/transaction/0 HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
Content-Length: 29
|
||||||
|
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
X-UT-SID: <SID>
|
||||||
|
|
||||||
|
{"state":"TRANSACTIONCANCEL"}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
PUT /ut/game/fifa17/clientdata/userHubData HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
Content-Length: 53
|
||||||
|
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
X-UT-SID: <SID>
|
||||||
|
|
||||||
|
{"entries":[{"key":0,"value":0},{"key":1,"value":1}]}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
GET /ut/game/fifa17/store/purchasegroup/all?ppInfo=true HTTP/1.1
|
||||||
|
Host: 10.10.0.121:8099
|
||||||
|
User-Agent: ProtoHttp 1.3/DS 15.1.2.1.0 (Windows)
|
||||||
|
Accept: application/json
|
||||||
|
Content-Type: application/json
|
||||||
|
X-UT-SID: <SID>
|
||||||
|
Accept-Encoding: gzip
|
||||||
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"_countdown_proof": {
|
||||||
|
"delta": -4,
|
||||||
|
"expires_after_4s": 2216,
|
||||||
|
"expires_first": 2220,
|
||||||
|
"frozen": false
|
||||||
|
},
|
||||||
|
"auctionhouse-browse": {
|
||||||
|
"container": "object",
|
||||||
|
"path": "/auctionhouse",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"marketdata-pricelimits": {
|
||||||
|
"container": "array",
|
||||||
|
"path": "/marketdata/pricelimits?defId=20801",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade-status-filtered": {
|
||||||
|
"container": "object",
|
||||||
|
"path": "/trade/status?tradeIds=1000000097",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade-status-unfiltered": {
|
||||||
|
"container": "object",
|
||||||
|
"path": "/trade/status",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"container": "object",
|
||||||
|
"path": "/tradePile",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile-counts": {
|
||||||
|
"container": "object",
|
||||||
|
"path": "/tradePile/counts",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 2220,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 20801,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 20801,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 20801,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000097,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 38,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 94,
|
||||||
|
"resourceId": 20801,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 1000000097,
|
||||||
|
"tradeState": "active",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 0,
|
||||||
|
"credits": 29843976,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 1,
|
||||||
|
"sold": 0,
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
image_base=0x180000000 sections=7
|
||||||
|
.text rva=0x00001000 vsize=0x1e3f62 rawptr=0x00000400 rawsize=0x1e4000
|
||||||
|
.rdata rva=0x001e5000 vsize=0xa4094 rawptr=0x001e4400 rawsize=0xa4200
|
||||||
|
.data rva=0x0028a000 vsize=0x65c08 rawptr=0x00288600 rawsize=0x54000
|
||||||
|
.pdata rva=0x002f0000 vsize=0x132cc rawptr=0x002dc600 rawsize=0x13400
|
||||||
|
.tls rva=0x00304000 vsize=0x9 rawptr=0x002efa00 rawsize=0x200
|
||||||
|
.rsrc rva=0x00305000 vsize=0x4e8 rawptr=0x002efc00 rawsize=0x600
|
||||||
|
.reloc rva=0x00306000 vsize=0x16bc4 rawptr=0x002f0200 rawsize=0x16c00
|
||||||
|
|
||||||
|
route table VA 0x18021df80 -> file offset 0x21d380
|
||||||
|
|
||||||
|
# field A field B
|
||||||
|
0 'ut/%s/auctionhouse' 'AUCTIONHOUSE'
|
||||||
|
1 'ut/%s/clubUser' 'CLUB_USER'
|
||||||
|
2 'ut/%s/user/list' 'CLUB_INFO'
|
||||||
|
3 'ut/%s/club' 'CLUB'
|
||||||
|
4 'ut/%s/defid' 'DREAM'
|
||||||
|
5 'ut/%s/squad' 'SQUAD'
|
||||||
|
6 'ut/delete/%s/squad' 'DELETE_SQUAD'
|
||||||
|
7 'ut/%s/leaderboards/options' 'LBOPTIONS'
|
||||||
|
8 'ut/%s/leaderboards' 'LBDEFAULT'
|
||||||
|
9 'ut/%s/activeMessage' 'PAFPRACTICE'
|
||||||
|
10 'ut/%s' 'UT'
|
||||||
|
11 'ut/%s/user' 'USER'
|
||||||
|
12 'ut/delete/%s/user' 'DELETEUSER'
|
||||||
|
13 'ut/%s/item' 'ITEMS'
|
||||||
|
14 'ut/%s/item/resource' 'ITEMS_BY_RES'
|
||||||
|
15 'ut/delete/%s/item' 'DELETEITEMS'
|
||||||
|
16 'ut/%s/match' 'MATCH'
|
||||||
|
17 'ut/%s/sbs' 'SBC'
|
||||||
|
18 'ut/%s/tournament' 'TOURNAMENT'
|
||||||
|
19 'ut/%s/tournament/user' 'TOURNAMENTUSER'
|
||||||
|
20 'ut/delete/%s/tournament/user' 'TOURNAMENTQUIT'
|
||||||
|
21 'ut/%s/season' 'SEASON'
|
||||||
|
22 'ut/%s/season/user' 'SEASONUSER'
|
||||||
|
23 'ut/%s/season/%%s/user' 'SEASONUSER_ALTER'
|
||||||
|
24 'ut/%s/season/%%s/reset' 'SEASONRESET'
|
||||||
|
25 'ut/%s/season/friendly' 'FRIENDLYSEASON'
|
||||||
|
26 'ut/%s/purchased' 'PURCHASED'
|
||||||
|
27 'ut/%s/store' 'STORE'
|
||||||
|
28 'ut/%s/watchList' 'WATCHLIST'
|
||||||
|
29 'ut/delete/%s/watchList' 'DELETEWATCHLIST'
|
||||||
|
30 'ut/%s/tradePile' 'TRADEPILE'
|
||||||
|
31 'ut/%s/trade' 'TRADE'
|
||||||
|
32 'ut/delete/%s/trade' 'DELETETRADE'
|
||||||
|
33 'ut/%s/marketdata' 'MARKETDATA'
|
||||||
|
34 'ut/%s/clientdata' 'CLIENTDATA'
|
||||||
|
35 'ut/auth' 'AUTH'
|
||||||
|
36 'ut/delete/auth' 'DELETE_AUTH'
|
||||||
|
37 'ut/%s/phishing' 'PHISHING'
|
||||||
|
38 'ut/%s/captcha' 'CAPTCHA'
|
||||||
|
39 'ut/%s/tfa' 'TFA'
|
||||||
|
40 'ut/%s/squad/mode' 'SQUADMODE'
|
||||||
|
41 'ut/%s/draft/mode' 'DRAFT'
|
||||||
|
42 'ut/%s/champion' 'CHAMPIONS'
|
||||||
|
43 'ut/v2/%s/store' 'V2STORE'
|
||||||
|
44 'ut/%s/livemessage' 'LIVEMESSAGE'
|
||||||
|
45 '' 'ADMIN'
|
||||||
|
46 '' 'DEBUG'
|
||||||
|
47 '' 'MAINTENANCE'
|
||||||
|
-- table ends after 48 rows --
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
|
||||||
|
=== tradeState (table walk) VA 0x180229e40 -> off 0x229240 ===
|
||||||
|
'active' = 1
|
||||||
|
'inactive' = 2
|
||||||
|
'expired' = 3
|
||||||
|
'closed' = 4
|
||||||
|
-- NULL terminator after 4 rows --
|
||||||
|
|
||||||
|
=== type VA 0x180229c30 -> off 0x229030 ===
|
||||||
|
'any' = -1
|
||||||
|
'player' = 1
|
||||||
|
'staff' = 2
|
||||||
|
'clubInfo' = 3
|
||||||
|
'training' = 4
|
||||||
|
'development' = 5
|
||||||
|
'stadium' = 6
|
||||||
|
'ball' = 7
|
||||||
|
-- NULL terminator after 8 rows --
|
||||||
|
|
||||||
|
=== zone VA 0x1802296e0 -> off 0x228ae0 ===
|
||||||
|
'goalKeeper' = 0
|
||||||
|
'defense' = 1
|
||||||
|
'midfield' = 2
|
||||||
|
'attacker' = 3
|
||||||
|
-- NULL terminator after 4 rows --
|
||||||
|
|
||||||
|
=== lev VA 0x180229a60 -> off 0x228e60 ===
|
||||||
|
'any' = 0
|
||||||
|
'gold' = 3
|
||||||
|
'silver' = 2
|
||||||
|
'bronze' = 1
|
||||||
|
-- NULL terminator after 4 rows --
|
||||||
|
|
||||||
|
=== pos VA 0x1802295c0 -> off 0x2289c0 ===
|
||||||
|
'GK' = 0
|
||||||
|
'RWB' = 2
|
||||||
|
'RB' = 3
|
||||||
|
'CB' = 5
|
||||||
|
'LB' = 7
|
||||||
|
'LWB' = 8
|
||||||
|
'CDM' = 10
|
||||||
|
'RM' = 12
|
||||||
|
'CM' = 14
|
||||||
|
'LM' = 16
|
||||||
|
'CAM' = 18
|
||||||
|
'RF' = 20
|
||||||
|
'CF' = 21
|
||||||
|
'LF' = 22
|
||||||
|
'RW' = 23
|
||||||
|
'ST' = 25
|
||||||
|
'LW' = 27
|
||||||
|
-- NULL terminator after 17 rows --
|
||||||
|
|
||||||
|
=== cat VA 0x180229ab0 -> off 0x228eb0 ===
|
||||||
|
'any' = -1
|
||||||
|
'playerGK' = 1
|
||||||
|
'playerRD' = 2
|
||||||
|
'playerRM' = 3
|
||||||
|
'playerRA' = 4
|
||||||
|
'manager' = 5
|
||||||
|
'headCoach' = 6
|
||||||
|
'GKCoach' = 7
|
||||||
|
'fitnessCoach' = 8
|
||||||
|
'physio' = 9
|
||||||
|
'badge' = 10
|
||||||
|
'kit' = 11
|
||||||
|
'leagueLogo' = 12
|
||||||
|
'playerTraining' = 13
|
||||||
|
'GKTraining' = 14
|
||||||
|
'position' = 15
|
||||||
|
'playStyle' = 16
|
||||||
|
'managerLeagueModifier' = 17
|
||||||
|
'contract' = 18
|
||||||
|
'fitness' = 19
|
||||||
|
'healing' = 20
|
||||||
|
'stadium' = 21
|
||||||
|
'ball' = 22
|
||||||
|
-- NULL terminator after 23 rows --
|
||||||
|
|
||||||
|
=== form VA 0x180229880 -> off 0x228c80 ===
|
||||||
|
'f3412' = 23
|
||||||
|
'f3421' = 24
|
||||||
|
'f343' = 25
|
||||||
|
'f352' = 27
|
||||||
|
'f41212' = 14
|
||||||
|
'f4231' = 3
|
||||||
|
'f4222' = 13
|
||||||
|
'f4312' = 6
|
||||||
|
'f4321' = 7
|
||||||
|
'f433' = 8
|
||||||
|
'f4411' = 19
|
||||||
|
'f442' = 16
|
||||||
|
'f451' = 21
|
||||||
|
'f5212' = 29
|
||||||
|
'f5221' = 30
|
||||||
|
'f532' = 31
|
||||||
|
'f541' = 32
|
||||||
|
'f41212a' = 15
|
||||||
|
'f4141' = 2
|
||||||
|
'f4231a' = 4
|
||||||
|
'f433a' = 9
|
||||||
|
'f433b' = 10
|
||||||
|
'f433c' = 11
|
||||||
|
'f433d' = 12
|
||||||
|
'f442a' = 17
|
||||||
|
'f451a' = 20
|
||||||
|
-- NULL terminator after 26 rows --
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"gap": 1,
|
||||||
|
"test": "ACTIVE seller row under itemState=forSale",
|
||||||
|
"date": "2026-08-17",
|
||||||
|
"disposable_card": {
|
||||||
|
"wire_item_id": 100000178,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"rating": 75,
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"copies_owned": 3,
|
||||||
|
"discardValue": 600,
|
||||||
|
"in_active_squad": false,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"pre_state": {
|
||||||
|
"club_items": 1966,
|
||||||
|
"coins": 29843976,
|
||||||
|
"tradePile_rows": 0,
|
||||||
|
"tradePile_counts": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"listing_creation_route_log": "utas-host owner=RUST route=market-list POST item_id=100000178 listed=true trade_id=1000000178",
|
||||||
|
"client_flow_observed_rust_owned": [
|
||||||
|
"PUT /ut/game/fifa17/item (club -> trade pile move)",
|
||||||
|
"POST /ut/game/fifa17/auctionhouse (ISStart) -> 200",
|
||||||
|
"GET /ut/game/fifa17/tradePile (repeated)",
|
||||||
|
"GET /ut/game/fifa17/trade/status?tradeIds=1000000178 -> requested=1 returned=1 (repeated poll)",
|
||||||
|
"GET /ut/game/fifa17/tradePile/counts"
|
||||||
|
],
|
||||||
|
"wire_active_row": {
|
||||||
|
"tradeId": 1000000178,
|
||||||
|
"tradeState": "active",
|
||||||
|
"itemState": "forSale",
|
||||||
|
"startingBid": 150,
|
||||||
|
"buyNowPrice": 200,
|
||||||
|
"currentBid": 0,
|
||||||
|
"bidState": "none",
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"watched": false,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"atom_count": 12,
|
||||||
|
"itemData_attributeList_entries": 6
|
||||||
|
},
|
||||||
|
"countdown_sample": {
|
||||||
|
"expires": [
|
||||||
|
3562,
|
||||||
|
3556
|
||||||
|
],
|
||||||
|
"delta_secs": 6,
|
||||||
|
"live": true
|
||||||
|
},
|
||||||
|
"trade_status_coherent_while_running": true,
|
||||||
|
"trade_status_rows": [
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 200,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 3556,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 24
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 62
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 212188,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 212188,
|
||||||
|
"discardValue": 600,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 19,
|
||||||
|
"nation": 21,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"teamid": 112172,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 1000000178,
|
||||||
|
"tradeState": "active",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tradePile_counts_while_active": {
|
||||||
|
"count": 1,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 1,
|
||||||
|
"sold": 0
|
||||||
|
},
|
||||||
|
"coins_unchanged": true,
|
||||||
|
"fix_a_inactive_rows": 0,
|
||||||
|
"CARD_OFFERSTATE_live_decode": {
|
||||||
|
"observed": true,
|
||||||
|
"method": "read-only /proc/<pid>/mem via fifa17-recon-derived auction_record_probe.py; slide proven by FNV control MATCH before any other read; O_RDONLY + pread only",
|
||||||
|
"client_pid": 29670,
|
||||||
|
"container": "model->vt[0x120] (trade pile), rows=1, +0x60 override=1",
|
||||||
|
"record": {
|
||||||
|
"tradeId": 1000000178,
|
||||||
|
"STATE_tradeState": "1(active)",
|
||||||
|
"YOURBID_bidState": "0(none)",
|
||||||
|
"MAX_CREDITS_buyNowPrice": 200,
|
||||||
|
"RESERVEDPRICE_startingBid": 150,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 3457,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"IS_GLOW_flagB": 0,
|
||||||
|
"INBOX_flagA": 0,
|
||||||
|
"IS_WATCHED": 0
|
||||||
|
},
|
||||||
|
"item": {
|
||||||
|
"CARD_ID": 212188,
|
||||||
|
"CARD_TYPE": 1,
|
||||||
|
"tradeable": 1,
|
||||||
|
"rating": 75,
|
||||||
|
"CARD_OFFERSTATE_itemState": "5(forSale)",
|
||||||
|
"pile": 0
|
||||||
|
},
|
||||||
|
"ab_significance": "This is the direct A/B on the only changed field. Baseline `listFS` decoded to -1(<unrecognised>) in the client's own record; candidate `forSale` decodes to 5. Every other field in the record is unchanged in kind.",
|
||||||
|
"claim_boundary": "Proves the CLIENT DECODES the token correctly. Says nothing about the Flash action-gate term."
|
||||||
|
},
|
||||||
|
"ui_acceptance": {
|
||||||
|
"operator_report": "looks right",
|
||||||
|
"confirmed": [
|
||||||
|
"card renders under LISTED ITEMS (not the TRANSFER LIST section)",
|
||||||
|
"Start Price 150 and Buy Now 200 displayed correctly, Current Bid as expected",
|
||||||
|
"Time Remaining visibly counting down",
|
||||||
|
"card art renders normally; no malformed row, no duplicate, no premature Expired, no crash",
|
||||||
|
"row correctly non-actionable while active (established FIFA 17 behaviour, not a fault)"
|
||||||
|
],
|
||||||
|
"corroboration": "the client's trade-pile container populated with exactly this row (rows=1), so the Transfer List screen bound and drew it"
|
||||||
|
},
|
||||||
|
"verdict": "GAP 1 CLOSED \u2014 LIVE-CONFIRMED: an active seller auction with itemState=forSale renders correctly and is decoded by the client as CARD_OFFERSTATE=5",
|
||||||
|
"code_change_required": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{"t": 1787012091, "present": true, "tradeState": "active", "expires": 3175, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012151, "present": true, "tradeState": "active", "expires": 3115, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012211, "present": true, "tradeState": "active", "expires": 3055, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012271, "present": true, "tradeState": "active", "expires": 2995, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012331, "present": true, "tradeState": "active", "expires": 2935, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012391, "present": true, "tradeState": "active", "expires": 2875, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012451, "present": true, "tradeState": "active", "expires": 2815, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012511, "present": true, "tradeState": "active", "expires": 2755, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012571, "present": true, "tradeState": "active", "expires": 2695, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012631, "present": true, "tradeState": "active", "expires": 2635, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012691, "present": true, "tradeState": "active", "expires": 2575, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012751, "present": true, "tradeState": "active", "expires": 2515, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012811, "present": true, "tradeState": "active", "expires": 2455, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012871, "present": true, "tradeState": "active", "expires": 2395, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012931, "present": true, "tradeState": "active", "expires": 2335, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787012991, "present": true, "tradeState": "active", "expires": 2275, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013051, "present": true, "tradeState": "active", "expires": 2215, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013111, "present": true, "tradeState": "active", "expires": 2155, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013171, "present": true, "tradeState": "active", "expires": 2095, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013231, "present": true, "tradeState": "active", "expires": 2035, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013291, "present": true, "tradeState": "active", "expires": 1975, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013351, "present": true, "tradeState": "active", "expires": 1915, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013411, "present": true, "tradeState": "active", "expires": 1855, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013471, "present": true, "tradeState": "active", "expires": 1795, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013531, "present": true, "tradeState": "active", "expires": 1735, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013591, "present": true, "tradeState": "active", "expires": 1675, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013651, "present": true, "tradeState": "active", "expires": 1615, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013711, "present": true, "tradeState": "active", "expires": 1555, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013771, "present": true, "tradeState": "active", "expires": 1495, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013831, "present": true, "tradeState": "active", "expires": 1435, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013891, "present": true, "tradeState": "active", "expires": 1375, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787013951, "present": true, "tradeState": "active", "expires": 1315, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014011, "present": true, "tradeState": "active", "expires": 1255, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014071, "present": true, "tradeState": "active", "expires": 1195, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014131, "present": true, "tradeState": "active", "expires": 1135, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014191, "present": true, "tradeState": "active", "expires": 1075, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014251, "present": true, "tradeState": "active", "expires": 1015, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014311, "present": true, "tradeState": "active", "expires": 955, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014371, "present": true, "tradeState": "active", "expires": 895, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014431, "present": true, "tradeState": "active", "expires": 835, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014491, "present": true, "tradeState": "active", "expires": 775, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014551, "present": true, "tradeState": "active", "expires": 715, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014611, "present": true, "tradeState": "active", "expires": 655, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014671, "present": true, "tradeState": "active", "expires": 595, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014731, "present": true, "tradeState": "active", "expires": 535, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014791, "present": true, "tradeState": "active", "expires": 475, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014851, "present": true, "tradeState": "active", "expires": 415, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014911, "present": true, "tradeState": "active", "expires": 355, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787014971, "present": true, "tradeState": "active", "expires": 295, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787015031, "present": true, "tradeState": "active", "expires": 235, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787015091, "present": true, "tradeState": "active", "expires": 175, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787015151, "present": true, "tradeState": "active", "expires": 115, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787015211, "present": true, "tradeState": "active", "expires": 55, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787015271, "present": true, "tradeState": "expired", "expires": 0, "itemState": "forSale", "startingBid": 150, "buyNowPrice": 200, "total": 1, "credits": 29843976}
|
||||||
|
{"t": 1787015301, "present": false, "tradeState": null, "expires": null, "itemState": null, "startingBid": null, "buyNowPrice": null, "total": 0, "credits": 29843976}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"gap": 2,
|
||||||
|
"test": "expired -> Return to Club -> full exit/re-entry persistence",
|
||||||
|
"date": "2026-08-17",
|
||||||
|
"item": {
|
||||||
|
"wire_item_id": 100000178,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"rating": 75,
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"tradeId": 1000000178
|
||||||
|
},
|
||||||
|
"natural_expiry": {
|
||||||
|
"method": "read-only sampler polling /tradePile once a minute; expiry is derived from created_at + duration, so it was OBSERVED, never forced. No timestamp was mutated at any point.",
|
||||||
|
"samples": 55,
|
||||||
|
"transitions": [
|
||||||
|
{
|
||||||
|
"t": 1787012091,
|
||||||
|
"tradeState": "active",
|
||||||
|
"expires": 3175,
|
||||||
|
"itemState": "forSale"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1787015271,
|
||||||
|
"tradeState": "expired",
|
||||||
|
"expires": 0,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"note": "natural expiry reached; itemState stayed forSale through the transition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"t": 1787015301,
|
||||||
|
"tradeState": null,
|
||||||
|
"total": 0,
|
||||||
|
"note": "row left the pile entirely \u2014 the operator's Return to Club"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auction_duration_secs": 3600
|
||||||
|
},
|
||||||
|
"return_to_club_server_state": {
|
||||||
|
"tradePile_total": 0,
|
||||||
|
"tradePile_rows": 0,
|
||||||
|
"tradePile_counts": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 0
|
||||||
|
},
|
||||||
|
"trade_status": {
|
||||||
|
"tradeId": 1000000178,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"expires": 0
|
||||||
|
},
|
||||||
|
"club_items": 1966,
|
||||||
|
"club_row": {
|
||||||
|
"id": 100000178,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"rating": 75,
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"itemState": "free",
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"duplicate_owned_ids": 0,
|
||||||
|
"market_store": {
|
||||||
|
"listing_1000000178": "cancelled",
|
||||||
|
"active": 0,
|
||||||
|
"reserved": 0,
|
||||||
|
"all_by_state": {
|
||||||
|
"cancelled": 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"coins": 29843976,
|
||||||
|
"fix_a_inactive_rows": 0,
|
||||||
|
"checks_passed": "18/18"
|
||||||
|
},
|
||||||
|
"status": "GAP 2 CLOSED \u2014 Return to Club is durable across repeated full FUT exit/re-entry",
|
||||||
|
"exit_reentry": {
|
||||||
|
"observed": true,
|
||||||
|
"operator_report": "yes that card is still there",
|
||||||
|
"host_log_evidence": [
|
||||||
|
"route=move-items wire=100000178 pile=club auction_cancelled=1",
|
||||||
|
"route=auth-delete",
|
||||||
|
"route=auth ... ip=10.10.0.105 persona=33068179 sid_opened=true (fresh session)",
|
||||||
|
"route=hub clubPlayers=1966 auctionCount=0",
|
||||||
|
"route=club total=1986 emitted=1966 dropped_no_asset=0",
|
||||||
|
"the full auth -> userMassInfo -> hub -> club -> tradePile -> trade/status cycle repeated for a SECOND fresh session"
|
||||||
|
],
|
||||||
|
"session_boundaries_crossed": 2,
|
||||||
|
"server_checks_after_reentry": "18/18, identical to before the re-entry"
|
||||||
|
},
|
||||||
|
"code_change_required": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"auctionCount": 1,
|
||||||
|
"clubPlayers": 1965,
|
||||||
|
"tradePile": {
|
||||||
|
"count": 1,
|
||||||
|
"selling": 1,
|
||||||
|
"sold": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-08-17",
|
||||||
|
"change": "removed tradeState:'inactive' projection from /tradePile and /trade/status",
|
||||||
|
"why": "RE of the FUT front-end: the action gate is in the APT/ActionScript layer; only rows classified as auctions (getCardsInAuction/isInActiveAuction) reach PreCheckCardOptions -> handleTradeCardAction, so an 'inactive' row renders and can never be acted on. The corpus already said 'do not emit it' (plan-2026-08-06-transfer-market.md:731-733).",
|
||||||
|
"before": {
|
||||||
|
"total": 7,
|
||||||
|
"by_state": {
|
||||||
|
"expired": 1,
|
||||||
|
"inactive": 6
|
||||||
|
},
|
||||||
|
"credits": 29843976,
|
||||||
|
"trade_status_returned": 7
|
||||||
|
},
|
||||||
|
"after": {
|
||||||
|
"total": 1,
|
||||||
|
"by_state": {
|
||||||
|
"expired": 1
|
||||||
|
},
|
||||||
|
"credits": 29843976,
|
||||||
|
"trade_status_returned": 1
|
||||||
|
},
|
||||||
|
"club_invariant": {
|
||||||
|
"formerly_inactive_members_visible_in_club": 6,
|
||||||
|
"of": 6,
|
||||||
|
"stranded": 0,
|
||||||
|
"club_items": 1965
|
||||||
|
},
|
||||||
|
"not_fixed_here": {
|
||||||
|
"itemState": "listFS",
|
||||||
|
"finding": "0 occurrences in CardsDLL (md5 4de3493131d7d2ff7f8b360c5ac9b655) and 0 in 4.26 GiB of process memory; decodes to -1. FIFA 17 value is forSale (5).",
|
||||||
|
"reason_deferred": "CARD_OFFERSTATE is one of three unresolved action-gate candidates; every actionable row observed carried itemState -1, so changing it could perturb the working expired -> Return-to-Club flow. Ships alone with its own live A/B."
|
||||||
|
},
|
||||||
|
"gate_status": "Category C, STRONGLY SUPPORTED. Gate term NOT PROVEN: STATE, CARD_OFFERSTATE and RESERVEDPRICE/MAX_CREDITS all differ together in every observable row. Proof needs AVM1 disassembly of tradepile.isInActiveAuction."
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
{
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 2674,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 61
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 158023,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000155,
|
||||||
|
"itemState": "listFS",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 1000000155,
|
||||||
|
"tradeState": "active",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 0,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 61
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 158023,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000059,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 0,
|
||||||
|
"tradeId": 1000000059,
|
||||||
|
"tradeState": "inactive",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 29843976,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-08-17",
|
||||||
|
"change": "itemData.itemState on the seller's own auction rows: listFS -> forSale",
|
||||||
|
"sites": [
|
||||||
|
"openfut-utas-host/src/market.rs handle_market_query (GET /tradePile)",
|
||||||
|
"openfut-utas-host/src/market.rs handle_market_status (GET /trade/status)"
|
||||||
|
],
|
||||||
|
"why": {
|
||||||
|
"listFS": "not a FIFA 17 token: 0 occurrences in CardsDLL_Win64_retail.dll (md5 4de3493131d7d2ff7f8b360c5ac9b655), 0 in 4.26 GiB of live client memory, decodes to -1 via FUN_180166660",
|
||||||
|
"forSale": "FIFA 17 native, value 5, from the 12-row itemState table at 0x180229cc0",
|
||||||
|
"oracle": "the Python oracle emits listFS (utas_server.py:3921); the differential asserted PARITY and so passed while both sides were wrong"
|
||||||
|
},
|
||||||
|
"supporting_evidence_the_token_is_safe": "the market SEARCH route (/transfermarket) already emitted forSale before this change, so the client has been decoding forSale=5 on a live path all along",
|
||||||
|
"wire_ab": {
|
||||||
|
"baseline": {
|
||||||
|
"tradeId": 1000000155,
|
||||||
|
"tradeState": "expired",
|
||||||
|
"bidState": "none",
|
||||||
|
"expires": 0,
|
||||||
|
"startingBid": 150,
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"currentBid": 0,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"watched": false,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"atom_count": 12,
|
||||||
|
"itemState": "listFS",
|
||||||
|
"item_id": 100000155,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"untradeable": false,
|
||||||
|
"rating": 93,
|
||||||
|
"preferredPosition": "RW"
|
||||||
|
},
|
||||||
|
"candidate": {
|
||||||
|
"tradeId": 1000000155,
|
||||||
|
"tradeState": "expired",
|
||||||
|
"bidState": "none",
|
||||||
|
"expires": 0,
|
||||||
|
"startingBid": 150,
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"currentBid": 0,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"watched": false,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"atom_count": 12,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"item_id": 100000155,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"untradeable": false,
|
||||||
|
"rating": 93,
|
||||||
|
"preferredPosition": "RW"
|
||||||
|
},
|
||||||
|
"fields_changed": [
|
||||||
|
"itemState"
|
||||||
|
],
|
||||||
|
"fields_identical": 17,
|
||||||
|
"credits_before": 29843976,
|
||||||
|
"credits_after": 29843976,
|
||||||
|
"fix_a_inactive_rows_before": 0,
|
||||||
|
"fix_a_inactive_rows_after": 0
|
||||||
|
},
|
||||||
|
"offline_gates": {
|
||||||
|
"host_tests": "126 pass",
|
||||||
|
"adapter_tests": "214 pass",
|
||||||
|
"fmt": "clean",
|
||||||
|
"clippy": "clean",
|
||||||
|
"differential": "relabelled market-query tradePile PARITY -> DIFFERENT-BY-DESIGN; now pins oracle=listFS and rust=forSale so the divergence stays visible"
|
||||||
|
},
|
||||||
|
"status": "PROMOTED \u2014 live-confirmed on the expired path",
|
||||||
|
"live_status": {
|
||||||
|
"session": "operator FIFA 17 session, 2026-08-17, against the already-expired row 1000000155 (res 158023, 93 RW)",
|
||||||
|
"expired_row_actionable": true,
|
||||||
|
"return_to_club_offered": true,
|
||||||
|
"return_to_club_succeeded": true,
|
||||||
|
"operator_report": "the card is back in my club",
|
||||||
|
"server_verified_after": {
|
||||||
|
"tradePile_total": 0,
|
||||||
|
"tradePile_rows": 0,
|
||||||
|
"tradePile_counts": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 0
|
||||||
|
},
|
||||||
|
"trade_status_1000000155": "closed",
|
||||||
|
"market_store_state": "cancelled",
|
||||||
|
"club_items": 1966,
|
||||||
|
"club_items_before": 1965,
|
||||||
|
"card_in_club": {
|
||||||
|
"id": 100000155,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"rating": 93,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"itemState": "free",
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"duplicate_ids_in_club": 0,
|
||||||
|
"stale_active_listings": 0,
|
||||||
|
"inactive_rows": 0
|
||||||
|
},
|
||||||
|
"active_row_render_with_forSale": "NOT OBSERVED \u2014 no active listing existed during the session",
|
||||||
|
"full_exit_reentry_ui": "NOT DIRECTLY OBSERVED \u2014 durability established from the server state a fresh session reads (empty tradePile + card in club)"
|
||||||
|
},
|
||||||
|
"revert": "single-line: set both auction_record_as(l, \"forSale\") calls in market.rs back to \"listFS\", rebuild, hub restart prod-host",
|
||||||
|
"claim_boundary": "This validates PROTOCOL CORRECTNESS of the token only. It does NOT prove the Flash action-gate term; STATE / CARD_OFFERSTATE / RESERVEDPRICE+MAX_CREDITS remain confounded and the gate stays Category C / STRONGLY SUPPORTED / not proven.",
|
||||||
|
"narrows": "CARD_OFFERSTATE is not a gate term requiring -1: an expired row decoding 5 stayed actionable. STATE and RESERVEDPRICE/MAX_CREDITS remain confounded; the Flash gate is still Category C / not proven."
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"defId": 20801,
|
||||||
|
"maxPrice": 15000,
|
||||||
|
"minPrice": 150
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
{
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 1474,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 61
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 158023,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000155,
|
||||||
|
"itemState": "listFS",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 1000000155,
|
||||||
|
"tradeState": "active",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 0,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 172610,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 47
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 57
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 76
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 172610,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 172610,
|
||||||
|
"discardValue": 900,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100004867,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 31,
|
||||||
|
"nation": 54,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 80,
|
||||||
|
"resourceId": 172610,
|
||||||
|
"teamid": 54,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 0,
|
||||||
|
"tradeId": 1000004867,
|
||||||
|
"tradeState": "inactive",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 0,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 163824,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 43
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 56
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 82
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 163824,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 163824,
|
||||||
|
"discardValue": 900,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100004866,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 13,
|
||||||
|
"nation": 50,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 83,
|
||||||
|
"resourceId": 163824,
|
||||||
|
"teamid": 7,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 0,
|
||||||
|
"tradeId": 1000004866,
|
||||||
|
"tradeState": "inactive",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 0,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 177003,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 66
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 177003,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 177003,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000122,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 10,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 89,
|
||||||
|
"resourceId": 177003,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 0,
|
||||||
|
"tradeId": 1000000122,
|
||||||
|
"tradeState": "inactive",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 0,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 177003,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 66
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 177003,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 177003,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000127,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 10,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 89,
|
||||||
|
"resourceId": 177003,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 0,
|
||||||
|
"tradeId": 1000000127,
|
||||||
|
"tradeState": "inactive",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 0,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 61
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 158023,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000059,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 0,
|
||||||
|
"tradeId": 1000000059,
|
||||||
|
"tradeState": "inactive",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 0,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 61
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 158023,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000079,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 0,
|
||||||
|
"tradeId": 1000000079,
|
||||||
|
"tradeState": "inactive",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 29843976,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 7
|
||||||
|
}
|
||||||
+190
@@ -0,0 +1,190 @@
|
|||||||
|
{
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 20801,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 20801,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 20801,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000097,
|
||||||
|
"itemState": "listFS",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 38,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 94,
|
||||||
|
"resourceId": 20801,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 1000000097,
|
||||||
|
"tradeState": "expired",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 2996,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 61
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 158023,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000155,
|
||||||
|
"itemState": "listFS",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 1000000155,
|
||||||
|
"tradeState": "active",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 0,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 61
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 158023,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000059,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 0,
|
||||||
|
"tradeId": 1000000059,
|
||||||
|
"tradeState": "inactive",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 29843976,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 3
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 2220,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 20801,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 20801,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 20801,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000097,
|
||||||
|
"itemState": "listFS",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 38,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 94,
|
||||||
|
"resourceId": 20801,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 1000000097,
|
||||||
|
"tradeState": "active",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 29843976
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 1199,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 20801,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 20801,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 20801,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000097,
|
||||||
|
"itemState": "listFS",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 38,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 94,
|
||||||
|
"resourceId": 20801,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 1000000097,
|
||||||
|
"tradeState": "active",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 29843976
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"count": 1,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 1,
|
||||||
|
"sold": 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "none",
|
||||||
|
"buyNowPrice": 15000,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 0,
|
||||||
|
"expires": 1199,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 20801,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 20801,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 20801,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000097,
|
||||||
|
"itemState": "listFS",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 38,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 94,
|
||||||
|
"resourceId": 20801,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 1000000097,
|
||||||
|
"tradeState": "active",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 29843976,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
{
|
||||||
|
"100000059": {
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 61
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 158023,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000059,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"100000122": {
|
||||||
|
"assetId": 177003,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 66
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 177003,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 177003,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000122,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 10,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 89,
|
||||||
|
"resourceId": 177003,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"100000127": {
|
||||||
|
"assetId": 177003,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 66
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 177003,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 177003,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000127,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 10,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 89,
|
||||||
|
"resourceId": 177003,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"100004866": {
|
||||||
|
"assetId": 163824,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 43
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 56
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 82
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 163824,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 163824,
|
||||||
|
"discardValue": 900,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100004866,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 13,
|
||||||
|
"nation": 50,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 83,
|
||||||
|
"resourceId": 163824,
|
||||||
|
"teamid": 7,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"100004867": {
|
||||||
|
"assetId": 172610,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 47
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 57
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 76
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 172610,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 172610,
|
||||||
|
"discardValue": 900,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100004867,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 31,
|
||||||
|
"nation": 54,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 80,
|
||||||
|
"resourceId": 172610,
|
||||||
|
"teamid": 54,
|
||||||
|
"untradeable": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
==============================================================================
|
||||||
|
== auctionInfo deserializer 0x18013e410 — full decompilation
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||||
|
|
||||||
|
undefined4 FUN_18013e410(undefined8 param_1,longlong param_2,undefined8 param_3)
|
||||||
|
|
||||||
|
{
|
||||||
|
longlong *plVar1;
|
||||||
|
undefined1 *puVar2;
|
||||||
|
undefined1 *puVar3;
|
||||||
|
undefined ***pppuVar4;
|
||||||
|
undefined ***pppuVar5;
|
||||||
|
char cVar6;
|
||||||
|
int iVar7;
|
||||||
|
int iVar8;
|
||||||
|
undefined8 uVar9;
|
||||||
|
longlong *plVar10;
|
||||||
|
undefined4 uVar11;
|
||||||
|
undefined1 auStack_138 [32];
|
||||||
|
undefined **local_118;
|
||||||
|
undefined8 local_110;
|
||||||
|
longlong local_108;
|
||||||
|
undefined8 local_100;
|
||||||
|
undefined **local_f8;
|
||||||
|
undefined1 local_f0 [32];
|
||||||
|
undefined1 local_d0;
|
||||||
|
longlong *local_c8;
|
||||||
|
undefined8 local_c0;
|
||||||
|
undefined1 local_b0 [36];
|
||||||
|
undefined4 local_8c;
|
||||||
|
undefined4 local_88;
|
||||||
|
undefined4 local_84;
|
||||||
|
undefined4 local_80;
|
||||||
|
int local_70;
|
||||||
|
undefined8 local_68;
|
||||||
|
undefined1 local_58 [8];
|
||||||
|
longlong local_50;
|
||||||
|
longlong *local_48;
|
||||||
|
int local_40;
|
||||||
|
undefined1 local_3c;
|
||||||
|
undefined1 local_3b;
|
||||||
|
undefined1 local_3a;
|
||||||
|
undefined1 local_39;
|
||||||
|
ulonglong local_38;
|
||||||
|
|
||||||
|
local_100 = 0xfffffffffffffffe;
|
||||||
|
local_38 = DAT_1802db6e0 ^ (ulonglong)auStack_138;
|
||||||
|
iVar8 = 0x38c;
|
||||||
|
FUN_18010f5f0(&local_f8);
|
||||||
|
iVar7 = FUN_1801c7f10(param_2);
|
||||||
|
while (iVar7 != 10) {
|
||||||
|
if (*(int *)(param_2 + 0xd0) == 0xb) {
|
||||||
|
iVar8 = FUN_180180d00(*(undefined8 *)(param_2 + 0xf8));
|
||||||
|
iVar7 = FUN_1801c7f10(param_2);
|
||||||
|
if (iVar7 != 6) goto LAB_18013e498;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LAB_18013e498:
|
||||||
|
if (iVar8 < 0x2b8) {
|
||||||
|
if (iVar8 == 0x2b7) {
|
||||||
|
uVar9 = FUN_1801c7aa0(param_2);
|
||||||
|
FUN_180008120(local_b0,uVar9,0x1e);
|
||||||
|
}
|
||||||
|
else if (iVar8 < 0x117) {
|
||||||
|
if (iVar8 == 0x116) {
|
||||||
|
local_68 = FUN_1801c79d0(param_2);
|
||||||
|
}
|
||||||
|
else if (iVar8 == 0x57) {
|
||||||
|
uVar9 = FUN_1801c7aa0(param_2);
|
||||||
|
local_40 = FUN_180166380(uVar9);
|
||||||
|
}
|
||||||
|
else if (iVar8 == 0x65) {
|
||||||
|
uVar9 = FUN_1801c79d0(param_2);
|
||||||
|
local_88 = FUN_1800d7b30(uVar9);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (iVar8 != 0xc1) goto LAB_18013e59b;
|
||||||
|
uVar9 = FUN_1801c79d0(param_2);
|
||||||
|
local_80 = FUN_1800d7b30(uVar9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (iVar8 == 0x16b) {
|
||||||
|
FUN_18013fe00(local_58,param_2,0);
|
||||||
|
}
|
||||||
|
else if (iVar8 == 0x2b6) {
|
||||||
|
uVar9 = FUN_1801c79d0(param_2);
|
||||||
|
local_8c = FUN_1800d7b30(uVar9);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LAB_18013e59b:
|
||||||
|
FUN_180135ff0(iVar8,param_2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (iVar8 == 0x2e6) {
|
||||||
|
uVar9 = FUN_1801c79d0(param_2);
|
||||||
|
local_84 = FUN_1800d7b30(uVar9);
|
||||||
|
}
|
||||||
|
else if (iVar8 == 0x2f4) {
|
||||||
|
uVar9 = FUN_1801c79d0(param_2);
|
||||||
|
local_39 = FUN_1800d7b50(uVar9);
|
||||||
|
}
|
||||||
|
else if (iVar8 == 0x331) {
|
||||||
|
local_c0 = FUN_1801c79d0(param_2);
|
||||||
|
}
|
||||||
|
else if (iVar8 == 0x335) {
|
||||||
|
uVar9 = FUN_1801c7aa0(param_2);
|
||||||
|
local_70 = FUN_180166bd0(uVar9);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (iVar8 != 0x380) goto LAB_18013e59b;
|
||||||
|
cVar6 = FUN_1801c7620(param_2);
|
||||||
|
local_3c = cVar6 != '\0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iVar7 = FUN_1801c7f10(param_2);
|
||||||
|
}
|
||||||
|
if (local_70 == 4) {
|
||||||
|
local_3b = local_40 != 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
local_3b = (local_40 - 1U & 0xfffffffd) == 0;
|
||||||
|
}
|
||||||
|
local_3a = local_40 - 2U < 2;
|
||||||
|
local_d0 = 1;
|
||||||
|
FUN_180007f60(local_f0,param_3,1);
|
||||||
|
plVar10 = (longlong *)FUN_18011a830();
|
||||||
|
if (local_48 == (longlong *)0x0) {
|
||||||
|
local_110 = 0;
|
||||||
|
local_118 = &PTR_LAB_180214de8;
|
||||||
|
local_108 = 0;
|
||||||
|
(**(code **)(*plVar10 + 0xa00))(plVar10,&local_118,local_c0);
|
||||||
|
if (local_108 == 0) {
|
||||||
|
uVar11 = 1;
|
||||||
|
goto LAB_18013e795;
|
||||||
|
}
|
||||||
|
if ((local_58 != (undefined1 *)(local_108 + 0xa0)) &&
|
||||||
|
(plVar1 = *(longlong **)(local_108 + 0xb0), local_48 != plVar1)) {
|
||||||
|
if (local_48 != (longlong *)0x0) {
|
||||||
|
puVar2 = (undefined1 *)*local_48;
|
||||||
|
puVar3 = *(undefined1 **)(puVar2 + 8);
|
||||||
|
if (local_58 == puVar2) {
|
||||||
|
*local_48 = (longlong)puVar3;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (; puVar3 != (undefined1 *)0x0; puVar3 = *(undefined1 **)(puVar3 + 8)) {
|
||||||
|
if (puVar3 == local_58) {
|
||||||
|
*(undefined8 *)(puVar2 + 8) = *(undefined8 *)(puVar3 + 8);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
puVar2 = puVar3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local_48 = plVar1;
|
||||||
|
if (plVar1 != (longlong *)0x0) {
|
||||||
|
local_50 = *plVar1;
|
||||||
|
*plVar1 = (longlong)local_58;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
local_118 = &PTR_LAB_180214de8;
|
||||||
|
if (local_108 != 0) {
|
||||||
|
pppuVar4 = *(undefined ****)(local_108 + 0x30);
|
||||||
|
pppuVar5 = (undefined ***)pppuVar4[1];
|
||||||
|
if (&local_118 == pppuVar4) {
|
||||||
|
*(undefined ****)(local_108 + 0x30) = pppuVar5;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (; pppuVar5 != (undefined ***)0x0; pppuVar5 = (undefined ***)pppuVar5[1]) {
|
||||||
|
if (pppuVar5 == &local_118) {
|
||||||
|
pppuVar4[1] = pppuVar5[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pppuVar4 = pppuVar5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(**(code **)(*plVar10 + 0x9f8))(plVar10,param_1,&local_f8);
|
||||||
|
uVar11 = 10;
|
||||||
|
LAB_18013e795:
|
||||||
|
FUN_18000a3c0(local_58);
|
||||||
|
local_f8 = &PTR_FUN_18021b520;
|
||||||
|
while (local_c8 != (longlong *)0x0) {
|
||||||
|
plVar10 = (longlong *)local_c8[1];
|
||||||
|
(**(code **)(*local_c8 + 8))();
|
||||||
|
local_c8 = plVar10;
|
||||||
|
}
|
||||||
|
return uVar11;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== Flash publisher 0x1801bf030 — full decompilation
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
void FUN_1801bf030(longlong *param_1,longlong param_2,undefined8 param_3,undefined8 param_4)
|
||||||
|
|
||||||
|
{
|
||||||
|
longlong lVar1;
|
||||||
|
longlong *plVar2;
|
||||||
|
longlong lVar3;
|
||||||
|
int iVar4;
|
||||||
|
undefined4 local_res8 [2];
|
||||||
|
undefined4 local_res10 [2];
|
||||||
|
undefined4 local_res18 [2];
|
||||||
|
undefined4 local_res20 [2];
|
||||||
|
undefined1 *local_b0;
|
||||||
|
undefined1 *local_a8;
|
||||||
|
undefined1 *local_a0;
|
||||||
|
longlong *local_98;
|
||||||
|
undefined4 local_90;
|
||||||
|
char *local_88;
|
||||||
|
longlong local_80 [2];
|
||||||
|
longlong local_70;
|
||||||
|
longlong *local_68;
|
||||||
|
|
||||||
|
(**(code **)(*param_1 + 8))
|
||||||
|
(param_1,(*(longlong *)(param_2 + 0x38) - *(longlong *)(param_2 + 0x30)) / 0x18,*param_1
|
||||||
|
,param_4,0xfffffffffffffffe);
|
||||||
|
iVar4 = 0;
|
||||||
|
lVar3 = *(longlong *)(param_2 + 0x30);
|
||||||
|
if (lVar3 != *(longlong *)(param_2 + 0x38)) {
|
||||||
|
do {
|
||||||
|
lVar1 = *(longlong *)(lVar3 + 0x10);
|
||||||
|
FUN_1800d8fa0(*(undefined8 *)(lVar1 + 0x38),local_res10,local_res8);
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"TRADEID_LOWER",local_res8[0]);
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"TRADEID_UPPER",local_res10[0]);
|
||||||
|
FUN_1801a9200(local_80,*(undefined4 *)(lVar1 + 0x6c));
|
||||||
|
local_98 = (longlong *)FUN_1800d7370();
|
||||||
|
local_90 = 1;
|
||||||
|
local_88 = "FUT String";
|
||||||
|
local_b0 = &DAT_1802e13a0;
|
||||||
|
local_a8 = &DAT_1802e13a0;
|
||||||
|
local_a0 = &DAT_1802e13a1;
|
||||||
|
if (*(longlong *)(lVar1 + 0x90) - 1U < 0x20f5800) {
|
||||||
|
FUN_1801c2e50(&local_b0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
plVar2 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar2 + 400))(plVar2,&local_b0,"FUT_AUCTION_EXPIRED",1);
|
||||||
|
}
|
||||||
|
(**(code **)(*param_1 + 0x30))(param_1,iVar4,"DURATION",local_b0);
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"TIME_REMAINING",*(undefined4 *)(lVar1 + 0x90));
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"MIN_CREDITS",*(undefined4 *)(lVar1 + 0x78));
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"MAX_CREDITS",*(undefined4 *)(lVar1 + 0x70));
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"RESERVEDPRICE",*(undefined4 *)(lVar1 + 0x74));
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"YOURBID",*(undefined4 *)(lVar1 + 0xb8));
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"STATE",*(undefined4 *)(lVar1 + 0x88));
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"COINS_AWARDED",*(undefined1 *)(lVar1 + 0xbf));
|
||||||
|
FUN_1800d8fa0(*(undefined8 *)(*(longlong *)(lVar1 + 0xb0) + 8),local_res18,local_res20);
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"UUID_UPPER",local_res18[0]);
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"UUID_LOWER",local_res20[0]);
|
||||||
|
(**(code **)(*param_1 + 0x20))
|
||||||
|
(param_1,iVar4,"CARD_ID",*(undefined4 *)(*(longlong *)(lVar1 + 0xb0) + 0x18));
|
||||||
|
(**(code **)(*param_1 + 0x20))
|
||||||
|
(param_1,iVar4,"FIFA_ID",*(uint *)(*(longlong *)(lVar1 + 0xb0) + 0x18) & 0xffffff);
|
||||||
|
(**(code **)(*param_1 + 0x20))
|
||||||
|
(param_1,iVar4,"CARD_TYPE",*(undefined4 *)(*(longlong *)(lVar1 + 0xb0) + 0x4c));
|
||||||
|
(**(code **)(*param_1 + 0x20))
|
||||||
|
(param_1,iVar4,"CARD_OFFERSTATE",*(undefined4 *)(*(longlong *)(lVar1 + 0xb0) + 0x5c)
|
||||||
|
);
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"IS_WATCHED",*(undefined1 *)(lVar1 + 0xbc));
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"INBOX",*(undefined1 *)(lVar1 + 0xbe));
|
||||||
|
(**(code **)(*param_1 + 0x20))(param_1,iVar4,"IS_GLOW",*(undefined1 *)(lVar1 + 0xbd));
|
||||||
|
(**(code **)(*param_1 + 0x18))(param_1,iVar4,"TRADE_DATA_AVAILABLE",1);
|
||||||
|
if (1 < (longlong)local_a0 - (longlong)local_b0) {
|
||||||
|
if (local_b0 != (undefined1 *)0x0) {
|
||||||
|
(**(code **)(*local_98 + 0x18))(local_98,local_b0,(int)local_a0 - (int)local_b0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ((1 < local_70 - local_80[0]) && (local_80[0] != 0)) {
|
||||||
|
(**(code **)(*local_68 + 0x18))(local_68,local_80[0],(int)local_70 - (int)local_80[0]);
|
||||||
|
}
|
||||||
|
lVar3 = lVar3 + 0x18;
|
||||||
|
iVar4 = iVar4 + 1;
|
||||||
|
} while (lVar3 != *(longlong *)(param_2 + 0x38));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== strings: sold / clear / inbox / glow / collect
|
||||||
|
==============================================================================
|
||||||
|
sold 0x180228bed 'sold'
|
||||||
|
sold 0x18022f524 'sold'
|
||||||
|
Sold 0x1801efaf1 'SoldFromTradePile'
|
||||||
|
Sold 0x1802302cf 'SoldBid'
|
||||||
|
Sold 0x1802302df 'SoldBuyNow'
|
||||||
|
SOLD 0x18020a0c7 'SOLD'
|
||||||
|
clearSold ABSENT
|
||||||
|
ClearSold ABSENT
|
||||||
|
INBOX 0x180215044 'INBOX'
|
||||||
|
inbox ABSENT
|
||||||
|
IS_GLOW 0x1802151a0 'IS_GLOW'
|
||||||
|
glow ABSENT
|
||||||
|
Glow ABSENT
|
||||||
|
collect 0x1802307e0 'collector'
|
||||||
|
Collect 0x1801ec3c1 'CollectedCards'
|
||||||
|
Collect 0x1801ec3f8 'Collection'
|
||||||
|
Collect 0x1801ec422 'Collection'
|
||||||
|
Collect 0x1801ec449 'Collection'
|
||||||
|
unclaimed 0x18022fd68 'unclaimedPrizesChampionEvents'
|
||||||
|
claim 0x1802099d1 'claimedPack'
|
||||||
|
claim 0x1802099e9 'claimedPack2'
|
||||||
|
claim 0x18022fd6a 'claimedPrizesChampionEvents'
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""Q1 — what does FIFA17 do with tradeState == closed(4)?
|
||||||
|
|
||||||
|
Targets the ONE native tradeState consumer branch (cmp [rbp-0x38],0x4 at
|
||||||
|
0x18013e619) inside the auctionInfo deserializer 0x18013e410, and the Flash
|
||||||
|
publisher 0x1801bf030.
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== auctionInfo deserializer 0x18013e410 — full decompilation")
|
||||||
|
print("=" * 78)
|
||||||
|
print(dec(0x18013E410))
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== Flash publisher 0x1801bf030 — full decompilation")
|
||||||
|
print("=" * 78)
|
||||||
|
print(dec(0x1801BF030))
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== strings: sold / clear / inbox / glow / collect")
|
||||||
|
print("=" * 78)
|
||||||
|
for needle in (b"sold", b"Sold", b"SOLD", b"clearSold", b"ClearSold",
|
||||||
|
b"INBOX", b"inbox", b"IS_GLOW", b"glow", b"Glow",
|
||||||
|
b"collect", b"Collect", b"unclaimed", b"claim"):
|
||||||
|
hits = find_all(needle, blocks=(".rdata", ".data", ".text"))
|
||||||
|
for h in hits[:14]:
|
||||||
|
s = rd_str(h, 90)
|
||||||
|
if s and len(s) > 2:
|
||||||
|
print(f" {needle.decode():12s} 0x{h:x} {s!r}")
|
||||||
|
if not hits:
|
||||||
|
print(f" {needle.decode():12s} ABSENT")
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
==============================================================================
|
||||||
|
== neighbourhood of SoldBid/SoldBuyNow (0x1802302cf) — read the whole table
|
||||||
|
==============================================================================
|
||||||
|
0x180230280 'auctionLostBidRejected'
|
||||||
|
0x180230298 'auctionLostOutbid'
|
||||||
|
0x1802302b0 'auctionLostOutbidSelf'
|
||||||
|
0x1802302c8 'auctionSoldBid'
|
||||||
|
0x1802302d8 'auctionSoldBuyNow'
|
||||||
|
0x1802302f0 'auctionWonBid'
|
||||||
|
0x180230300 'auctionWonBuyNow'
|
||||||
|
0x180230318 'authToken'
|
||||||
|
0x180230328 'available'
|
||||||
|
0x180230338 'awaykit'
|
||||||
|
0x180230340 'awardCount'
|
||||||
|
0x180230350 'awardedPrizes'
|
||||||
|
0x180230360 'awardItemData'
|
||||||
|
0x180230370 'awardMappings'
|
||||||
|
0x180230380 'awardType'
|
||||||
|
0x180230390 'awardSet'
|
||||||
|
0x1802303a0 'awardSetId'
|
||||||
|
0x1802303ac 'awards'
|
||||||
|
0x1802303b8 'awardValue'
|
||||||
|
0x1802303c8 'badgeDBid'
|
||||||
|
0x1802303d4 'badges'
|
||||||
|
0x1802303dc 'balls'
|
||||||
|
0x1802303e8 'BEGINNER'
|
||||||
|
0x1802303f8 'bestBuilderScore'
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== xrefs to each sold-ish string
|
||||||
|
==============================================================================
|
||||||
|
sold@228bed 0 xref(s)
|
||||||
|
sold@22f524 1 xref(s)
|
||||||
|
from 0x1802d3da8 DATA in ? (entry 0x0)
|
||||||
|
SoldFromTradePile 0 xref(s)
|
||||||
|
SoldBid 0 xref(s)
|
||||||
|
SoldBuyNow 0 xref(s)
|
||||||
|
SOLD@20a0c7 0 xref(s)
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== who reads record +0xbf (COINS_AWARDED) / +0xbe (INBOX) / +0xbd (IS_GLOW)
|
||||||
|
== i.e. the deserializer's coinsProcessed + the two computed flags
|
||||||
|
==============================================================================
|
||||||
|
-- decoder for atom 0x2f4 (coinsProcessed): FUN_1800d7b50
|
||||||
|
|
||||||
|
ulonglong FUN_1800d7b50(longlong param_1)
|
||||||
|
|
||||||
|
{
|
||||||
|
ulonglong in_RAX;
|
||||||
|
byte bVar1;
|
||||||
|
|
||||||
|
if (0 < param_1) {
|
||||||
|
bVar1 = 0xff;
|
||||||
|
if (param_1 < 0xff) {
|
||||||
|
bVar1 = (byte)param_1;
|
||||||
|
}
|
||||||
|
return (ulonglong)bVar1;
|
||||||
|
}
|
||||||
|
return in_RAX & 0xffffffffffffff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== route table 0x18021df80 — all 45 rows, looking for a clear/sold route
|
||||||
|
==============================================================================
|
||||||
|
[ 0] 0x18021df80 -> 0x18021e308 'ut/%s/auctionhouse'
|
||||||
|
[ 1] 0x18021df88 -> 0x18021e320 'AUCTIONHOUSE'
|
||||||
|
[ 2] 0x18021df90 -> 0x18021e330 'ut/%s/clubUser'
|
||||||
|
[ 3] 0x18021df98 -> 0x18021e340 'CLUB_USER'
|
||||||
|
[ 4] 0x18021dfa0 -> 0x18021e350 'ut/%s/user/list'
|
||||||
|
[ 5] 0x18021dfa8 -> 0x1801fa0b8 'CLUB_INFO'
|
||||||
|
[ 6] 0x18021dfb0 -> 0x18021e360 'ut/%s/club'
|
||||||
|
[ 7] 0x18021dfb8 -> 0x1801f6438 'CLUB'
|
||||||
|
[ 8] 0x18021dfc0 -> 0x18021e370 'ut/%s/defid'
|
||||||
|
[ 9] 0x18021dfc8 -> 0x18021e37c 'DREAM'
|
||||||
|
[10] 0x18021dfd0 -> 0x18021e388 'ut/%s/squad'
|
||||||
|
[11] 0x18021dfd8 -> 0x18021e394 'SQUAD'
|
||||||
|
[12] 0x18021dfe0 -> 0x18021e3a0 'ut/delete/%s/squad'
|
||||||
|
[13] 0x18021dfe8 -> 0x18021e3b8 'DELETE_SQUAD'
|
||||||
|
[14] 0x18021dff0 -> 0x18021e3c8 'ut/%s/leaderboards/options'
|
||||||
|
[15] 0x18021dff8 -> 0x18021e3e8 'LBOPTIONS'
|
||||||
|
[16] 0x18021e000 -> 0x18021e3f8 'ut/%s/leaderboards'
|
||||||
|
[17] 0x18021e008 -> 0x18021e410 'LBDEFAULT'
|
||||||
|
[18] 0x18021e010 -> 0x18021e420 'ut/%s/activeMessage'
|
||||||
|
[19] 0x18021e018 -> 0x18021e438 'PAFPRACTICE'
|
||||||
|
[20] 0x18021e020 -> 0x18021e444 'ut/%s'
|
||||||
|
[21] 0x18021e028 -> 0x18021e44c 'UT'
|
||||||
|
[22] 0x18021e030 -> 0x18021e450 'ut/%s/user'
|
||||||
|
[23] 0x18021e038 -> 0x18021e45c 'USER'
|
||||||
|
[24] 0x18021e040 -> 0x18021e468 'ut/delete/%s/user'
|
||||||
|
[25] 0x18021e048 -> 0x18021e480 'DELETEUSER'
|
||||||
|
[26] 0x18021e050 -> 0x18021e490 'ut/%s/item'
|
||||||
|
[27] 0x18021e058 -> 0x18021e49c 'ITEMS'
|
||||||
|
[28] 0x18021e060 -> 0x18021e4a8 'ut/%s/item/resource'
|
||||||
|
[29] 0x18021e068 -> 0x18021e4c0 'ITEMS_BY_RES'
|
||||||
|
[30] 0x18021e070 -> 0x18021e4d0 'ut/delete/%s/item'
|
||||||
|
[31] 0x18021e078 -> 0x18021e4e8 'DELETEITEMS'
|
||||||
|
[32] 0x18021e080 -> 0x18021e4f8 'ut/%s/match'
|
||||||
|
[33] 0x18021e088 -> 0x1801ecc34 'MATCH'
|
||||||
|
[34] 0x18021e090 -> 0x18021e508 'ut/%s/sbs'
|
||||||
|
[35] 0x18021e098 -> 0x18021e514 'SBC'
|
||||||
|
[36] 0x18021e0a0 -> 0x18021e518 'ut/%s/tournament'
|
||||||
|
[37] 0x18021e0a8 -> 0x18021e530 'TOURNAMENT'
|
||||||
|
[38] 0x18021e0b0 -> 0x18021e540 'ut/%s/tournament/user'
|
||||||
|
[39] 0x18021e0b8 -> 0x18021e558 'TOURNAMENTUSER'
|
||||||
|
[40] 0x18021e0c0 -> 0x18021e568 'ut/delete/%s/tournament/user'
|
||||||
|
[41] 0x18021e0c8 -> 0x18021e588 'TOURNAMENTQUIT'
|
||||||
|
[42] 0x18021e0d0 -> 0x18021e598 'ut/%s/season'
|
||||||
|
[43] 0x18021e0d8 -> 0x180209fc4 'SEASON'
|
||||||
|
[44] 0x18021e0e0 -> 0x18021e5a8 'ut/%s/season/user'
|
||||||
|
[45] 0x18021e0e8 -> 0x18021e5c0 'SEASONUSER'
|
||||||
|
[46] 0x18021e0f0 -> 0x18021e5d0 'ut/%s/season/%%s/user'
|
||||||
|
[47] 0x18021e0f8 -> 0x18021e5e8 'SEASONUSER_ALTER'
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
"""Q2 — the sold vocabulary: SoldBid / SoldBuyNow / SoldFromTradePile / sold,
|
||||||
|
the coinsProcessed -> COINS_AWARDED path, and the route table (clear-sold?).
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== neighbourhood of SoldBid/SoldBuyNow (0x1802302cf) — read the whole table")
|
||||||
|
print("=" * 78)
|
||||||
|
a = 0x180230280
|
||||||
|
while a < 0x180230400:
|
||||||
|
s = rd_str(a, 60)
|
||||||
|
if s and len(s) > 1 and s.isprintable():
|
||||||
|
print(f" 0x{a:x} {s!r}")
|
||||||
|
a += len(s.encode()) + 1
|
||||||
|
else:
|
||||||
|
a += 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== xrefs to each sold-ish string")
|
||||||
|
print("=" * 78)
|
||||||
|
for name, sa in (("sold@228bed", 0x180228BED), ("sold@22f524", 0x18022F524),
|
||||||
|
("SoldFromTradePile", 0x1801EFAF1), ("SoldBid", 0x1802302CF),
|
||||||
|
("SoldBuyNow", 0x1802302DF), ("SOLD@20a0c7", 0x18020A0C7)):
|
||||||
|
xs = xrefs_to(sa)
|
||||||
|
print(f" {name:20s} {len(xs)} xref(s)")
|
||||||
|
for x in xs[:8]:
|
||||||
|
print(f" from 0x{x[0]:x} {x[1]} in {x[2]} (entry 0x{x[3]:x})")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== who reads record +0xbf (COINS_AWARDED) / +0xbe (INBOX) / +0xbd (IS_GLOW)")
|
||||||
|
print("== i.e. the deserializer's coinsProcessed + the two computed flags")
|
||||||
|
print("=" * 78)
|
||||||
|
print("-- decoder for atom 0x2f4 (coinsProcessed): FUN_1800d7b50")
|
||||||
|
print(dec(0x1800D7B50))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== route table 0x18021df80 — all 45 rows, looking for a clear/sold route")
|
||||||
|
print("=" * 78)
|
||||||
|
for i in range(48):
|
||||||
|
ea = 0x18021DF80 + i * 8
|
||||||
|
try:
|
||||||
|
p = qword(ea)
|
||||||
|
except Exception:
|
||||||
|
break
|
||||||
|
s = ""
|
||||||
|
if 0x180000000 < p < 0x180400000:
|
||||||
|
s = rd_str(p, 120)
|
||||||
|
print(f" [{i:2d}] 0x{ea:x} -> 0x{p:x} {s!r}")
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
==============================================================================
|
||||||
|
== route table continued from 0x18021e0f8 until it stops looking like one
|
||||||
|
==============================================================================
|
||||||
|
0x18021e100 -> 0x18021e600 'ut/%s/season/%%s/reset'
|
||||||
|
0x18021e108 -> 0x18021e618 'SEASONRESET'
|
||||||
|
0x18021e110 -> 0x18021e628 'ut/%s/season/friendly'
|
||||||
|
0x18021e118 -> 0x18021e640 'FRIENDLYSEASON'
|
||||||
|
0x18021e120 -> 0x18021e650 'ut/%s/purchased'
|
||||||
|
0x18021e128 -> 0x18021e660 'PURCHASED'
|
||||||
|
0x18021e130 -> 0x18021e670 'ut/%s/store'
|
||||||
|
0x18021e138 -> 0x18021e67c 'STORE'
|
||||||
|
0x18021e140 -> 0x18021e688 'ut/%s/watchList'
|
||||||
|
0x18021e148 -> 0x18021e698 'WATCHLIST'
|
||||||
|
0x18021e150 -> 0x18021e6a8 'ut/delete/%s/watchList'
|
||||||
|
0x18021e158 -> 0x18021e6c0 'DELETEWATCHLIST'
|
||||||
|
0x18021e160 -> 0x18021e6d0 'ut/%s/tradePile'
|
||||||
|
0x18021e168 -> 0x18021e6e0 'TRADEPILE'
|
||||||
|
0x18021e170 -> 0x18021e6f0 'ut/%s/trade'
|
||||||
|
0x18021e178 -> 0x18021e6fc 'TRADE'
|
||||||
|
0x18021e180 -> 0x18021e708 'ut/delete/%s/trade'
|
||||||
|
0x18021e188 -> 0x18021e720 'DELETETRADE'
|
||||||
|
0x18021e190 -> 0x18021e730 'ut/%s/marketdata'
|
||||||
|
0x18021e198 -> 0x18021e748 'MARKETDATA'
|
||||||
|
0x18021e1a0 -> 0x18021e758 'ut/%s/clientdata'
|
||||||
|
0x18021e1a8 -> 0x18021e770 'CLIENTDATA'
|
||||||
|
0x18021e1b0 -> 0x18021e780 'ut/auth'
|
||||||
|
0x18021e1b8 -> 0x18021e788 'AUTH'
|
||||||
|
0x18021e1c0 -> 0x18021e790 'ut/delete/auth'
|
||||||
|
0x18021e1c8 -> 0x18021e7a0 'DELETE_AUTH'
|
||||||
|
0x18021e1d0 -> 0x18021e7b0 'ut/%s/phishing'
|
||||||
|
0x18021e1d8 -> 0x18021e7c0 'PHISHING'
|
||||||
|
0x18021e1e0 -> 0x18021e7d0 'ut/%s/captcha'
|
||||||
|
0x18021e1e8 -> 0x18021e7e0 'CAPTCHA'
|
||||||
|
0x18021e1f0 -> 0x18021e7e8 'ut/%s/tfa'
|
||||||
|
0x18021e1f8 -> 0x18021e7f4 'TFA'
|
||||||
|
0x18021e200 -> 0x18021e7f8 'ut/%s/squad/mode'
|
||||||
|
0x18021e208 -> 0x18021e810 'SQUADMODE'
|
||||||
|
0x18021e210 -> 0x18021e820 'ut/%s/draft/mode'
|
||||||
|
0x18021e218 -> 0x18021e834 'DRAFT'
|
||||||
|
0x18021e220 -> 0x18021e840 'ut/%s/champion'
|
||||||
|
0x18021e228 -> 0x18021e850 'CHAMPIONS'
|
||||||
|
0x18021e230 -> 0x18021e860 'ut/v2/%s/store'
|
||||||
|
0x18021e238 -> 0x18021e870 'V2STORE'
|
||||||
|
0x18021e240 -> 0x18021e878 'ut/%s/livemessage'
|
||||||
|
0x18021e248 -> 0x18021e890 'LIVEMESSAGE'
|
||||||
|
0x18021e258 -> 0x18021e89c 'ADMIN'
|
||||||
|
0x18021e268 -> 0x18021e8a4 'DEBUG'
|
||||||
|
0x18021e278 -> 0x18021e8b0 'MAINTENANCE'
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== itemState table 0x180229cc0 (prior work: 12 rows) — dump generously
|
||||||
|
==============================================================================
|
||||||
|
[ 0] 0x180229cc0 str=0x18022a2a0 'invalid' val=0 (0x0)
|
||||||
|
[ 1] 0x180229cd0 str=0x18022a2a8 'free' val=1 (0x1)
|
||||||
|
[ 2] 0x180229ce0 str=0x18022a2b0 'WAITING_FOR_GAME' val=2 (0x2)
|
||||||
|
[ 3] 0x180229cf0 str=0x18022a2c4 'inGame' val=2 (0x2)
|
||||||
|
[ 4] 0x180229d00 str=0x18022a2d0 'forSale' val=5 (0x5)
|
||||||
|
[ 5] 0x180229d10 str=0x18022a2d8 'offered' val=6 (0x6)
|
||||||
|
[ 6] 0x180229d20 str=0x18022a2e0 'activeBadge' val=100 (0x64)
|
||||||
|
[ 7] 0x180229d30 str=0x18022a2f0 'activeHomeKit' val=101 (0x65)
|
||||||
|
[ 8] 0x180229d40 str=0x18022a300 'activeAwayKit' val=102 (0x66)
|
||||||
|
[ 9] 0x180229d50 str=0x18022a310 'activeBall' val=103 (0x67)
|
||||||
|
[10] 0x180229d60 str=0x18022a320 'activeStadium' val=104 (0x68)
|
||||||
|
[11] 0x180229d70 str=0x180221c04 'active' val=255 (0xff)
|
||||||
|
[12] 0x180229d80 str=0x0 '' val=4294967295 (0xffffffff)
|
||||||
|
[13] 0x180229d90 str=0x18022a330 'none' val=8 (0x8)
|
||||||
|
[14] 0x180229da0 str=0x18022a338 'head' val=0 (0x0)
|
||||||
|
[15] 0x180229db0 str=0x18022a340 'shoulder' val=1 (0x1)
|
||||||
|
[16] 0x180229dc0 str=0x18022a34c 'arm' val=2 (0x2)
|
||||||
|
[17] 0x180229dd0 str=0x1802009d0 'back' val=3 (0x3)
|
||||||
|
[18] 0x180229de0 str=0x18022a350 'hip' val=4 (0x4)
|
||||||
|
[19] 0x180229df0 str=0x18022a354 'leg' val=5 (0x5)
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== tradeState table 0x180229e40 — dump generously
|
||||||
|
==============================================================================
|
||||||
|
[ 0] 0x180229e40 str=0x180221c04 'active' val=1 (0x1)
|
||||||
|
[ 1] 0x180229e50 str=0x18022a360 'inactive' val=2 (0x2)
|
||||||
|
[ 2] 0x180229e60 str=0x18022a370 'expired' val=3 (0x3)
|
||||||
|
[ 3] 0x180229e70 str=0x18022a378 'closed' val=4 (0x4)
|
||||||
|
[ 4] 0x180229e80 str=0x0 '' val=0 (0x0)
|
||||||
|
[ 5] 0x180229e90 str=0x4e4f5f53544f4853 '' val=1380013151 (0x5241545f)
|
||||||
|
[ 6] 0x180229ea0 str=0x4653534543435553 '' val=1415531605 (0x545f4c55)
|
||||||
|
[ 7] 0x180229eb0 str=0x5345 '' val=1314017091 (0x4e524f43)
|
||||||
|
[ 8] 0x180229ec0 str=0x4548534e41454c43 '' val=5461061 (0x535445)
|
||||||
|
[ 9] 0x180229ed0 str=0x5f474e4953534150 '' val=1129465168 (0x43524550)
|
||||||
|
[10] 0x180229ee0 str=0x4547 '' val=1397968720 (0x53534f50)
|
||||||
|
[11] 0x180229ef0 str=0x45435245505f4e4f '' val=1195463758 (0x4741544e)
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== pointer arrays containing the sold vocabulary
|
||||||
|
==============================================================================
|
||||||
|
auctionSoldBid pointer found at: ['0x1802d2928']
|
||||||
|
auctionSoldBuyNow pointer found at: ['0x1802d2930']
|
||||||
|
auctionWonBuyNow pointer found at: ['0x1802d2940']
|
||||||
|
sold@22f524 pointer found at: ['0x1802d3da8']
|
||||||
|
sold@228bed pointer found at: NOWHERE
|
||||||
|
SoldFromTradePile pointer found at: NOWHERE
|
||||||
|
SOLD@20a0c7 pointer found at: NOWHERE
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== the .data array around 0x1802d3da8 (the one DATA xref to 'sold')
|
||||||
|
==============================================================================
|
||||||
|
0x1802d3d68 -> 0x1801e98ac 'shots'
|
||||||
|
0x1802d3d70 -> 0x18022f4c8 'shotsOnTarget'
|
||||||
|
0x1802d3d78 -> 0x18022f4d8 'silhouetteName'
|
||||||
|
0x1802d3d80 -> 0x18022f4e8 'silName'
|
||||||
|
0x1802d3d88 -> 0x1801ec108 'silver'
|
||||||
|
0x1802d3d90 -> 0x18022f4f0 'silverQuantity'
|
||||||
|
0x1802d3d98 -> 0x18022f500 'sizeBeforeEncode'
|
||||||
|
0x1802d3da0 -> 0x18022f518 'slotIndex'
|
||||||
|
0x1802d3da8 -> 0x18022f524 'sold' <<<
|
||||||
|
0x1802d3db0 -> 0x18022f52c 'sort'
|
||||||
|
0x1802d3db8 -> 0x18022f538 'sortPriority'
|
||||||
|
0x1802d3dc0 -> 0x18022ed30 'source'
|
||||||
|
0x1802d3dc8 -> 0x18022f548 'squad'
|
||||||
|
0x1802d3dd0 -> 0x18022f550 'squadActives'
|
||||||
|
0x1802d3dd8 -> 0x18022f560 'squadBuildingSetsClientData'
|
||||||
|
0x1802d3de0 -> 0x18022f580 'squadBuildingSetsGracePeriodMinutes'
|
||||||
|
0x1802d3de8 -> 0x18022f5a8 'squadChallenge'
|
||||||
|
0x1802d3df0 -> 0x18022f5b8 'squadId'
|
||||||
|
0x1802d3df8 -> 0x18022f5c0 'squadName'
|
||||||
|
0x1802d3e00 -> 0x18022f5d0 'squadList'
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
"""Q3 — full route table, the itemState/tradeState enum tables, and the pointer
|
||||||
|
arrays that reference the auctionSold* vocabulary and the bare 'sold' string.
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== route table continued from 0x18021e0f8 until it stops looking like one")
|
||||||
|
print("=" * 78)
|
||||||
|
a = 0x18021E100
|
||||||
|
misses = 0
|
||||||
|
while a < 0x18021E400 and misses < 6:
|
||||||
|
p = qword(a)
|
||||||
|
s = rd_str(p, 120) if 0x180000000 < p < 0x180400000 else ""
|
||||||
|
if s and s.isprintable():
|
||||||
|
print(f" 0x{a:x} -> 0x{p:x} {s!r}")
|
||||||
|
misses = 0
|
||||||
|
else:
|
||||||
|
misses += 1
|
||||||
|
a += 8
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== itemState table 0x180229cc0 (prior work: 12 rows) — dump generously")
|
||||||
|
print("=" * 78)
|
||||||
|
for i in range(20):
|
||||||
|
ea = 0x180229CC0 + i * 16
|
||||||
|
p, v = qword(ea), dword(ea + 8)
|
||||||
|
s = rd_str(p, 40) if 0x180000000 < p < 0x180400000 else ""
|
||||||
|
print(f" [{i:2d}] 0x{ea:x} str=0x{p:x} {s!r:24s} val={v} (0x{v:x})")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== tradeState table 0x180229e40 — dump generously")
|
||||||
|
print("=" * 78)
|
||||||
|
for i in range(12):
|
||||||
|
ea = 0x180229E40 + i * 16
|
||||||
|
p, v = qword(ea), dword(ea + 8)
|
||||||
|
s = rd_str(p, 40) if 0x180000000 < p < 0x180400000 else ""
|
||||||
|
print(f" [{i:2d}] 0x{ea:x} str=0x{p:x} {s!r:24s} val={v} (0x{v:x})")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== pointer arrays containing the sold vocabulary")
|
||||||
|
print("=" * 78)
|
||||||
|
for label, target in (("auctionSoldBid", 0x1802302C8),
|
||||||
|
("auctionSoldBuyNow", 0x1802302D8),
|
||||||
|
("auctionWonBuyNow", 0x180230300),
|
||||||
|
("sold@22f524", 0x18022F524),
|
||||||
|
("sold@228bed", 0x180228BED),
|
||||||
|
("SoldFromTradePile", 0x1801EFAF1),
|
||||||
|
("SOLD@20a0c7", 0x18020A0C7)):
|
||||||
|
pat = struct.pack("<Q", target)
|
||||||
|
hits = find_all(pat, blocks=(".rdata", ".data", ".text"))
|
||||||
|
print(f" {label:20s} pointer found at: {[hex(h) for h in hits] or 'NOWHERE'}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== the .data array around 0x1802d3da8 (the one DATA xref to 'sold')")
|
||||||
|
print("=" * 78)
|
||||||
|
for i in range(-8, 12):
|
||||||
|
ea = 0x1802D3DA8 + i * 8
|
||||||
|
try:
|
||||||
|
p = qword(ea)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
s = rd_str(p, 50) if 0x180000000 < p < 0x180400000 else ""
|
||||||
|
mark = " <<<" if i == 0 else ""
|
||||||
|
print(f" 0x{ea:x} -> 0x{p:x} {s!r}{mark}")
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""Q4 — is the atom ID the index into the alphabetical atom-name pointer table?
|
||||||
|
|
||||||
|
Validate against the twelve KNOWN auctionInfo atoms. If all twelve agree on one
|
||||||
|
base, the table is the atom dictionary and we can read the ID of any name.
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
|
||||||
|
KNOWN = { # atom id -> name, from the confirmed auctionInfo deserializer
|
||||||
|
0x57: "bidState", 0x65: "buyNowPrice", 0xC1: "currentBid", 0x116: "expires",
|
||||||
|
0x16B: "itemData", 0x2B6: "sellerEstablished", 0x2B7: "sellerName",
|
||||||
|
0x2E6: "startingBid", 0x2F4: "coinsProcessed", 0x331: "tradeId",
|
||||||
|
0x335: "tradeState", 0x380: "watched",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def str_addr(name):
|
||||||
|
"""Address of the exact NUL-terminated string `name`."""
|
||||||
|
for h in find_all(name.encode() + b"\x00", blocks=(".rdata", ".data")):
|
||||||
|
return h
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ptr_addr(sa):
|
||||||
|
hits = find_all(struct.pack("<Q", sa), blocks=(".rdata", ".data"))
|
||||||
|
return hits[0] if hits else None
|
||||||
|
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== solve for the table base using the twelve known atoms")
|
||||||
|
print("=" * 78)
|
||||||
|
bases = {}
|
||||||
|
for aid, name in sorted(KNOWN.items()):
|
||||||
|
sa = str_addr(name)
|
||||||
|
pa = ptr_addr(sa) if sa else None
|
||||||
|
if pa is None:
|
||||||
|
print(f" 0x{aid:03x} {name:20s} string=0x{sa or 0:x} pointer=NOT FOUND")
|
||||||
|
continue
|
||||||
|
base = pa - 8 * aid
|
||||||
|
bases[base] = bases.get(base, 0) + 1
|
||||||
|
print(f" 0x{aid:03x} {name:20s} str=0x{sa:x} ptr=0x{pa:x} -> base 0x{base:x}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" base histogram:", {hex(b): n for b, n in sorted(bases.items(), key=lambda kv: -kv[1])})
|
||||||
|
if not bases:
|
||||||
|
raise SystemExit("no bases resolved")
|
||||||
|
BASE = max(bases, key=bases.get)
|
||||||
|
print(f" CONSENSUS BASE = 0x{BASE:x} ({bases[BASE]}/{len(KNOWN)} atoms agree)")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== read the atom id of every sold/counts-related name")
|
||||||
|
print("=" * 78)
|
||||||
|
for name in ("sold", "selling", "offered", "count", "maxAuctionsAllowed",
|
||||||
|
"auctionSoldBid", "auctionSoldBuyNow", "auctionWonBid",
|
||||||
|
"auctionWonBuyNow", "auctionLostOutbid", "auctionLostOutbidSelf",
|
||||||
|
"auctionLostBidRejected", "credits", "coins", "tradeIdStr",
|
||||||
|
"itemState", "offers", "bids", "watched", "expires"):
|
||||||
|
sa = str_addr(name)
|
||||||
|
pa = ptr_addr(sa) if sa else None
|
||||||
|
if pa is None:
|
||||||
|
print(f" {name:24s} ABSENT from the table")
|
||||||
|
continue
|
||||||
|
off = pa - BASE
|
||||||
|
if off % 8:
|
||||||
|
print(f" {name:24s} ptr=0x{pa:x} misaligned (off {off})")
|
||||||
|
continue
|
||||||
|
print(f" {name:24s} ptr=0x{pa:x} ATOM ID = 0x{off // 8:x} ({off // 8})")
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
==============================================================================
|
||||||
|
== atom ids, resolved against the table range only
|
||||||
|
==============================================================================
|
||||||
|
sold str=0x18022f524 ptr=0x1802d3da8 ATOM ID = 0x2c9 (713)
|
||||||
|
count str=0x180230abc ptr=0x1802d2d40 ATOM ID = 0xbc (188)
|
||||||
|
offered str=0x18022a2d8 ptr=0x1802d3688 ATOM ID = 0x1e5 (485)
|
||||||
|
selling str=0x18022f460 ptr=0x1802d3d20 ATOM ID = 0x2b8 (696)
|
||||||
|
maxAuctionsAllowed str=0x1802318a8 ptr=0x1802d3558 ATOM ID = 0x1bf (447)
|
||||||
|
credits str=0x180230af8 ptr=0x1802d2d60 ATOM ID = 0xc0 (192)
|
||||||
|
auctionInfo str=0x180230270 ptr=0x1802d2908 ATOM ID = 0x35 (53)
|
||||||
|
itemData str=0x180231438 ptr=0x1802d32b8 ATOM ID = 0x16b (363)
|
||||||
|
bidState str=0x180230458 ptr=0x1802d2a18 ATOM ID = 0x57 (87)
|
||||||
|
tradeState str=0x18022fb48 ptr=0x1802d4108 ATOM ID = 0x335 (821)
|
||||||
|
coinsProcessed str=0x18022f768 ptr=0x1802d3f00 ATOM ID = 0x2f4 (756)
|
||||||
|
offers str=0x180231ae4 ptr=0x1802d3690 ATOM ID = 0x1e6 (486)
|
||||||
|
duplicateItemIdList str=0x180230d18 ptr=0x1802d2ec0 ATOM ID = 0xec (236)
|
||||||
|
total str=0x18022fa4c ptr=0x1802d4088 ATOM ID = 0x325 (805)
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== functions that compare against >=3 of the auction-outcome atoms 0x36..0x3c
|
||||||
|
==============================================================================
|
||||||
|
scanned 13308 functions
|
||||||
|
0x180043b90 FUN_180043b90 sees ['0x36', '0x37', '0x38', '0x3c']
|
||||||
|
0x1800455b0 FUN_1800455b0 sees ['0x37', '0x38', '0x39']
|
||||||
|
0x18006b4c0 FUN_18006b4c0 sees ['0x38', '0x39', '0x3c']
|
||||||
|
0x180094ce0 FUN_180094ce0 sees ['0x36', '0x37', '0x38']
|
||||||
|
0x180097c70 FUN_180097c70 sees ['0x36', '0x37', '0x38']
|
||||||
|
0x1800cabb0 FUN_1800cabb0 sees ['0x38', '0x39', '0x3a']
|
||||||
|
0x1800cf3b0 FUN_1800cf3b0 sees ['0x36', '0x38', '0x3a', '0x3c']
|
||||||
|
0x1800e6410 FUN_1800e6410 sees ['0x36', '0x37', '0x38', '0x39']
|
||||||
|
0x1800ee7c0 FUN_1800ee7c0 sees ['0x36', '0x37', '0x38', '0x39', '0x3a', '0x3c']
|
||||||
|
0x1800fb5b0 FUN_1800fb5b0 sees ['0x38', '0x39', '0x3a', '0x3b']
|
||||||
|
0x1801071f0 FUN_1801071f0 sees ['0x36', '0x37', '0x38', '0x3a', '0x3b', '0x3c']
|
||||||
|
0x18012b370 FUN_18012b370 sees ['0x36', '0x37', '0x38', '0x39', '0x3a', '0x3b', '0x3c']
|
||||||
|
0x18012fd40 FUN_18012fd40 sees ['0x36', '0x37', '0x38', '0x3c']
|
||||||
|
0x18013fe00 FUN_18013fe00 sees ['0x36', '0x38', '0x3a', '0x3b']
|
||||||
|
0x180163770 FUN_180163770 sees ['0x36', '0x38', '0x3a']
|
||||||
|
0x180172d00 FUN_180172d00 sees ['0x38', '0x3a', '0x3c']
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== string neighbourhoods: SoldFromTradePile / SOLD / sold
|
||||||
|
==============================================================================
|
||||||
|
-- SoldFromTradePile
|
||||||
|
0x1801efa40 'CardsRequestStaffBonuses'
|
||||||
|
0x1801efa60 'CardsGetStaffBonuses'
|
||||||
|
0x1801efa78 'WatchListRequest'
|
||||||
|
0x1801efa90 'WatchListResultsRequest'
|
||||||
|
0x1801efaa8 'AddToWatchList'
|
||||||
|
0x1801efab8 'RemoveFromWatchList'
|
||||||
|
0x1801efad0 'RemoveFromTradePile'
|
||||||
|
0x1801efae8 'RemoveAllSoldFromTradePile'
|
||||||
|
0x1801efb08 'GetPhishingQuestion'
|
||||||
|
0x1801efb20 'SetPhishingAnswer'
|
||||||
|
0x1801efb38 'ValidatePhishingAnswer'
|
||||||
|
0x1801efb50 'RetrieveTrustedConsoleList'
|
||||||
|
0x1801efb70 'EnableLiveStreaming'
|
||||||
|
-- SOLD
|
||||||
|
0x18020a040 'D_PRIZES'
|
||||||
|
0x18020a050 'EVENT_ID'
|
||||||
|
0x18020a05c 'TEXT0'
|
||||||
|
0x18020a068 'FUT_UC_SQUAD'
|
||||||
|
0x18020a078 'TEXT1'
|
||||||
|
0x18020a080 'FUT_TOTAL_AUCTIONS'
|
||||||
|
0x18020a098 'FUT_UC_ITEMS'
|
||||||
|
0x18020a0a8 'FUT_TF_SELLING'
|
||||||
|
0x18020a0b8 'TEXT2'
|
||||||
|
0x18020a0c0 'FUT_TF_SOLD'
|
||||||
|
0x18020a0cc 'TEXT3'
|
||||||
|
0x18020a0d8 'FUT_TF_WINNING'
|
||||||
|
0x18020a0e8 'FUT_TF_OUTBID'
|
||||||
|
0x18020a0f8 'FUT_GH_TOTAL_PLAYERS_0'
|
||||||
|
0x18020a110 'FUT_GH_TOTAL_PLAYERS_1'
|
||||||
|
0x18020a128 'FUT_GH_TOP_SCORER'
|
||||||
|
-- sold@228bed
|
||||||
|
0x180228bc8 'RS4:FutISRemoveTradeServerResponse'
|
||||||
|
0x180228bec '/sold'
|
||||||
|
0x180228c00 '\\�&'
|
||||||
|
0x180228c11 "�'"
|
||||||
|
0x180228c28 "X�'"
|
||||||
|
0x180228c38 "`�'"
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""Q5 — fix the atom lookup, then find the CONSUMER of atoms 0x36..0x3c
|
||||||
|
(auctionLost*/auctionSold*/auctionWon*) and read the sold-related string
|
||||||
|
neighbourhoods.
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
|
||||||
|
BASE = 0x1802D2760
|
||||||
|
TABLE_LO, TABLE_HI = 0x1802D2760, 0x1802D4800
|
||||||
|
|
||||||
|
|
||||||
|
def atom_id(name):
|
||||||
|
"""Every string with this exact spelling, then the pointer that lies INSIDE
|
||||||
|
the atom table. Taking the first string match is wrong: common words appear
|
||||||
|
in several unrelated tables."""
|
||||||
|
out = []
|
||||||
|
for sa in find_all(name.encode() + b"\x00", blocks=(".rdata", ".data")):
|
||||||
|
for pa in find_all(struct.pack("<Q", sa), blocks=(".rdata", ".data")):
|
||||||
|
if TABLE_LO <= pa < TABLE_HI and (pa - BASE) % 8 == 0:
|
||||||
|
out.append((sa, pa, (pa - BASE) // 8))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== atom ids, resolved against the table range only")
|
||||||
|
print("=" * 78)
|
||||||
|
for name in ("sold", "count", "offered", "selling", "maxAuctionsAllowed",
|
||||||
|
"credits", "auctionInfo", "itemData", "bidState", "tradeState",
|
||||||
|
"coinsProcessed", "offers", "duplicateItemIdList", "total"):
|
||||||
|
res = atom_id(name)
|
||||||
|
if not res:
|
||||||
|
print(f" {name:22s} ABSENT from the atom table")
|
||||||
|
for sa, pa, aid in res:
|
||||||
|
print(f" {name:22s} str=0x{sa:x} ptr=0x{pa:x} ATOM ID = 0x{aid:x} ({aid})")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== functions that compare against >=3 of the auction-outcome atoms 0x36..0x3c")
|
||||||
|
print("=" * 78)
|
||||||
|
WANT = set(range(0x36, 0x3D))
|
||||||
|
hits = {}
|
||||||
|
fi = fm.getFunctions(True)
|
||||||
|
n = 0
|
||||||
|
for f in fi:
|
||||||
|
n += 1
|
||||||
|
ent = int(f.getEntryPoint().getOffset())
|
||||||
|
body = f.getBody()
|
||||||
|
seen = set()
|
||||||
|
it = listing.getInstructions(body, True)
|
||||||
|
while it.hasNext():
|
||||||
|
ins = it.next()
|
||||||
|
m = ins.getMnemonicString()
|
||||||
|
if m not in ("CMP", "SUB", "MOV", "LEA"):
|
||||||
|
continue
|
||||||
|
for i in range(ins.getNumOperands()):
|
||||||
|
for o in ins.getOpObjects(i):
|
||||||
|
try:
|
||||||
|
v = int(o.getValue())
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if v in WANT:
|
||||||
|
seen.add(v)
|
||||||
|
if len(seen) >= 3:
|
||||||
|
hits[ent] = (f.getName(), sorted(hex(v) for v in seen))
|
||||||
|
print(f" scanned {n} functions")
|
||||||
|
for ent, (nm, vals) in sorted(hits.items()):
|
||||||
|
print(f" 0x{ent:x} {nm:28s} sees {vals}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== string neighbourhoods: SoldFromTradePile / SOLD / sold")
|
||||||
|
print("=" * 78)
|
||||||
|
for label, lo, hi in (("SoldFromTradePile", 0x1801EFA40, 0x1801EFB80),
|
||||||
|
("SOLD", 0x18020A040, 0x18020A140),
|
||||||
|
("sold@228bed", 0x180228B60, 0x180228C60)):
|
||||||
|
print(f"-- {label}")
|
||||||
|
a = lo
|
||||||
|
while a < hi:
|
||||||
|
s = rd_str(a, 70)
|
||||||
|
if s and len(s) > 1 and s.isprintable():
|
||||||
|
print(f" 0x{a:x} {s!r}")
|
||||||
|
a += len(s.encode()) + 1
|
||||||
|
else:
|
||||||
|
a += 1
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
==============================================================================
|
||||||
|
== (a) xrefs: request names + the /sold path suffix
|
||||||
|
==============================================================================
|
||||||
|
RemoveAllSoldFromTradePile 1 xref(s)
|
||||||
|
0x1800293fe DATA in FUN_180028b50 (entry 0x180028b50)
|
||||||
|
RemoveFromTradePile 1 xref(s)
|
||||||
|
0x1800293d4 DATA in FUN_180028b50 (entry 0x180028b50)
|
||||||
|
AddToWatchList 1 xref(s)
|
||||||
|
0x180029380 DATA in FUN_180028b50 (entry 0x180028b50)
|
||||||
|
/sold suffix 1 xref(s)
|
||||||
|
0x180164813 DATA in FUN_1801647c0 (entry 0x1801647c0)
|
||||||
|
RS4:FutISRemoveTradeServerResponse 2 xref(s)
|
||||||
|
0x1801defe9 DATA in Unwind@1801defd0 (entry 0x1801defd0)
|
||||||
|
0x18016488d DATA in FUN_180164860 (entry 0x180164860)
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== (b) xrefs: the Transfer List caption keys
|
||||||
|
==============================================================================
|
||||||
|
FUT_TOTAL_AUCTIONS 1 xref(s)
|
||||||
|
0x1800b1ef8 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||||
|
FUT_UC_ITEMS 4 xref(s)
|
||||||
|
0x1800fbbdc DATA in FUN_1800fbae0 (entry 0x1800fbae0)
|
||||||
|
0x1800fbe5a DATA in FUN_1800fbae0 (entry 0x1800fbae0)
|
||||||
|
0x1800b1fe7 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||||
|
0x1800b21a9 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||||
|
FUT_TF_SELLING 1 xref(s)
|
||||||
|
0x1800b2023 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||||
|
FUT_TF_SOLD 1 xref(s)
|
||||||
|
0x1800b208e DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||||
|
FUT_TF_WINNING 1 xref(s)
|
||||||
|
0x1800b21e5 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||||
|
FUT_TF_OUTBID 1 xref(s)
|
||||||
|
0x1800b2250 DATA in FUN_1800b1dc0 (entry 0x1800b1dc0)
|
||||||
|
|
||||||
|
caption publisher candidates (entry -> #caption refs): {'0x1800b1dc0': 7, '0x1800fbae0': 2}
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== decompiled caption publisher 0x1800b1dc0
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
void FUN_1800b1dc0(undefined8 param_1,longlong *param_2)
|
||||||
|
|
||||||
|
{
|
||||||
|
char cVar1;
|
||||||
|
undefined8 uVar2;
|
||||||
|
longlong lVar3;
|
||||||
|
longlong *plVar4;
|
||||||
|
ushort uVar5;
|
||||||
|
longlong *local_res10;
|
||||||
|
undefined1 *local_90;
|
||||||
|
undefined1 *local_88;
|
||||||
|
undefined1 *local_80;
|
||||||
|
longlong *local_78;
|
||||||
|
undefined4 local_70;
|
||||||
|
char *local_68;
|
||||||
|
undefined1 *local_60;
|
||||||
|
undefined1 *local_58;
|
||||||
|
undefined1 *local_50;
|
||||||
|
longlong *local_48;
|
||||||
|
undefined4 local_40;
|
||||||
|
char *local_38;
|
||||||
|
|
||||||
|
uVar2 = FUN_1800d7170();
|
||||||
|
FUN_180009c80(&local_res10,uVar2);
|
||||||
|
lVar3 = (**(code **)(*local_res10 + 0x1f8))(local_res10);
|
||||||
|
local_78 = (longlong *)FUN_1800d7370();
|
||||||
|
local_70 = 1;
|
||||||
|
local_68 = "FUT String";
|
||||||
|
local_90 = &DAT_1802e13a0;
|
||||||
|
local_88 = &DAT_1802e13a0;
|
||||||
|
local_80 = &DAT_1802e13a1;
|
||||||
|
local_48 = (longlong *)FUN_1800d7370();
|
||||||
|
local_40 = 1;
|
||||||
|
local_38 = "FUT String";
|
||||||
|
local_60 = &DAT_1802e13a0;
|
||||||
|
local_58 = &DAT_1802e13a0;
|
||||||
|
local_50 = &DAT_1802e13a1;
|
||||||
|
(**(code **)(*param_2 + 0x58))(param_2,4);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,0,"UPDATE_TYPE",PTR_s_PANEL_UPDATE_1802a4fb0);
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,0,"TILE_ID",0x1b0);
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,0,"ADDON_ENUM",5);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_90,*(undefined4 *)(lVar3 + 0x38));
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,0,"TEXT0",local_90);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TOTAL_AUCTIONS",1);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,0,"TEXT1",local_90);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,1,"UPDATE_TYPE",PTR_s_PANEL_UPDATE_1802a4fb0);
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,1,"TILE_ID",0x1c0);
|
||||||
|
uVar5 = *(ushort *)(lVar3 + 0x1d6);
|
||||||
|
if (0x62 < uVar5) {
|
||||||
|
uVar5 = 99;
|
||||||
|
}
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,1,"NOTIFICATION",uVar5);
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,1,"ADDON_ENUM");
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_90,*(undefined2 *)(lVar3 + 0x1d4));
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,1,"TEXT0",local_90);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_UC_ITEMS",1);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,1,"TEXT1",local_90);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TF_SELLING",1);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_60,*(undefined2 *)(lVar3 + 0x1d2));
|
||||||
|
FUN_18000fbd0(&local_90,local_90,local_60);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,1,"TEXT2",local_90);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TF_SOLD",1);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_60,*(undefined2 *)(lVar3 + 0x1d8));
|
||||||
|
FUN_18000fbd0(&local_90,local_90,local_60);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,1,"TEXT3",local_90);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,2,"UPDATE_TYPE",PTR_s_PANEL_UPDATE_1802a4fb0);
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,2,"TILE_ID",0x1d0);
|
||||||
|
uVar5 = 99;
|
||||||
|
if (*(ushort *)(lVar3 + 0x1ce) < 99) {
|
||||||
|
uVar5 = *(ushort *)(lVar3 + 0x1ce);
|
||||||
|
}
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,2,"NOTIFICATION",uVar5);
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,2,"ADDON_ENUM");
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_90,*(undefined2 *)(lVar3 + 0x1ca));
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,2,"TEXT0",local_90);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_UC_ITEMS",1);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,2,"TEXT1",local_90);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TF_WINNING",1);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_60,*(undefined2 *)(lVar3 + 0x1c8));
|
||||||
|
FUN_18000fbd0(&local_90,local_90,local_60);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,2,"TEXT2",local_90);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 400))(plVar4,&local_90,"FUT_TF_OUTBID",1);
|
||||||
|
plVar4 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar4 + 0xb8))(plVar4,&local_60,*(undefined2 *)(lVar3 + 0x1cc));
|
||||||
|
FUN_18000fbd0(&local_90,local_90,local_60);
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,2,"TEXT3",local_90);
|
||||||
|
uVar2 = FUN_1800264e0();
|
||||||
|
cVar1 = FUN_180026510(uVar2);
|
||||||
|
if (cVar1 == '\0') {
|
||||||
|
(**(code **)(*param_2 + 0x78))(param_2,3,"UPDATE_TYPE",PTR_s_PANEL_UPDATE_1802a4fb0);
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,3,"TILE_ID",0x200);
|
||||||
|
(**(code **)(*param_2 + 0x70))(param_2,3,"ADDON_ENUM");
|
||||||
|
}
|
||||||
|
if ((1 < (longlong)local_50 - (longlong)local_60) && (local_60 != (undefined1 *)0x0)) {
|
||||||
|
(**(code **)(*local_48 + 0x18))(local_48,local_60,(int)local_50 - (int)local_60);
|
||||||
|
}
|
||||||
|
if ((1 < (longlong)local_80 - (longlong)local_90) && (local_90 != (undefined1 *)0x0)) {
|
||||||
|
(**(code **)(*local_78 + 0x18))(local_78,local_90,(int)local_80 - (int)local_90);
|
||||||
|
}
|
||||||
|
(**(code **)(*local_res10 + 8))(local_res10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== decompiled caption publisher 0x1800fbae0
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||||
|
/* WARNING: Globals starting with '_' overlap smaller symbols at the same address */
|
||||||
|
|
||||||
|
uint FUN_1800fbae0(undefined8 param_1,undefined4 param_2,undefined8 *param_3)
|
||||||
|
|
||||||
|
{
|
||||||
|
undefined1 *puVar1;
|
||||||
|
longlong lVar2;
|
||||||
|
uint uVar3;
|
||||||
|
int iVar4;
|
||||||
|
longlong *plVar5;
|
||||||
|
undefined8 uVar6;
|
||||||
|
longlong *plVar7;
|
||||||
|
longlong lVar8;
|
||||||
|
size_t sVar9;
|
||||||
|
size_t sVar10;
|
||||||
|
undefined1 auStack_2e8 [32];
|
||||||
|
undefined4 local_2c8;
|
||||||
|
undefined1 local_2b8;
|
||||||
|
uint local_2b4;
|
||||||
|
undefined4 local_2b0;
|
||||||
|
undefined4 uStack_2ac;
|
||||||
|
undefined1 *local_2a8;
|
||||||
|
undefined1 *local_2a0;
|
||||||
|
undefined1 *local_298;
|
||||||
|
longlong *local_290;
|
||||||
|
undefined4 local_288;
|
||||||
|
char *local_280;
|
||||||
|
undefined1 *local_278;
|
||||||
|
undefined1 *local_270;
|
||||||
|
undefined1 *local_268;
|
||||||
|
longlong *local_260;
|
||||||
|
undefined4 local_258;
|
||||||
|
char *local_250;
|
||||||
|
undefined1 *local_248;
|
||||||
|
undefined1 *local_240;
|
||||||
|
undefined1 *local_238;
|
||||||
|
longlong *local_230;
|
||||||
|
undefined4 local_228;
|
||||||
|
char *local_220;
|
||||||
|
longlong local_218;
|
||||||
|
undefined8 local_210;
|
||||||
|
longlong local_208;
|
||||||
|
longlong *local_200;
|
||||||
|
undefined4 local_1f8;
|
||||||
|
char *local_1f0;
|
||||||
|
void *local_1e8;
|
||||||
|
longlong local_1e0;
|
||||||
|
longlong local_1d8;
|
||||||
|
longlong *local_1d0;
|
||||||
|
undefined8 local_1b8;
|
||||||
|
longlong *local_1a8;
|
||||||
|
undefined8 local_1a0;
|
||||||
|
undefined8 local_198;
|
||||||
|
undefined8 local_190;
|
||||||
|
undefined4 local_188;
|
||||||
|
undefined2 local_184;
|
||||||
|
undefined8 local_180;
|
||||||
|
undefined8 local_178;
|
||||||
|
undefined8 local_170;
|
||||||
|
undefined8 local_168;
|
||||||
|
undefined2 local_160;
|
||||||
|
undefined4 local_15c;
|
||||||
|
ulonglong local_158;
|
||||||
|
undefined8 uStack_150;
|
||||||
|
undefined4 local_148;
|
||||||
|
undefined2 local_144;
|
||||||
|
undefined4 local_140;
|
||||||
|
undefined **local_138;
|
||||||
|
undefined8 local_130;
|
||||||
|
undefined8 local_128;
|
||||||
|
undefined8 local_120;
|
||||||
|
undefined8 local_118;
|
||||||
|
undefined8 local_110;
|
||||||
|
undefined8 local_108;
|
||||||
|
undefined8 local_100;
|
||||||
|
undefined4 local_f8;
|
||||||
|
undefined2 local_f4;
|
||||||
|
undefined1 local_f2;
|
||||||
|
undefined1 local_f0 [168];
|
||||||
|
ulonglong local_48;
|
||||||
|
|
||||||
|
local_1b8 = 0xfffffffffffffffe;
|
||||||
|
local_48 = DAT_1802db6e0 ^ (ulonglong)auStack_2e8;
|
||||||
|
local_200 = (longlong *)FUN_1800d7370();
|
||||||
|
local_218 = 0;
|
||||||
|
local_208 = 0;
|
||||||
|
local_1f8 = 1;
|
||||||
|
local_1f0 = "FUT Vector";
|
||||||
|
FUN_18004fff0(0,0,local_2b8);
|
||||||
|
local_210 = 0;
|
||||||
|
local_2b0 = 0;
|
||||||
|
uVar3 = FUN_1800fb8c0(param_1,param_2,&local_218,&local_2b0);
|
||||||
|
if ((undefined1 *)*param_3 != (undefined1 *)param_3[1]) {
|
||||||
|
*(undefined1 *)*param_3 = 0;
|
||||||
|
param_3[1] = *param_3;
|
||||||
|
}
|
||||||
|
local_2b4 = uVar3;
|
||||||
|
local_260 = (longlong *)FUN_1800d7370();
|
||||||
|
lVar2 = local_218;
|
||||||
|
local_258 = 1;
|
||||||
|
local_250 = "FUT String";
|
||||||
|
local_278 = &DAT_1802e13a0;
|
||||||
|
local_270 = &DAT_1802e13a0;
|
||||||
|
local_268 = &DAT_1802e13a1;
|
||||||
|
if (uVar3 < 2) {
|
||||||
|
if (uVar3 == 1) {
|
||||||
|
local_1a8 = (longlong *)0x0;
|
||||||
|
local_1a0 = 0;
|
||||||
|
local_198 = 0;
|
||||||
|
local_190 = 0;
|
||||||
|
local_188 = 0;
|
||||||
|
local_184 = 0;
|
||||||
|
local_180 = 0;
|
||||||
|
local_178 = 0;
|
||||||
|
local_170 = 0;
|
||||||
|
local_168 = 0;
|
||||||
|
local_160 = 0x100;
|
||||||
|
lVar8 = -1;
|
||||||
|
local_15c = 0xffffffff;
|
||||||
|
local_158 = _DAT_1801f66a0;
|
||||||
|
uStack_150 = _UNK_1801f66a8;
|
||||||
|
local_148 = 0;
|
||||||
|
local_144 = 0;
|
||||||
|
local_140 = 0;
|
||||||
|
local_130 = 0;
|
||||||
|
local_138 = &PTR_LAB_1801eaac0;
|
||||||
|
local_128 = 0;
|
||||||
|
local_120 = 0;
|
||||||
|
local_118 = 0;
|
||||||
|
local_f4 = 0;
|
||||||
|
local_f2 = 0;
|
||||||
|
local_110 = 0;
|
||||||
|
local_108 = 0;
|
||||||
|
local_100 = 0;
|
||||||
|
local_f8 = 0;
|
||||||
|
memset(local_f0,0,0xa0);
|
||||||
|
local_190 = CONCAT44(local_190._4_4_,*(undefined4 *)(lVar2 + 4)) & 0xffffffff00ffffff;
|
||||||
|
FUN_1801362e0(&local_1a8);
|
||||||
|
FUN_180141660(&local_1a8);
|
||||||
|
uVar6 = FUN_1800d7170();
|
||||||
|
FUN_180009c80(&local_2b0,uVar6);
|
||||||
|
plVar5 = (longlong *)CONCAT44(uStack_2ac,local_2b0);
|
||||||
|
local_2c8 = (undefined4)local_190;
|
||||||
|
(**(code **)(*plVar5 + 0x490))(plVar5,&local_1e8,local_15c,local_158 & 0xffffffff);
|
||||||
|
sVar10 = local_1e0 - (longlong)local_1e8;
|
||||||
|
sVar9 = sVar10;
|
||||||
|
if (0 < (longlong)sVar10) {
|
||||||
|
sVar9 = 0;
|
||||||
|
}
|
||||||
|
iVar4 = memcmp(local_1e8,&DAT_1801e9caf,sVar9);
|
||||||
|
if (((iVar4 == 0) && (-1 < (longlong)sVar10)) && ((longlong)sVar10 < 1)) {
|
||||||
|
local_230 = (longlong *)FUN_1800d7370();
|
||||||
|
local_228 = 1;
|
||||||
|
local_220 = "FUT String";
|
||||||
|
local_248 = &DAT_1802e13a0;
|
||||||
|
local_240 = &DAT_1802e13a0;
|
||||||
|
local_238 = &DAT_1802e13a1;
|
||||||
|
local_290 = (longlong *)FUN_1800d7370();
|
||||||
|
local_288 = 1;
|
||||||
|
local_280 = "FUT String";
|
||||||
|
local_2a8 = &DAT_1802e13a0;
|
||||||
|
local_2a0 = &DAT_1802e13a0;
|
||||||
|
local_298 = &DAT_1802e13a1;
|
||||||
|
FUN_18000fbd0(&local_248,"AWARD_LABEL_%i",*(undefined4 *)(lVar2 + 8));
|
||||||
|
plVar7 = (longlong *)FUN_18019e320();
|
||||||
|
puVar1 = local_248;
|
||||||
|
(**(code **)(*plVar7 + 400))(plVar7,&local_2a8,local_248,1);
|
||||||
|
sVar10 = (longlong)local_2a0 - (longlong)local_2a8;
|
||||||
|
sVar9 = sVar10;
|
||||||
|
if (0 < (longlong)sVar10) {
|
||||||
|
sVar9 = 0;
|
||||||
|
}
|
||||||
|
iVar4 = memcmp(local_2a8,&DAT_1801e9caf,sVar9);
|
||||||
|
if (((iVar4 == 0) && (-1 < (longlong)sVar10)) && ((longlong)sVar10 < 1)) {
|
||||||
|
plVar7 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar7 + 400))(plVar7,&local_2a8,"FUT_UC_ITEMS",1);
|
||||||
|
}
|
||||||
|
FUN_18000fbd0(param_3,&DAT_18021940c,*(undefined4 *)(lVar2 + 0xc));
|
||||||
|
do {
|
||||||
|
lVar8 = lVar8 + 1;
|
||||||
|
} while (local_2a8[lVar8] != '\0');
|
||||||
|
FUN_18000f650(param_3,local_2a8,local_2a8 + lVar8);
|
||||||
|
if (1 < (longlong)local_298 - (longlong)local_2a8) {
|
||||||
|
if (local_2a8 != (undefined1 *)0x0) {
|
||||||
|
(**(code **)(*local_290 + 0x18))(local_290,local_2a8,(int)local_298 - (int)local_2a8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (1 < (longlong)local_238 - (longlong)puVar1) {
|
||||||
|
if (puVar1 != (undefined1 *)0x0) {
|
||||||
|
(**(code **)(*local_230 + 0x18))(local_230,puVar1,(int)local_238 - (int)local_248);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_18000fbd0(param_3,&DAT_18021940c,*(undefined4 *)(lVar2 + 0xc));
|
||||||
|
do {
|
||||||
|
lVar8 = lVar8 + 1;
|
||||||
|
} while (*(char *)((longlong)local_1e8 + lVar8) != '\0');
|
||||||
|
FUN_18000f650(param_3,local_1e8,(longlong)local_1e8 + lVar8);
|
||||||
|
}
|
||||||
|
if ((1 < local_1d8 - (longlong)local_1e8) && (local_1e8 != (void *)0x0)) {
|
||||||
|
(**(code **)(*local_1d0 + 0x18))(local_1d0,local_1e8,(int)local_1d8 - (int)local_1e8);
|
||||||
|
}
|
||||||
|
(**(code **)(*plVar5 + 8))(plVar5);
|
||||||
|
FUN_18000a3c0(&local_138);
|
||||||
|
plVar5 = local_1a8;
|
||||||
|
while (uVar3 = local_2b4, plVar5 != (longlong *)0x0) {
|
||||||
|
plVar7 = (longlong *)plVar5[1];
|
||||||
|
(**(code **)(*plVar5 + 8))();
|
||||||
|
plVar5 = plVar7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
plVar5 = (longlong *)FUN_18019e320();
|
||||||
|
(**(code **)(*plVar5 + 400))(plVar5,&local_278,"FUT_UC_ITEMS",1);
|
||||||
|
FUN_18000fbd0(param_3,"%i %s",local_2b0,local_278);
|
||||||
|
}
|
||||||
|
if (1 < (longlong)local_268 - (longlong)local_278) {
|
||||||
|
if (local_278 != (undefined1 *)0x0) {
|
||||||
|
(**(code **)(*local_260 + 0x18))(local_260,local_278,(int)local_268 - (int)local_278);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FUN_18004fff0(lVar2,local_210,local_2b8);
|
||||||
|
if (lVar2 != 0) {
|
||||||
|
(**(code **)(*local_200 + 0x18))(local_200,lVar2,((local_208 - lVar2) / 0x38) * 0x38);
|
||||||
|
}
|
||||||
|
return uVar3;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== (c) GetAuctionCount deserializer FUN_180163770
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||||
|
|
||||||
|
undefined8 FUN_180163770(undefined8 param_1,undefined8 *param_2)
|
||||||
|
|
||||||
|
{
|
||||||
|
undefined2 uVar1;
|
||||||
|
int iVar2;
|
||||||
|
undefined4 uVar3;
|
||||||
|
undefined8 uVar4;
|
||||||
|
longlong *plVar5;
|
||||||
|
longlong lVar6;
|
||||||
|
undefined1 auStack_168 [32];
|
||||||
|
int local_148 [2];
|
||||||
|
undefined8 local_140;
|
||||||
|
undefined1 local_138 [288];
|
||||||
|
ulonglong local_18;
|
||||||
|
|
||||||
|
local_140 = 0xfffffffffffffffe;
|
||||||
|
local_18 = DAT_1802db6e0 ^ (ulonglong)auStack_168;
|
||||||
|
FUN_1801c63e0(local_138,0,0);
|
||||||
|
local_148[0] = 0x38c;
|
||||||
|
uVar4 = FUN_180008130(*param_2);
|
||||||
|
FUN_1801c8270(local_138,*param_2,uVar4,0);
|
||||||
|
FUN_1801c7f10(local_138);
|
||||||
|
FUN_1801c7f10(local_138);
|
||||||
|
plVar5 = (longlong *)FUN_18011a830();
|
||||||
|
lVar6 = (**(code **)(*plVar5 + 0x130))(plVar5);
|
||||||
|
*(undefined4 *)(lVar6 + 0x30) = 0xffffffff;
|
||||||
|
*(undefined8 *)(lVar6 + 0x34) = 0;
|
||||||
|
iVar2 = FUN_1801c7f10(local_138);
|
||||||
|
while (iVar2 != 10) {
|
||||||
|
iVar2 = FUN_180141ee0(local_148,local_138);
|
||||||
|
if (iVar2 != 6) {
|
||||||
|
if (local_148[0] == 0xbc) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(lVar6 + 0x34) = uVar1;
|
||||||
|
}
|
||||||
|
else if (local_148[0] == 0x1bf) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar3 = FUN_1800d7af0(uVar4);
|
||||||
|
*(undefined4 *)(lVar6 + 0x30) = uVar3;
|
||||||
|
}
|
||||||
|
else if (local_148[0] == 0x1e5) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(lVar6 + 0x3a) = uVar1;
|
||||||
|
}
|
||||||
|
else if (local_148[0] == 0x2b8) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(lVar6 + 0x36) = uVar1;
|
||||||
|
}
|
||||||
|
else if (local_148[0] == 0x2c9) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(lVar6 + 0x38) = uVar1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_180135ff0(local_148[0],local_138);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iVar2 = FUN_1801c7f10(local_138);
|
||||||
|
}
|
||||||
|
*(undefined1 *)(lVar6 + 0x28) = 1;
|
||||||
|
FUN_1801c6560(local_138);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""Q6 — the decisive reads:
|
||||||
|
(a) who builds the /sold request (clear-all-sold) and what it is called;
|
||||||
|
(b) who publishes FUT_TF_SELLING / FUT_TF_SOLD / FUT_TF_WINNING / FUT_TF_OUTBID,
|
||||||
|
i.e. which count feeds the seller's SOLD bucket;
|
||||||
|
(c) the GetAuctionCount deserializer, to pin the five counts atoms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== (a) xrefs: request names + the /sold path suffix")
|
||||||
|
print("=" * 78)
|
||||||
|
for label, a in (("RemoveAllSoldFromTradePile", 0x1801EFAE8),
|
||||||
|
("RemoveFromTradePile", 0x1801EFAD0),
|
||||||
|
("AddToWatchList", 0x1801EFAA8),
|
||||||
|
("/sold suffix", 0x180228BEC),
|
||||||
|
("RS4:FutISRemoveTradeServerResponse", 0x180228BC8)):
|
||||||
|
xs = xrefs_to(a)
|
||||||
|
print(f" {label:36s} {len(xs)} xref(s)")
|
||||||
|
for x in xs[:10]:
|
||||||
|
print(f" 0x{x[0]:x} {x[1]:12s} in {x[2]} (entry 0x{x[3]:x})")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== (b) xrefs: the Transfer List caption keys")
|
||||||
|
print("=" * 78)
|
||||||
|
caps = (("FUT_TOTAL_AUCTIONS", 0x18020A080), ("FUT_UC_ITEMS", 0x18020A098),
|
||||||
|
("FUT_TF_SELLING", 0x18020A0A8), ("FUT_TF_SOLD", 0x18020A0C0),
|
||||||
|
("FUT_TF_WINNING", 0x18020A0D8), ("FUT_TF_OUTBID", 0x18020A0E8))
|
||||||
|
owners = {}
|
||||||
|
for label, a in caps:
|
||||||
|
xs = xrefs_to(a)
|
||||||
|
print(f" {label:20s} {len(xs)} xref(s)")
|
||||||
|
for x in xs[:10]:
|
||||||
|
print(f" 0x{x[0]:x} {x[1]:12s} in {x[2]} (entry 0x{x[3]:x})")
|
||||||
|
owners[x[3]] = owners.get(x[3], 0) + 1
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(" caption publisher candidates (entry -> #caption refs):",
|
||||||
|
{hex(k): v for k, v in sorted(owners.items(), key=lambda kv: -kv[1])})
|
||||||
|
|
||||||
|
for ent in sorted(owners, key=lambda e: -owners[e])[:2]:
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"== decompiled caption publisher 0x{ent:x}")
|
||||||
|
print("=" * 78)
|
||||||
|
print(dec(ent))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== (c) GetAuctionCount deserializer FUN_180163770")
|
||||||
|
print("=" * 78)
|
||||||
|
print(dec(0x180163770))
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
==============================================================================
|
||||||
|
== (a) hub tradePile sub-deserializer 0x18013ead0
|
||||||
|
== atom ids of interest: count=0xbc notification=0x1da selling=0x2b8 sold=0x2c9
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
undefined4 FUN_18013ead0(longlong param_1,longlong param_2,longlong param_3)
|
||||||
|
|
||||||
|
{
|
||||||
|
undefined2 uVar1;
|
||||||
|
int iVar2;
|
||||||
|
int iVar3;
|
||||||
|
undefined8 uVar4;
|
||||||
|
|
||||||
|
iVar3 = 0x38c;
|
||||||
|
iVar2 = FUN_1801c7f10(param_3);
|
||||||
|
do {
|
||||||
|
if (iVar2 == 10) {
|
||||||
|
*(undefined1 *)(param_1 + 0x1da) = 1;
|
||||||
|
return 10;
|
||||||
|
}
|
||||||
|
if (*(int *)(param_3 + 0xd0) == 0xb) {
|
||||||
|
iVar3 = FUN_180180d00(*(undefined8 *)(param_3 + 0xf8));
|
||||||
|
iVar2 = FUN_1801c7f10(param_3);
|
||||||
|
if (iVar2 != 6) goto LAB_18013eb2e;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
LAB_18013eb2e:
|
||||||
|
if (iVar3 == 0xbc) {
|
||||||
|
uVar4 = FUN_1801c79d0(param_3);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(param_1 + 0x1d4) = uVar1;
|
||||||
|
}
|
||||||
|
else if (iVar3 == 0x1da) {
|
||||||
|
uVar4 = FUN_1801c79d0(param_3);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(param_1 + 0x1d6) = uVar1;
|
||||||
|
}
|
||||||
|
else if (iVar3 == 0x2b8) {
|
||||||
|
uVar4 = FUN_1801c79d0(param_3);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(param_1 + 0x1d2) = uVar1;
|
||||||
|
uVar4 = FUN_1801c79d0(param_3);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(param_2 + 0x36) = uVar1;
|
||||||
|
}
|
||||||
|
else if (iVar3 == 0x2c9) {
|
||||||
|
uVar4 = FUN_1801c79d0(param_3);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(param_1 + 0x1d8) = uVar1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_180135ff0(iVar3,param_3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iVar2 = FUN_1801c7f10(param_3);
|
||||||
|
} while( true );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== (b) /sold request builder 0x1801647c0
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||||
|
|
||||||
|
void FUN_1801647c0(longlong param_1,undefined8 param_2,undefined4 param_3)
|
||||||
|
|
||||||
|
{
|
||||||
|
undefined1 auStack_58 [32];
|
||||||
|
undefined1 local_38;
|
||||||
|
undefined8 local_37;
|
||||||
|
undefined8 local_2f;
|
||||||
|
undefined8 local_27;
|
||||||
|
undefined4 local_1f;
|
||||||
|
undefined2 local_1b;
|
||||||
|
undefined1 local_19;
|
||||||
|
ulonglong local_18;
|
||||||
|
|
||||||
|
local_18 = DAT_1802db6e0 ^ (ulonglong)auStack_58;
|
||||||
|
local_38 = 0;
|
||||||
|
local_37 = 0;
|
||||||
|
local_2f = 0;
|
||||||
|
local_27 = 0;
|
||||||
|
local_1f = 0;
|
||||||
|
local_1b = 0;
|
||||||
|
local_19 = 0;
|
||||||
|
if (*(longlong *)(param_1 + 0x10) == 0) {
|
||||||
|
FUN_180007f80(&local_38,0x20,"/sold");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_180007f80(&local_38,0x20,"/%lld");
|
||||||
|
}
|
||||||
|
FUN_180008100(param_2,&local_38,param_3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== (c) FutGetAuctionCount deserializer 0x180163770
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
|
/* WARNING: Function: __security_check_cookie replaced with injection: security_check_cookie */
|
||||||
|
|
||||||
|
undefined8 FUN_180163770(undefined8 param_1,undefined8 *param_2)
|
||||||
|
|
||||||
|
{
|
||||||
|
undefined2 uVar1;
|
||||||
|
int iVar2;
|
||||||
|
undefined4 uVar3;
|
||||||
|
undefined8 uVar4;
|
||||||
|
longlong *plVar5;
|
||||||
|
longlong lVar6;
|
||||||
|
undefined1 auStack_168 [32];
|
||||||
|
int local_148 [2];
|
||||||
|
undefined8 local_140;
|
||||||
|
undefined1 local_138 [288];
|
||||||
|
ulonglong local_18;
|
||||||
|
|
||||||
|
local_140 = 0xfffffffffffffffe;
|
||||||
|
local_18 = DAT_1802db6e0 ^ (ulonglong)auStack_168;
|
||||||
|
FUN_1801c63e0(local_138,0,0);
|
||||||
|
local_148[0] = 0x38c;
|
||||||
|
uVar4 = FUN_180008130(*param_2);
|
||||||
|
FUN_1801c8270(local_138,*param_2,uVar4,0);
|
||||||
|
FUN_1801c7f10(local_138);
|
||||||
|
FUN_1801c7f10(local_138);
|
||||||
|
plVar5 = (longlong *)FUN_18011a830();
|
||||||
|
lVar6 = (**(code **)(*plVar5 + 0x130))(plVar5);
|
||||||
|
*(undefined4 *)(lVar6 + 0x30) = 0xffffffff;
|
||||||
|
*(undefined8 *)(lVar6 + 0x34) = 0;
|
||||||
|
iVar2 = FUN_1801c7f10(local_138);
|
||||||
|
while (iVar2 != 10) {
|
||||||
|
iVar2 = FUN_180141ee0(local_148,local_138);
|
||||||
|
if (iVar2 != 6) {
|
||||||
|
if (local_148[0] == 0xbc) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(lVar6 + 0x34) = uVar1;
|
||||||
|
}
|
||||||
|
else if (local_148[0] == 0x1bf) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar3 = FUN_1800d7af0(uVar4);
|
||||||
|
*(undefined4 *)(lVar6 + 0x30) = uVar3;
|
||||||
|
}
|
||||||
|
else if (local_148[0] == 0x1e5) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(lVar6 + 0x3a) = uVar1;
|
||||||
|
}
|
||||||
|
else if (local_148[0] == 0x2b8) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(lVar6 + 0x36) = uVar1;
|
||||||
|
}
|
||||||
|
else if (local_148[0] == 0x2c9) {
|
||||||
|
uVar4 = FUN_1801c79d0(local_138);
|
||||||
|
uVar1 = FUN_1800d7b10(uVar4);
|
||||||
|
*(undefined2 *)(lVar6 + 0x38) = uVar1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
FUN_180135ff0(local_148[0],local_138);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iVar2 = FUN_1801c7f10(local_138);
|
||||||
|
}
|
||||||
|
*(undefined1 *)(lVar6 + 0x28) = 1;
|
||||||
|
FUN_1801c6560(local_138);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== callers of the hub tradePile sub-deserializer (which response embeds it)
|
||||||
|
==============================================================================
|
||||||
|
0x180139610 FUN_180139610
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""Q7 — close it:
|
||||||
|
(a) hub tradePile sub-deserializer 0x18013ead0: which ATOM writes +0x1d8 (the
|
||||||
|
offset the FUT_TF_SOLD caption reads)?
|
||||||
|
(b) the /sold request builder 0x1801647c0 — bulk clear, or per-trade?
|
||||||
|
(c) FutGetAuctionCount deserializer 0x180163770 — the five counts atoms.
|
||||||
|
"""
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== (a) hub tradePile sub-deserializer 0x18013ead0")
|
||||||
|
print("== atom ids of interest: count=0xbc notification=0x1da selling=0x2b8 sold=0x2c9")
|
||||||
|
print("=" * 78)
|
||||||
|
print(dec(0x18013EAD0))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== (b) /sold request builder 0x1801647c0")
|
||||||
|
print("=" * 78)
|
||||||
|
print(dec(0x1801647C0))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== (c) FutGetAuctionCount deserializer 0x180163770")
|
||||||
|
print("=" * 78)
|
||||||
|
print(dec(0x180163770))
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== callers of the hub tradePile sub-deserializer (which response embeds it)")
|
||||||
|
print("=" * 78)
|
||||||
|
for a, n in callers(0x18013EAD0):
|
||||||
|
print(f" 0x{a:x} {n}")
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""Q8 — are atoms 0x36..0x3c (auctionLost*/auctionSold*/auctionWon*) real response
|
||||||
|
keys? If a deserializer dispatches on them, the server tells the client HOW an
|
||||||
|
auction concluded via its own atom, and bidState is not the carrier.
|
||||||
|
|
||||||
|
Signature of a genuine atom dispatcher: the SAME local compared against many atom
|
||||||
|
ids, with FUN_180135ff0 (value-SKIP) as the else, and FUN_1801c7f10 driving the loop.
|
||||||
|
"""
|
||||||
|
|
||||||
|
CANDIDATES = (0x18012B370, 0x1801071F0, 0x1800EE7C0, 0x1800FB5B0, 0x1800CF3B0)
|
||||||
|
|
||||||
|
for ent in CANDIDATES:
|
||||||
|
print("=" * 78)
|
||||||
|
print(f"== 0x{ent:x} callees include value-SKIP? "
|
||||||
|
f"{any(c[0] == 0x180135FF0 for c in callees(ent))}"
|
||||||
|
f" atom-loop? {any(c[0] == 0x1801C7F10 for c in callees(ent))}")
|
||||||
|
print("=" * 78)
|
||||||
|
body = dec(ent)
|
||||||
|
# Only print if it smells like an atom dispatcher, else just report.
|
||||||
|
if "FUN_180135ff0" in body or "FUN_1801c7f10" in body:
|
||||||
|
print(body[:6000])
|
||||||
|
else:
|
||||||
|
print(" NOT an atom dispatcher (no value-SKIP, no atom loop) — small-immediate noise")
|
||||||
|
print(f" size {len(body)} chars; first 400:")
|
||||||
|
print(body[:400])
|
||||||
|
print()
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
==============================================================================
|
||||||
|
== localisation / string keys about tax, fee, net proceeds
|
||||||
|
==============================================================================
|
||||||
|
TAX (no printable hits)
|
||||||
|
tax (no printable hits)
|
||||||
|
Tax (no printable hits)
|
||||||
|
FEE 0x18023a3a8 'FEET_ATTEMPTED'
|
||||||
|
FEE 0x18023a3c0 'FEET_COMPLETED'
|
||||||
|
_FEE 0x18023a3a7 '_FEET_ATTEMPTED'
|
||||||
|
_FEE 0x18023a3bf '_FEET_COMPLETED'
|
||||||
|
fee (no printable hits)
|
||||||
|
NET 0x180209b1c 'NETOURNAMENT_0'
|
||||||
|
NET 0x180209b3c 'NETOURNAMENT_1'
|
||||||
|
NET 0x18020dd10 'NET'
|
||||||
|
_NET 0x18020dd0f '_NET'
|
||||||
|
RECEIVE 0x18023933f 'RECEIVER'
|
||||||
|
RECEIVE 0x180239350 'RECEIVER'
|
||||||
|
RECEIVE 0x18023983f 'RECEIVER'
|
||||||
|
RECEIVE 0x18023990d 'RECEIVER'
|
||||||
|
RECEIVE 0x180239a0b 'RECEIVER'
|
||||||
|
RECEIVE 0x18023a06f 'RECEIVED'
|
||||||
|
Receive (no printable hits)
|
||||||
|
PROCEEDS (no printable hits)
|
||||||
|
COMMISSION (no printable hits)
|
||||||
|
EA_TAX (no printable hits)
|
||||||
|
FUT_TF_ 0x18020a0a8 'FUT_TF_SELLING'
|
||||||
|
FUT_TF_ 0x18020a0c0 'FUT_TF_SOLD'
|
||||||
|
FUT_TF_ 0x18020a0d8 'FUT_TF_WINNING'
|
||||||
|
FUT_TF_ 0x18020a0e8 'FUT_TF_OUTBID'
|
||||||
|
AFTER_TAX (no printable hits)
|
||||||
|
earnings (no printable hits)
|
||||||
|
Earnings (no printable hits)
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== float constants 0.95 / 0.05 / 0.95f / 0.05f
|
||||||
|
==============================================================================
|
||||||
|
double 0.95 ABSENT
|
||||||
|
double 0.05 ABSENT
|
||||||
|
float 0.95f ABSENT
|
||||||
|
float 0.05f ABSENT
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
|
== functions using BOTH 95 (or 5) and 100 as immediates — integer fee math
|
||||||
|
==============================================================================
|
||||||
|
17 candidate(s)
|
||||||
|
0x18000b5c0 FUN_18000b5c0 immediates [5, 100]
|
||||||
|
0x180051cd0 FUN_180051cd0 immediates [5, 20, 100]
|
||||||
|
0x180057b00 FUN_180057b00 immediates [5, 100]
|
||||||
|
0x180082c30 FUN_180082c30 immediates [5, 100]
|
||||||
|
0x180088cb0 FUN_180088cb0 immediates [5, 100]
|
||||||
|
0x1800a3cb0 FUN_1800a3cb0 immediates [5, 100]
|
||||||
|
0x1800a47b0 FUN_1800a47b0 immediates [5, 100]
|
||||||
|
0x1800d5050 FUN_1800d5050 immediates [5, 20, 100]
|
||||||
|
0x1800d5450 FUN_1800d5450 immediates [5, 20, 100]
|
||||||
|
0x1801129f0 FUN_1801129f0 immediates [5, 100]
|
||||||
|
0x18013af30 FUN_18013af30 immediates [5, 20, 100]
|
||||||
|
0x18013ec10 FUN_18013ec10 immediates [5, 100]
|
||||||
|
0x18013fe00 FUN_18013fe00 immediates [5, 100]
|
||||||
|
0x180147070 FUN_180147070 immediates [5, 100]
|
||||||
|
0x180180ea0 FUN_180180ea0 immediates [5, 100]
|
||||||
|
0x1801adae0 FUN_1801adae0 immediates [5, 100]
|
||||||
|
0x1801b9e60 FUN_1801b9e60 immediates [5, 100]
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
"""Task B — does FIFA17.exe itself compute the transfer fee / net proceeds?
|
||||||
|
|
||||||
|
If the list-item confirmation screen shows "you will receive N", the rounding rule
|
||||||
|
is IN THE BINARY and needs no live measurement. Look for:
|
||||||
|
* localisation keys mentioning tax/fee/net/receive
|
||||||
|
* float constants 0.95 / 0.05
|
||||||
|
* integer 95 or 5 used with 100 in the same function
|
||||||
|
"""
|
||||||
|
import struct
|
||||||
|
|
||||||
|
print("=" * 78)
|
||||||
|
print("== localisation / string keys about tax, fee, net proceeds")
|
||||||
|
print("=" * 78)
|
||||||
|
for needle in (b"TAX", b"tax", b"Tax", b"FEE", b"_FEE", b"fee", b"NET", b"_NET",
|
||||||
|
b"RECEIVE", b"Receive", b"PROCEEDS", b"COMMISSION", b"EA_TAX",
|
||||||
|
b"FUT_TF_", b"AFTER_TAX", b"earnings", b"Earnings"):
|
||||||
|
hits = find_all(needle, blocks=(".rdata", ".data"))
|
||||||
|
shown = 0
|
||||||
|
for h in hits:
|
||||||
|
s = rd_str(h, 70)
|
||||||
|
if s and len(s) > 2 and s.isprintable():
|
||||||
|
print(f" {needle.decode():10s} 0x{h:x} {s!r}")
|
||||||
|
shown += 1
|
||||||
|
if shown >= 12:
|
||||||
|
break
|
||||||
|
if not shown:
|
||||||
|
print(f" {needle.decode():10s} (no printable hits)")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== float constants 0.95 / 0.05 / 0.95f / 0.05f")
|
||||||
|
print("=" * 78)
|
||||||
|
for label, pat in (("double 0.95", struct.pack("<d", 0.95)),
|
||||||
|
("double 0.05", struct.pack("<d", 0.05)),
|
||||||
|
("float 0.95f", struct.pack("<f", 0.95)),
|
||||||
|
("float 0.05f", struct.pack("<f", 0.05))):
|
||||||
|
hits = find_all(pat, blocks=(".rdata", ".data"))
|
||||||
|
print(f" {label:12s} {[hex(h) for h in hits[:10]] or 'ABSENT'}")
|
||||||
|
for h in hits[:6]:
|
||||||
|
xs = xrefs_to(h)
|
||||||
|
for x in xs[:4]:
|
||||||
|
print(f" used at 0x{x[0]:x} in {x[2]} (entry 0x{x[3]:x})")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 78)
|
||||||
|
print("== functions using BOTH 95 (or 5) and 100 as immediates — integer fee math")
|
||||||
|
print("=" * 78)
|
||||||
|
found = []
|
||||||
|
for f in fm.getFunctions(True):
|
||||||
|
ent = int(f.getEntryPoint().getOffset())
|
||||||
|
vals = set()
|
||||||
|
it = listing.getInstructions(f.getBody(), True)
|
||||||
|
while it.hasNext():
|
||||||
|
ins = it.next()
|
||||||
|
if ins.getMnemonicString() not in ("IMUL", "MOV", "CMP", "ADD", "SUB", "LEA", "SHL"):
|
||||||
|
continue
|
||||||
|
for i in range(ins.getNumOperands()):
|
||||||
|
for o in ins.getOpObjects(i):
|
||||||
|
try:
|
||||||
|
v = int(o.getValue())
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
if v in (95, 100, 5, 20):
|
||||||
|
vals.add(v)
|
||||||
|
if 100 in vals and (95 in vals or 5 in vals):
|
||||||
|
found.append((ent, f.getName(), sorted(vals)))
|
||||||
|
print(f" {len(found)} candidate(s)")
|
||||||
|
for ent, nm, vals in found[:40]:
|
||||||
|
print(f" 0x{ent:x} {nm:26s} immediates {vals}")
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{
|
||||||
|
"champion": {
|
||||||
|
"bytes": 2,
|
||||||
|
"path": "/ut/game/fifa17/champion",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"clubUser": {
|
||||||
|
"bytes": 2,
|
||||||
|
"path": "/ut/game/fifa17/clubUser",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"defid": {
|
||||||
|
"bytes": 600,
|
||||||
|
"path": "/ut/game/fifa17/defid?definitionId=200389",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"defs_resource": {
|
||||||
|
"bytes": 600,
|
||||||
|
"path": "/ut/game/fifa17/item/resource?resourceId=200389",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"draft_state": {
|
||||||
|
"bytes": 118,
|
||||||
|
"path": "/ut/game/fifa17/squad/mode/draft/state",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"marketdata": {
|
||||||
|
"bytes": 110,
|
||||||
|
"path": "/ut/game/fifa17/marketdata/pricelimits?defId=200389,200104",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"sbs_sets": {
|
||||||
|
"bytes": 537,
|
||||||
|
"path": "/ut/game/fifa17/sbs/sets",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"season": {
|
||||||
|
"bytes": 2,
|
||||||
|
"path": "/ut/game/fifa17/season",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"squad_0": {
|
||||||
|
"bytes": 7792,
|
||||||
|
"path": "/ut/game/fifa17/squad/0",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tournament": {
|
||||||
|
"bytes": 2,
|
||||||
|
"path": "/ut/game/fifa17/tournament",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"bytes": 751,
|
||||||
|
"path": "/ut/game/fifa17/user",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"user_list": {
|
||||||
|
"bytes": 2,
|
||||||
|
"path": "/ut/game/fifa17/user/list",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"watchList": {
|
||||||
|
"bytes": 52,
|
||||||
|
"path": "/ut/game/fifa17/watchList",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"itemData": [
|
||||||
|
{
|
||||||
|
"assetId": 200389,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 70
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardType": 0,
|
||||||
|
"cardassetid": 200389,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"commodityId": 200389,
|
||||||
|
"commonName": "Player",
|
||||||
|
"definitionId": 200389,
|
||||||
|
"id": 200389,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"lastName": "Player",
|
||||||
|
"leagueId": 0,
|
||||||
|
"name": "Player",
|
||||||
|
"nation": 0,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 200389,
|
||||||
|
"teamid": 0,
|
||||||
|
"untradeable": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"itemData": [
|
||||||
|
{
|
||||||
|
"assetId": 200389,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 70
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardType": 0,
|
||||||
|
"cardassetid": 200389,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"commodityId": 200389,
|
||||||
|
"commonName": "Player",
|
||||||
|
"definitionId": 200389,
|
||||||
|
"id": 200389,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"lastName": "Player",
|
||||||
|
"leagueId": 0,
|
||||||
|
"name": "Player",
|
||||||
|
"nation": 0,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 200389,
|
||||||
|
"teamid": 0,
|
||||||
|
"untradeable": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"gamesWonCurrentMatch": 0,
|
||||||
|
"roundsInfo": [],
|
||||||
|
"squadState": "INVALID",
|
||||||
|
"stateParam1": "INVALID",
|
||||||
|
"stateParam2": "0"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"defId": 200389,
|
||||||
|
"maxPrice": 15000,
|
||||||
|
"minPrice": 150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"defId": 200104,
|
||||||
|
"maxPrice": 15000,
|
||||||
|
"minPrice": 150
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"categories": [
|
||||||
|
{
|
||||||
|
"categoryId": 1,
|
||||||
|
"name": "Foundations",
|
||||||
|
"priority": 1,
|
||||||
|
"sets": [
|
||||||
|
{
|
||||||
|
"awards": [],
|
||||||
|
"categoryId": 1,
|
||||||
|
"challengesCompletedCount": 0,
|
||||||
|
"challengesCount": 1,
|
||||||
|
"description": "Submit an 11-player squad.",
|
||||||
|
"endTime": 4102444800,
|
||||||
|
"hidden": false,
|
||||||
|
"name": "Bronze Challenge",
|
||||||
|
"priority": 1,
|
||||||
|
"setId": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"awards": [],
|
||||||
|
"categoryId": 1,
|
||||||
|
"challengesCompletedCount": 0,
|
||||||
|
"challengesCount": 1,
|
||||||
|
"description": "Get started with your first SBC.",
|
||||||
|
"endTime": 4102444800,
|
||||||
|
"hidden": false,
|
||||||
|
"name": "Simple Start",
|
||||||
|
"priority": 2,
|
||||||
|
"setId": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,707 @@
|
|||||||
|
{
|
||||||
|
"actives": [],
|
||||||
|
"captain": 100000001,
|
||||||
|
"changed": 0,
|
||||||
|
"chemistry": 49,
|
||||||
|
"custom": "[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,50,50,0,50,40,65,0,65,50,50,1]",
|
||||||
|
"formation": "f433",
|
||||||
|
"id": 0,
|
||||||
|
"kicktakers": [
|
||||||
|
{
|
||||||
|
"dream": false,
|
||||||
|
"id": 100000001,
|
||||||
|
"index": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dream": false,
|
||||||
|
"id": 100000001,
|
||||||
|
"index": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dream": false,
|
||||||
|
"id": 100000001,
|
||||||
|
"index": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dream": false,
|
||||||
|
"id": 100000001,
|
||||||
|
"index": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"dream": false,
|
||||||
|
"id": 100000001,
|
||||||
|
"index": 4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"manager": [
|
||||||
|
{
|
||||||
|
"dream": false,
|
||||||
|
"id": 100000427
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"personaId": 33068179,
|
||||||
|
"players": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 200389,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 83
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 77
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 87
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 200389,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 200389,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000003,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 44,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "GK",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 87,
|
||||||
|
"resourceId": 200389,
|
||||||
|
"teamid": 240,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 197445,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 73
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 83
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 83
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 73
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 197445,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 197445,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000006,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 19,
|
||||||
|
"nation": 4,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 87,
|
||||||
|
"resourceId": 197445,
|
||||||
|
"teamid": 21,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 155862,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 63
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 87
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 83
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 155862,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 155862,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000005,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 45,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 89,
|
||||||
|
"resourceId": 155862,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 182521,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 80
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 88
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 79
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 70
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 182521,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 182521,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000008,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 21,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 88,
|
||||||
|
"resourceId": 182521,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 189332,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 83
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 75
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 189332,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 189332,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000007,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 45,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 86,
|
||||||
|
"resourceId": 189332,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 26
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 61
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 158023,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000002,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 158023,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 6,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 183907,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 79
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 68
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 85
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 183907,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 183907,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000004,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 19,
|
||||||
|
"nation": 21,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 90,
|
||||||
|
"resourceId": 183907,
|
||||||
|
"teamid": 21,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 7,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 183277,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 64
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 183277,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 183277,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000009,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 13,
|
||||||
|
"nation": 7,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LM",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 88,
|
||||||
|
"resourceId": 183277,
|
||||||
|
"teamid": 5,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 8,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 176580,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 79
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 87
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 42
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 79
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 176580,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 176580,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000010,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 60,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 92,
|
||||||
|
"resourceId": 176580,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 9,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 188545,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 87
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 74
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 85
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 82
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 188545,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 188545,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000025,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 19,
|
||||||
|
"nation": 37,
|
||||||
|
"owners": 1,
|
||||||
|
"pile": "club",
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 90,
|
||||||
|
"resourceId": 188545,
|
||||||
|
"teamid": 21,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 11
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 10,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 20801,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 81
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 80
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 20801,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 20801,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000001,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 38,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LW",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 94,
|
||||||
|
"resourceId": 20801,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": true
|
||||||
|
},
|
||||||
|
"kitNumber": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 11,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 12,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 13,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 14,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 15,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 16,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 17,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 18,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 19,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 20,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 21,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 22,
|
||||||
|
"itemData": {
|
||||||
|
"dream": false,
|
||||||
|
"id": 0
|
||||||
|
},
|
||||||
|
"kitNumber": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rating": 90,
|
||||||
|
"squadName": "OpenFUT",
|
||||||
|
"squadType": "REGULAR_SQUAD",
|
||||||
|
"starRating": 90
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"userInfo": {
|
||||||
|
"accountCreatedPlatformName": "pc",
|
||||||
|
"actives": [],
|
||||||
|
"bidTokens": {
|
||||||
|
"count": 0,
|
||||||
|
"updateTime": 0
|
||||||
|
},
|
||||||
|
"clubAbbr": "OFC",
|
||||||
|
"clubName": "OpenFUT",
|
||||||
|
"clubNameChangeAllowed": false,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"active": true,
|
||||||
|
"finalFunds": 29876776,
|
||||||
|
"funds": 29876776,
|
||||||
|
"name": "coins"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"active": true,
|
||||||
|
"finalFunds": 0,
|
||||||
|
"funds": 0,
|
||||||
|
"name": "points"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"divisionOffline": 10,
|
||||||
|
"divisionOnline": 10,
|
||||||
|
"draw": 0,
|
||||||
|
"established": "2026",
|
||||||
|
"feature": {},
|
||||||
|
"loss": 0,
|
||||||
|
"personaId": 33068179,
|
||||||
|
"purchased": false,
|
||||||
|
"reliability": {
|
||||||
|
"matchUnfinishedTime": 0,
|
||||||
|
"reliability": 100
|
||||||
|
},
|
||||||
|
"sessionCoinsBankBalance": 0,
|
||||||
|
"squadList": {
|
||||||
|
"squad": [
|
||||||
|
{
|
||||||
|
"chemistry": 49,
|
||||||
|
"formation": "f433",
|
||||||
|
"id": 0,
|
||||||
|
"rating": 89,
|
||||||
|
"squadName": "OpenFUT",
|
||||||
|
"squadType": "REGULAR_SQUAD"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"trophies": 0,
|
||||||
|
"won": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"auctionInfo": [],
|
||||||
|
"credits": 29876776,
|
||||||
|
"total": 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||||
|
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||||
|
},
|
||||||
|
"surfaces": {
|
||||||
|
"counts": {
|
||||||
|
"body": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"sha256": "caecc67a615ddd0f209b36c7c8ea6a04e0002552d9a42b8eeca20df84feedf3c",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_filtered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_unfiltered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||||
|
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "buyNow"
|
||||||
|
},
|
||||||
|
"surfaces": {
|
||||||
|
"counts": {
|
||||||
|
"body": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "buyNow",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"sha256": "0b6e5cc5ab58fdbdd76586b1cff685a562e669fd37750cbd1397db9ee9e97407",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_filtered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "buyNow",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "fab8d43b7ac0f0e09d6902155f7c5f6d91e57b4b595ec8f6093a5a9dcb02bcdb",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_unfiltered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "buyNow",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "fab8d43b7ac0f0e09d6902155f7c5f6d91e57b4b595ec8f6093a5a9dcb02bcdb",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||||
|
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||||
|
},
|
||||||
|
"surfaces": {
|
||||||
|
"counts": {
|
||||||
|
"body": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"sha256": "caecc67a615ddd0f209b36c7c8ea6a04e0002552d9a42b8eeca20df84feedf3c",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_filtered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_unfiltered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "1",
|
||||||
|
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||||
|
},
|
||||||
|
"surfaces": {
|
||||||
|
"counts": {
|
||||||
|
"body": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 1,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"sha256": "6e228a34b5d7fac6bf70710cbd4ca35b58740e04ae455ffeee04b6ce7faab8e5",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_filtered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 1,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "c1557013bf20285d0512ca0d055a16df8036260559d640da11102a2379ac5986",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_unfiltered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 1,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "c1557013bf20285d0512ca0d055a16df8036260559d640da11102a2379ac5986",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,509 @@
|
|||||||
|
{
|
||||||
|
"counts_states": {
|
||||||
|
"S1_zero_active_one_sold": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"S2_one_active_one_sold_count_active": {
|
||||||
|
"count": 1,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 1,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"S2_one_active_one_sold_count_membership": {
|
||||||
|
"count": 2,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 1,
|
||||||
|
"sold": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diffs": {
|
||||||
|
"orthogonal_coinsprocessed": [
|
||||||
|
{
|
||||||
|
"a": 0,
|
||||||
|
"b": 1,
|
||||||
|
"kind": "VALUE_MISMATCH",
|
||||||
|
"path": "auctionInfo[0].coinsProcessed"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primary_bidstate": [
|
||||||
|
{
|
||||||
|
"a": "highest",
|
||||||
|
"b": "buyNow",
|
||||||
|
"kind": "VALUE_MISMATCH",
|
||||||
|
"path": "auctionInfo[0].bidState"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"generated": "2026-08-18T02:19:25Z",
|
||||||
|
"settlement": {
|
||||||
|
"buyer": 19850,
|
||||||
|
"fee": 7,
|
||||||
|
"instances": 1,
|
||||||
|
"owner": "club-buyer-b",
|
||||||
|
"seller": 1143
|
||||||
|
},
|
||||||
|
"variants": {
|
||||||
|
"A_highest": {
|
||||||
|
"env": {
|
||||||
|
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||||
|
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||||
|
},
|
||||||
|
"surfaces": {
|
||||||
|
"counts": {
|
||||||
|
"body": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"sha256": "caecc67a615ddd0f209b36c7c8ea6a04e0002552d9a42b8eeca20df84feedf3c",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_filtered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_unfiltered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"B_buyNow": {
|
||||||
|
"env": {
|
||||||
|
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||||
|
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "buyNow"
|
||||||
|
},
|
||||||
|
"surfaces": {
|
||||||
|
"counts": {
|
||||||
|
"body": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "buyNow",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"sha256": "0b6e5cc5ab58fdbdd76586b1cff685a562e669fd37750cbd1397db9ee9e97407",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_filtered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "buyNow",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "fab8d43b7ac0f0e09d6902155f7c5f6d91e57b4b595ec8f6093a5a9dcb02bcdb",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_unfiltered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "buyNow",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "fab8d43b7ac0f0e09d6902155f7c5f6d91e57b4b595ec8f6093a5a9dcb02bcdb",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"C_cp0": {
|
||||||
|
"env": {
|
||||||
|
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "0",
|
||||||
|
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||||
|
},
|
||||||
|
"surfaces": {
|
||||||
|
"counts": {
|
||||||
|
"body": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"sha256": "caecc67a615ddd0f209b36c7c8ea6a04e0002552d9a42b8eeca20df84feedf3c",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_filtered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_unfiltered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "0107bf70809cce10fa5fa0f7f4e196f7ffdd6def1b9f63407eeffc7e66f35855",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"D_cp1": {
|
||||||
|
"env": {
|
||||||
|
"OPENFUT_FIFA17_SOLD_COINS_PROCESSED": "1",
|
||||||
|
"OPENFUT_FIFA17_SOLD_EXPERIMENT": "highest"
|
||||||
|
},
|
||||||
|
"surfaces": {
|
||||||
|
"counts": {
|
||||||
|
"body": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 1,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"sha256": "6e228a34b5d7fac6bf70710cbd4ca35b58740e04ae455ffeee04b6ce7faab8e5",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_filtered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 1,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "c1557013bf20285d0512ca0d055a16df8036260559d640da11102a2379ac5986",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_unfiltered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 1,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 212188,
|
||||||
|
"id": 100000178,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rating": 75,
|
||||||
|
"resourceId": 212188,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "c1557013bf20285d0512ca0d055a16df8036260559d640da11102a2379ac5986",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,850 @@
|
|||||||
|
{
|
||||||
|
"captured": "2026-08-18T02:21:49Z",
|
||||||
|
"phase": "0 \u2014 staging validated, sold row present, awaiting live client",
|
||||||
|
"settlement": {
|
||||||
|
"clubs": {
|
||||||
|
"club-buyer-b": 19850,
|
||||||
|
"club-seller-a-cage": 1143
|
||||||
|
},
|
||||||
|
"disposable_owner": [
|
||||||
|
"club-buyer-b"
|
||||||
|
],
|
||||||
|
"fee": 7,
|
||||||
|
"gross": 150,
|
||||||
|
"instances": 1,
|
||||||
|
"proceeds": 143
|
||||||
|
},
|
||||||
|
"sold_row": {
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 232273,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 53
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 50
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 232273,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 232273,
|
||||||
|
"discardValue": 150,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000012,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 32,
|
||||||
|
"nation": 117,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 51,
|
||||||
|
"resourceId": 232273,
|
||||||
|
"teamid": 112487,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
},
|
||||||
|
"surfaces": {
|
||||||
|
"club": {
|
||||||
|
"body": {
|
||||||
|
"itemData": [
|
||||||
|
{
|
||||||
|
"assetId": 20801,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 97
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 97
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 96
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 38
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 85
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 20801,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 83906881,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000001,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 38,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LW",
|
||||||
|
"rareflag": 11,
|
||||||
|
"rating": 99,
|
||||||
|
"resourceId": 83906881,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 158023,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 94
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 95
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 99
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 31
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 66
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 158023,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 84044103,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000002,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 52,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RW",
|
||||||
|
"rareflag": 11,
|
||||||
|
"rating": 98,
|
||||||
|
"resourceId": 84044103,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 167495,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 94
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 95
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 99
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 94
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 96
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 167495,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 84053575,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000003,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 19,
|
||||||
|
"nation": 21,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "GK",
|
||||||
|
"rareflag": 11,
|
||||||
|
"rating": 97,
|
||||||
|
"resourceId": 84053575,
|
||||||
|
"teamid": 21,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 176580,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 85
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 45
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 82
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 176580,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 117617092,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000004,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 60,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "ST",
|
||||||
|
"rareflag": 22,
|
||||||
|
"rating": 95,
|
||||||
|
"resourceId": 117617092,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 177003,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 78
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 79
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 76
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 70
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 177003,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 151171947,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000005,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 10,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 24,
|
||||||
|
"rating": 93,
|
||||||
|
"resourceId": 151171947,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 164240,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 77
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 76
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 76
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 93
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 82
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 164240,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 134381968,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000006,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 16,
|
||||||
|
"nation": 54,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CB",
|
||||||
|
"rareflag": 23,
|
||||||
|
"rating": 92,
|
||||||
|
"resourceId": 134381968,
|
||||||
|
"teamid": 73,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 182493,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 52
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 69
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 67
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 92
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 83
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 182493,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 151177437,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000007,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 60,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CB",
|
||||||
|
"rareflag": 24,
|
||||||
|
"rating": 92,
|
||||||
|
"resourceId": 151177437,
|
||||||
|
"teamid": 240,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 168651,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 71
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 89
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 87
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 71
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 168651,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 84054731,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000008,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 10,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 11,
|
||||||
|
"rating": 92,
|
||||||
|
"resourceId": 84054731,
|
||||||
|
"teamid": 241,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 182521,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 48
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 83
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 91
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 82
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 73
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 182521,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 134400249,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000009,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 53,
|
||||||
|
"nation": 21,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "CM",
|
||||||
|
"rareflag": 23,
|
||||||
|
"rating": 91,
|
||||||
|
"resourceId": 134400249,
|
||||||
|
"teamid": 243,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 197445,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 90
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 77
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 85
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 87
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 87
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 77
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 197445,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 151192389,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000010,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 19,
|
||||||
|
"nation": 4,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LB",
|
||||||
|
"rareflag": 24,
|
||||||
|
"rating": 91,
|
||||||
|
"resourceId": 151192389,
|
||||||
|
"teamid": 21,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 121939,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 70
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 58
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 84
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 86
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 88
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 66
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 121939,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 100785235,
|
||||||
|
"discardValue": 1500,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000011,
|
||||||
|
"itemState": "free",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 19,
|
||||||
|
"nation": 21,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "RB",
|
||||||
|
"rareflag": 21,
|
||||||
|
"rating": 90,
|
||||||
|
"resourceId": 100785235,
|
||||||
|
"teamid": 21,
|
||||||
|
"untradeable": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sha256": "0a916e8e129a9c05294784738c305d651beedd58ebd914a6abfa94bd0a59abc7",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"counts": {
|
||||||
|
"body": {
|
||||||
|
"count": 0,
|
||||||
|
"maxAuctionsAllowed": 100,
|
||||||
|
"offered": 0,
|
||||||
|
"selling": 0,
|
||||||
|
"sold": 1
|
||||||
|
},
|
||||||
|
"sha256": "766b1fcc4ca47442e6f570c81583840b6ae8df9cbeae52473b3c100ca881e206",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"tradePile": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 232273,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 53
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 50
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 232273,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 232273,
|
||||||
|
"discardValue": 150,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000012,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 32,
|
||||||
|
"nation": 117,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 51,
|
||||||
|
"resourceId": 232273,
|
||||||
|
"teamid": 112487,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143,
|
||||||
|
"duplicateItemIdList": [],
|
||||||
|
"total": 1
|
||||||
|
},
|
||||||
|
"sha256": "c792ebcf179a5c617ffe02859ad39eba830a4d5b5224a0953ecef774ee4cf75b",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_filtered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 232273,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 53
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 50
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 232273,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 232273,
|
||||||
|
"discardValue": 150,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000012,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 32,
|
||||||
|
"nation": 117,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 51,
|
||||||
|
"resourceId": 232273,
|
||||||
|
"teamid": 112487,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "db79d9fcfc47ae680b44311598e82918b8afd3284160143c33e525adf693f30e",
|
||||||
|
"status": 200
|
||||||
|
},
|
||||||
|
"trade_status_unfiltered": {
|
||||||
|
"body": {
|
||||||
|
"auctionInfo": [
|
||||||
|
{
|
||||||
|
"bidState": "highest",
|
||||||
|
"buyNowPrice": 150,
|
||||||
|
"coinsProcessed": 0,
|
||||||
|
"currentBid": 150,
|
||||||
|
"expires": 0,
|
||||||
|
"itemData": {
|
||||||
|
"assetId": 232273,
|
||||||
|
"attributeList": [
|
||||||
|
{
|
||||||
|
"index": 0,
|
||||||
|
"value": 62
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"value": 28
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 2,
|
||||||
|
"value": 33
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 3,
|
||||||
|
"value": 49
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 4,
|
||||||
|
"value": 53
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 5,
|
||||||
|
"value": 50
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"cardassetid": 232273,
|
||||||
|
"cardsubtypeid": 0,
|
||||||
|
"contract": 7,
|
||||||
|
"definitionId": 232273,
|
||||||
|
"discardValue": 150,
|
||||||
|
"fitness": 99,
|
||||||
|
"id": 100000012,
|
||||||
|
"itemState": "forSale",
|
||||||
|
"itemType": "player",
|
||||||
|
"leagueId": 32,
|
||||||
|
"nation": 117,
|
||||||
|
"owners": 1,
|
||||||
|
"playStyle": 250,
|
||||||
|
"preferredPosition": "LB",
|
||||||
|
"rareflag": 1,
|
||||||
|
"rating": 51,
|
||||||
|
"resourceId": 232273,
|
||||||
|
"teamid": 112487,
|
||||||
|
"untradeable": false
|
||||||
|
},
|
||||||
|
"sellerEstablished": 1,
|
||||||
|
"sellerName": "CAGE",
|
||||||
|
"startingBid": 150,
|
||||||
|
"tradeId": 900500150,
|
||||||
|
"tradeState": "closed",
|
||||||
|
"watched": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"credits": 1143
|
||||||
|
},
|
||||||
|
"sha256": "db79d9fcfc47ae680b44311598e82918b8afd3284160143c33e525adf693f30e",
|
||||||
|
"status": 200
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"topology": {
|
||||||
|
"advertises_utas": "http://<host>:8299/",
|
||||||
|
"blaze": [
|
||||||
|
42327,
|
||||||
|
42330,
|
||||||
|
42331
|
||||||
|
],
|
||||||
|
"core": 18081,
|
||||||
|
"utas_host": 8299
|
||||||
|
},
|
||||||
|
"variant": "A_highest (bidState=highest, coinsProcessed=0, countMode=active)"
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
{
|
||||||
|
"purchase": [
|
||||||
|
{
|
||||||
|
"assetId": 1,
|
||||||
|
"id": 1,
|
||||||
|
"packType": "BRONZE",
|
||||||
|
"description": "Bronze Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 1,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 400,
|
||||||
|
"finalFunds": 400
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 4,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 4,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 5,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 0,
|
||||||
|
"rareQuantity": 0,
|
||||||
|
"itemQuantity": 5
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "bronze"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 5,
|
||||||
|
"id": 5,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Gold Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 2,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 5000,
|
||||||
|
"finalFunds": 5000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 50,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 50,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 7,
|
||||||
|
"rareQuantity": 7,
|
||||||
|
"itemQuantity": 7
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "gold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 6,
|
||||||
|
"id": 6,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Premium Gold",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 3,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 15000,
|
||||||
|
"finalFunds": 15000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 150,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 150,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 11,
|
||||||
|
"rareQuantity": 11,
|
||||||
|
"itemQuantity": 11
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "gold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 7,
|
||||||
|
"id": 7,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Special Players Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 4,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 25000,
|
||||||
|
"finalFunds": 25000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 250,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 250,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 11,
|
||||||
|
"rareQuantity": 11,
|
||||||
|
"itemQuantity": 11
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "special"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 65534,
|
||||||
|
"id": 65534,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "",
|
||||||
|
"state": "inactive",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 1,
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 0,
|
||||||
|
"rareQuantity": 0,
|
||||||
|
"itemQuantity": 0
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "mypacks",
|
||||||
|
"priority": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": 1596326400
|
||||||
|
}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
{
|
||||||
|
"purchase": [
|
||||||
|
{
|
||||||
|
"assetId": 1,
|
||||||
|
"id": 1,
|
||||||
|
"packType": "BRONZE",
|
||||||
|
"description": "Bronze Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 1,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 400,
|
||||||
|
"finalFunds": 400
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 4,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 4,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 5,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 0,
|
||||||
|
"rareQuantity": 0,
|
||||||
|
"itemQuantity": 5
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "bronze"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 5,
|
||||||
|
"id": 5,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Gold Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 2,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 5000,
|
||||||
|
"finalFunds": 5000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 50,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 50,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 7,
|
||||||
|
"rareQuantity": 7,
|
||||||
|
"itemQuantity": 7
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "gold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 6,
|
||||||
|
"id": 6,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Premium Gold",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 3,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 15000,
|
||||||
|
"finalFunds": 15000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 150,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 150,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 11,
|
||||||
|
"rareQuantity": 11,
|
||||||
|
"itemQuantity": 11
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "gold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 7,
|
||||||
|
"id": 7,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Special Players Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 4,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 25000,
|
||||||
|
"finalFunds": 25000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 250,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 250,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 11,
|
||||||
|
"rareQuantity": 11,
|
||||||
|
"itemQuantity": 11
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "special"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 65534,
|
||||||
|
"id": 65534,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 1,
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 0,
|
||||||
|
"rareQuantity": 0,
|
||||||
|
"itemQuantity": 0
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "mypacks",
|
||||||
|
"priority": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": 1596326400
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"purchase": [{"assetId": 1, "id": 1, "packType": "BRONZE", "description": "Bronze Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 1, "currencies": [{"name": "coins", "funds": 400, "finalFunds": 400}], "extPrice": {"finalPrice": {"amount": 4, "currency": "mtx"}, "originalPrice": {"amount": 4, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 5, "silverQuantity": 0, "goldQuantity": 0, "rareQuantity": 0, "itemQuantity": 5}, "unopened": false, "displayGroup": {"value": "bronze"}}, {"assetId": 5, "id": 5, "packType": "GOLD", "description": "Gold Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 2, "currencies": [{"name": "coins", "funds": 5000, "finalFunds": 5000}], "extPrice": {"finalPrice": {"amount": 50, "currency": "mtx"}, "originalPrice": {"amount": 50, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 7, "rareQuantity": 7, "itemQuantity": 7}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 6, "id": 6, "packType": "GOLD", "description": "Premium Gold", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 3, "currencies": [{"name": "coins", "funds": 15000, "finalFunds": 15000}], "extPrice": {"finalPrice": {"amount": 150, "currency": "mtx"}, "originalPrice": {"amount": 150, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 7, "id": 7, "packType": "GOLD", "description": "Special Players Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 4, "currencies": [{"name": "coins", "funds": 25000, "finalFunds": 25000}], "extPrice": {"finalPrice": {"amount": 250, "currency": "mtx"}, "originalPrice": {"amount": 250, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "special"}}], "timestamp": 1596326400}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
{
|
||||||
|
"purchase": [
|
||||||
|
{
|
||||||
|
"assetId": 1,
|
||||||
|
"id": 1,
|
||||||
|
"packType": "BRONZE",
|
||||||
|
"description": "Bronze Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 1,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 400,
|
||||||
|
"finalFunds": 400
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 4,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 4,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 5,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 0,
|
||||||
|
"rareQuantity": 0,
|
||||||
|
"itemQuantity": 5
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "bronze"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 5,
|
||||||
|
"id": 5,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Gold Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 2,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 5000,
|
||||||
|
"finalFunds": 5000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 50,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 50,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 7,
|
||||||
|
"rareQuantity": 7,
|
||||||
|
"itemQuantity": 7
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "gold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 6,
|
||||||
|
"id": 6,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Premium Gold",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 3,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 15000,
|
||||||
|
"finalFunds": 15000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 150,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 150,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 11,
|
||||||
|
"rareQuantity": 11,
|
||||||
|
"itemQuantity": 11
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "gold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 7,
|
||||||
|
"id": 7,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Special Players Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 4,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 25000,
|
||||||
|
"finalFunds": 25000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 250,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 250,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 11,
|
||||||
|
"rareQuantity": 11,
|
||||||
|
"itemQuantity": 11
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "special"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": 1596326400
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"purchase": [{"assetId": 1, "id": 1, "packType": "BRONZE", "description": "Bronze Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 1, "currencies": [{"name": "coins", "funds": 400, "finalFunds": 400}], "extPrice": {"finalPrice": {"amount": 4, "currency": "mtx"}, "originalPrice": {"amount": 4, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 5, "silverQuantity": 0, "goldQuantity": 0, "rareQuantity": 0, "itemQuantity": 5}, "unopened": false, "displayGroup": {"value": "bronze"}}, {"assetId": 5, "id": 5, "packType": "GOLD", "description": "Gold Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 2, "currencies": [{"name": "coins", "funds": 5000, "finalFunds": 5000}], "extPrice": {"finalPrice": {"amount": 50, "currency": "mtx"}, "originalPrice": {"amount": 50, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 7, "rareQuantity": 7, "itemQuantity": 7}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 6, "id": 6, "packType": "GOLD", "description": "Premium Gold", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 3, "currencies": [{"name": "coins", "funds": 15000, "finalFunds": 15000}], "extPrice": {"finalPrice": {"amount": 150, "currency": "mtx"}, "originalPrice": {"amount": 150, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "gold"}}, {"assetId": 7, "id": 7, "packType": "GOLD", "description": "Special Players Pack", "state": "active", "saleType": "promo", "limitType": "NONE", "quantity": 0, "purchaseLimit": 0, "purchaseCount": 0, "isPremium": false, "sortPriority": 4, "currencies": [{"name": "coins", "funds": 25000, "finalFunds": 25000}], "extPrice": {"finalPrice": {"amount": 250, "currency": "mtx"}, "originalPrice": {"amount": 250, "currency": "mtx"}}, "packContentInfo": {"bronzeQuantity": 0, "silverQuantity": 0, "goldQuantity": 11, "rareQuantity": 11, "itemQuantity": 11}, "unopened": false, "displayGroup": {"value": "special"}}], "timestamp": 1596326400}
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
{
|
||||||
|
"purchase": [
|
||||||
|
{
|
||||||
|
"assetId": 1,
|
||||||
|
"id": 1,
|
||||||
|
"packType": "BRONZE",
|
||||||
|
"description": "Bronze Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 1,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 400,
|
||||||
|
"finalFunds": 400
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 4,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 4,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 5,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 0,
|
||||||
|
"rareQuantity": 0,
|
||||||
|
"itemQuantity": 5
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "bronze"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 5,
|
||||||
|
"id": 5,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Gold Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 2,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 5000,
|
||||||
|
"finalFunds": 5000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 50,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 50,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 7,
|
||||||
|
"rareQuantity": 7,
|
||||||
|
"itemQuantity": 7
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "gold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 6,
|
||||||
|
"id": 6,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Premium Gold",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 3,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 15000,
|
||||||
|
"finalFunds": 15000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 150,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 150,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 11,
|
||||||
|
"rareQuantity": 11,
|
||||||
|
"itemQuantity": 11
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "gold"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 7,
|
||||||
|
"id": 7,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Special Players Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 4,
|
||||||
|
"currencies": [
|
||||||
|
{
|
||||||
|
"name": "coins",
|
||||||
|
"funds": 25000,
|
||||||
|
"finalFunds": 25000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"extPrice": {
|
||||||
|
"finalPrice": {
|
||||||
|
"amount": 250,
|
||||||
|
"currency": "mtx"
|
||||||
|
},
|
||||||
|
"originalPrice": {
|
||||||
|
"amount": 250,
|
||||||
|
"currency": "mtx"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 11,
|
||||||
|
"rareQuantity": 11,
|
||||||
|
"itemQuantity": 11
|
||||||
|
},
|
||||||
|
"unopened": false,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "special"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"assetId": 70,
|
||||||
|
"id": 70,
|
||||||
|
"packType": "GOLD",
|
||||||
|
"description": "Reward Special Players Pack",
|
||||||
|
"state": "active",
|
||||||
|
"saleType": "promo",
|
||||||
|
"limitType": "NONE",
|
||||||
|
"quantity": 0,
|
||||||
|
"purchaseLimit": 0,
|
||||||
|
"purchaseCount": 0,
|
||||||
|
"isPremium": false,
|
||||||
|
"sortPriority": 1,
|
||||||
|
"packContentInfo": {
|
||||||
|
"bronzeQuantity": 0,
|
||||||
|
"silverQuantity": 0,
|
||||||
|
"goldQuantity": 11,
|
||||||
|
"rareQuantity": 11,
|
||||||
|
"itemQuantity": 11
|
||||||
|
},
|
||||||
|
"unopened": true,
|
||||||
|
"displayGroup": {
|
||||||
|
"value": "mypacks",
|
||||||
|
"priority": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"timestamp": 1596326400
|
||||||
|
}
|
||||||
@@ -0,0 +1,523 @@
|
|||||||
|
# FIFA 17 — Clean Empty-My-Packs client fix (DESIGN / RESEARCH ONLY)
|
||||||
|
|
||||||
|
Status: **design only — no client binary/movie changes made.** This is the client-side
|
||||||
|
follow-up to bug 6c. The backend already ships a compatibility workaround (P2, active
|
||||||
|
non-openable sentinel 65534; see `docs/evidence/STORE_TILE_6C.md` §17 and
|
||||||
|
`FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md`). This plan describes what a *client-side*
|
||||||
|
fix would need to change so the backend shim can eventually become unnecessary for
|
||||||
|
patched clients.
|
||||||
|
|
||||||
|
Do NOT patch the executable, DLLs, or Scaleform movies in this task.
|
||||||
|
|
||||||
|
## 1. Established client-side evidence
|
||||||
|
|
||||||
|
Binary: `CardsDLL_Win64_retail.dll`
|
||||||
|
SHA-256 `4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c`
|
||||||
|
(dump load base `0x00006FFFFC120000`; RE-space base `0x180000000`). `FIFA17.exe`
|
||||||
|
(`29c31cef…`) is Denuvo-packed (decrypts only in live memory).
|
||||||
|
|
||||||
|
Store category pipeline (all decompiled; see `docs/plan-2026-08-05-store-subsystem.md`):
|
||||||
|
- `FUN_1800150d0` — builds display groups from `purchase[]`; a `mypacks` group exists
|
||||||
|
iff some pack has `displayGroup.value=="mypacks"`. `group+0x104=(value=="mypacks")`,
|
||||||
|
tiles in `group+0x40`, ordinal in `group+0x00` (1-based creation order).
|
||||||
|
- `FUN_18007dab0` → `FUN_1800147f0(model, screen+0x290, …)` — renders/resolves a
|
||||||
|
category. `screen+0x290==0` lists group tiles (`FUN_180014610`); otherwise
|
||||||
|
`FUN_180014420` exact-matches the ordinal and **returns NULL on a miss**, after
|
||||||
|
which `FUN_1800147f0` dereferences `[RAX+0x48]` with **no null guard** →
|
||||||
|
**crash at `0x180014882`** (`ACCESS_VIOLATION` read of `0x48`, minidump-confirmed).
|
||||||
|
- `screen+0x290` is written in exactly two CardsDLL sites: ctor `FUN_18007d1a0`
|
||||||
|
writes `0`; **`FUN_18007e7f0` case `0x7551` copies the Flash movie message field
|
||||||
|
`CATEGORY_ID` verbatim** into it. So the category is chosen by the Scaleform movie.
|
||||||
|
- `FUN_18007e5e0` binds the six store tabs (`FUN_180014580`: `mypacks, points, bronze,
|
||||||
|
silver, gold, special`) to `PANEL_ID` = matching group ordinal, or hides the panel.
|
||||||
|
- Unopened-pack count signals (server, already correct at 0 when empty):
|
||||||
|
`userInfo.unopenedPacks.recoveredPacks` and `/user/credits .unopenedPacks`. The hub
|
||||||
|
`CentralUnclaimedPack` tile (destination `GOTO_STORE_MYPACK`) is gated by this count
|
||||||
|
in the hub model (`model+0x20950`). **Candidate F (a server count gating the STORE's
|
||||||
|
My-Packs resolution) was CONTRADICTED**: the count is correct at 0 yet the store
|
||||||
|
still resolves My Packs, because the decision is movie-side.
|
||||||
|
|
||||||
|
## 2. Desired clean client behavior
|
||||||
|
|
||||||
|
```
|
||||||
|
unopened-pack count == 0:
|
||||||
|
Store defaults to Browse Packs (e.g. a real category such as bronze/gold)
|
||||||
|
My Packs is NOT selected/resolved
|
||||||
|
no synthetic placeholder tile is required from the server
|
||||||
|
unopened-pack count > 0:
|
||||||
|
existing My Packs behavior unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Candidate insertion points (ranked)
|
||||||
|
|
||||||
|
Ranking favors fixing the UX (not merely preventing the crash) and the smallest,
|
||||||
|
lowest-risk change that achieves it.
|
||||||
|
|
||||||
|
### Rank 1 (preferred, best UX) — Scaleform / category-selection layer
|
||||||
|
Prevent the movie from emitting `CATEGORY_ID == mypacks` (and from defaulting the
|
||||||
|
store into My Packs) when the unopened-pack count is 0; default to Browse Packs
|
||||||
|
instead.
|
||||||
|
- **Where:** the FUT Store Scaleform movie / ActionScript (`StoreFront`,
|
||||||
|
`CATEGORY_ID`/`ACTION_GET_PACKLIST`, `GOTO_STORE_MYPACK`), which the packed exe hosts
|
||||||
|
and which reads the hub model (it already knows the count for the
|
||||||
|
`CentralUnclaimedPack` tile).
|
||||||
|
- **Behavior changed:** the store's initial/selected category when empty.
|
||||||
|
- **Scope:** movie asset edit (client-side), no native-code patch.
|
||||||
|
- **Risk:** medium — Scaleform RE/editing is fiddly; must find where the default
|
||||||
|
`CATEGORY_ID` is chosen and gate it on the count without breaking the count>0 path.
|
||||||
|
- **Compatibility:** per-client asset change; does not touch protocol or other clients.
|
||||||
|
- **Fixes UX or just crash?** **UX** — no fake tile, correct default; the crash also
|
||||||
|
disappears because `mypacks` is never resolved when absent.
|
||||||
|
- **Evidence:** `screen+0x290 ← CATEGORY_ID` (`FUN_18007e7f0` case `0x7551`); count
|
||||||
|
already available client-side (hub model / `unopenedPacks`).
|
||||||
|
|
||||||
|
### Rank 2 — Native Store resolver fallback (CardsDLL)
|
||||||
|
Make `FUN_1800147f0`/`FUN_180014420` fall back to a safe category (e.g. list-tiles
|
||||||
|
`N==0`, or the first existing group) when the requested ordinal misses, instead of
|
||||||
|
dereferencing NULL.
|
||||||
|
- **Behavior changed:** category-miss handling for ALL categories, not just mypacks.
|
||||||
|
- **Scope:** small, localized CardsDLL binary patch near `0x180014420`/`0x180014882`.
|
||||||
|
- **Risk:** medium — alters native store behavior globally; could mask other
|
||||||
|
legitimate misses; the movie may still believe it is in My Packs (empty/odd view).
|
||||||
|
- **Compatibility:** binary patch to the shipped DLL (client-side).
|
||||||
|
- **Fixes UX or just crash?** Crash + partial UX (no crash, but the empty-My-Packs
|
||||||
|
view may still be awkward).
|
||||||
|
- **Evidence:** the no-guard deref at `0x180014882`; `FUN_180014420` returns NULL on
|
||||||
|
miss.
|
||||||
|
|
||||||
|
### Rank 3 (cheapest, crash-only) — CardsDLL null guard
|
||||||
|
Insert a null check before the `[RAX+0x48]` dereference in `FUN_1800147f0` (a single
|
||||||
|
`TEST/JZ` around the deref) so a NULL group is skipped/returned safely.
|
||||||
|
- **Behavior changed:** only the crash path.
|
||||||
|
- **Scope:** minimal (a few bytes) binary patch at `~0x180014882`.
|
||||||
|
- **Risk:** low — smallest change; but purely crash-prevention. With no `mypacks`
|
||||||
|
group the resulting empty view is unverified (could be a blank/empty-category state).
|
||||||
|
- **Compatibility:** binary patch (client-side).
|
||||||
|
- **Fixes UX or just crash?** Crash only.
|
||||||
|
- **Evidence:** minidump faulting instruction `CardsDLL+0x14882`, `[NULL+0x48]`.
|
||||||
|
|
||||||
|
## 4. Recommended long-term outcome
|
||||||
|
|
||||||
|
Rank 1 (Scaleform default-category gating) is the clean fix: with count 0 the store
|
||||||
|
opens on Browse Packs, no `mypacks` resolution, no fake tile — and the **backend
|
||||||
|
sentinel 65534 can be dropped for patched clients** (the server would simply omit the
|
||||||
|
`mypacks` group when empty, which is safe once the client no longer resolves it).
|
||||||
|
Rank 3 (null guard) is a cheap universal crash-safety net that could ship alongside.
|
||||||
|
Until a client-side fix exists, the backend P2 sentinel remains the required
|
||||||
|
compatibility behavior for unpatched retail clients.
|
||||||
|
|
||||||
|
## 5. Open questions / next research (no execution here)
|
||||||
|
- Locate the Store movie's default/initial `CATEGORY_ID` selection and confirm it can
|
||||||
|
read the unopened count (Rank 1 feasibility).
|
||||||
|
- Confirm, via a guarded-resolver experiment, what the empty-My-Packs view degrades to
|
||||||
|
if the `mypacks` group is simply absent + a null guard is present (Rank 2/3).
|
||||||
|
- Determine whether the Browse-Packs→My-Packs navigation gate (observed with the
|
||||||
|
active sentinel) also resolves under Rank 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# PART II — Native client-fix design (RE-backed, 2026-08-13)
|
||||||
|
|
||||||
|
Investigation-and-design phase (no client binary/movie changed, no backend changed,
|
||||||
|
no new live Store experiment). CardsDLL was re-analysed in Ghidra on `.105`; the
|
||||||
|
in-repo decompiled addresses were reconfirmed against a freshly-built project. Every
|
||||||
|
claim below is labelled **ESTABLISHED** (read from this build's binary / crash dump),
|
||||||
|
**PROPOSED** (design, not yet implemented), or **UNKNOWN**.
|
||||||
|
|
||||||
|
## 6. Binary + environment verification (ESTABLISHED)
|
||||||
|
|
||||||
|
Hashes re-verified on `.105` (`/mnt/games/FIFA 17/`) — identical to the recorded RE:
|
||||||
|
- `CardsDLL_Win64_retail.dll` SHA-256 `4706a881ae1fc7b5769fd810b25a868d29d2b16a8e65a7513436327ef645573c`,
|
||||||
|
size 3179952, PE `TimeDateStamp` 1497050156 (2017-06-09T23:15:56Z), `SizeOfImage`
|
||||||
|
`0x31d000`, image base `0x180000000` (RE-space). **Unpacked → statically analysable.**
|
||||||
|
- `FIFA17.exe` SHA-256 `29c31cef12b0c3c2a7305220617c7b4fa139ab76b8c857851bdbe88987962899`,
|
||||||
|
size 224639408, **Denuvo-packed** → the Scaleform/StoreFront ActionScript that
|
||||||
|
*decides* to emit `CATEGORY_ID` is NOT statically readable. This is why a
|
||||||
|
pure-Scaleform edit (old "Rank 1") is not the practical vehicle; the fix is taken
|
||||||
|
at the readable native boundary in CardsDLL instead.
|
||||||
|
- Ghidra project rebuilt at `.105:/tmp/ghidra_fut/cardsdll` (headless import+analysis
|
||||||
|
succeeded). Tooling: `fifa17-recon/tools/ghidra_env.py` run under `~/.venv`
|
||||||
|
(`PYTHONPATH=/opt/ghidra/Ghidra/Features/PyGhidra/pypkg/src:/usr/lib/python3.14/site-packages`;
|
||||||
|
`jpype1` reinstalled offline from pip cache). RVAs below = static VA − `0x180000000`.
|
||||||
|
|
||||||
|
## 7. Category-selection path (ESTABLISHED — decompiled this build)
|
||||||
|
|
||||||
|
Store message dispatch `FUN_18007d880` (RVA `0x7d880`) routes Flash message ids:
|
||||||
|
`0x753f → FUN_18007dab0` (render), `0x278a → FUN_18007df60` (publish category ids),
|
||||||
|
and the input handler `FUN_18007e7f0` (RVA `0x7e7f0`) case **`0x7551`** copies the
|
||||||
|
movie field `CATEGORY_ID` verbatim into `screen+0x290` (the only non-ctor writer;
|
||||||
|
ctor `FUN_18007d1a0` writes 0).
|
||||||
|
|
||||||
|
**Store render `FUN_18007dab0` (RVA `0x7dab0`), decompiled verbatim, is the decision
|
||||||
|
point:**
|
||||||
|
```c
|
||||||
|
iVar1 = *(int *)(param_1 + 0x290); // requested CATEGORY_ID (screen+0x290)
|
||||||
|
iVar6 = FUN_180014580(store, 1); // the *points* category id (see tab map)
|
||||||
|
if (iVar1 == iVar6) { // requested category is POINTS (real-money)
|
||||||
|
if (region_check() == 0) { post "REGION_MISMATCH"; return; }
|
||||||
|
if (FUN_180014de0(store) != 0) return; // points group present → handled
|
||||||
|
FUN_180014b60(store, dp); // else points render
|
||||||
|
} else {
|
||||||
|
FUN_1800147f0(store, iVar1, dp, 0, 0); // EVERY other category, incl. My Packs
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- `param_1` (RCX) = the store-screen object; `+0x290` is the requested category.
|
||||||
|
- **Tab→id map `FUN_180014580(store, n)` (RVA `0x14580`): `0=mypacks, 1=points,
|
||||||
|
2=bronze, 3=silver, 4=gold, 5=special`.** Each returns the group's **1-based
|
||||||
|
ordinal** (via caption compare `FUN_180014380`) or **`-1`** if that group is absent.
|
||||||
|
So category ids are DYNAMIC ordinals, not fixed constants. The tab publisher
|
||||||
|
`FUN_18007df60` pushes `MYPACK_/BRONZE_/…_CATEGORY_ID` to the movie from these
|
||||||
|
lookups; the movie echoes one back as `CATEGORY_ID`.
|
||||||
|
- The **points** tab is the only one special-cased (commerce/region gate). **My Packs
|
||||||
|
is NOT special-cased — it falls into the `else` and is resolved by
|
||||||
|
`FUN_1800147f0`.**
|
||||||
|
|
||||||
|
**Resolver `FUN_1800147f0` (RVA `0x147f0`) — the crash (ESTABLISHED, instruction
|
||||||
|
level):**
|
||||||
|
```
|
||||||
|
0x14856: 85 ff TEST EDI,EDI ; EDI = category ordinal (param_2)
|
||||||
|
0x14858: 75 0f JNZ 0x14869 ; ==0 → list-all (Browse), else resolve
|
||||||
|
0x1485a: … CALL 0x14610 ; FUN_180014610 list ALL group tiles
|
||||||
|
0x14867: eb 29 JMP 0x14892
|
||||||
|
0x14869: 8b d7 MOV EDX,EDI
|
||||||
|
0x1486b: e8 … CALL 0x14420 ; FUN_180014420(store, ordinal) → RAX (group|NULL)
|
||||||
|
0x14870: 48 8d 50 40 LEA RDX,[RAX + 0x40] ; RDX = group+0x40 (=0x40 when RAX=NULL)
|
||||||
|
0x14878: 48 3b c2 CMP RAX,RDX
|
||||||
|
0x1487b: 74 15 JZ 0x14892
|
||||||
|
0x14882: 4c 8b 42 08 MOV R8,[RDX + 0x8] ; <-- FAULT: read [0x40+0x8]=0x48 when NULL
|
||||||
|
0x14886: 48 8b 12 MOV RDX,[RDX] ; [0x40]
|
||||||
|
```
|
||||||
|
`FUN_180014420` (RVA `0x14420`) exact-matches `group+0x00` (ordinal), stride `0x108`,
|
||||||
|
**returns NULL on a miss, with no guard in the caller** → faulting read of VA `0x48`
|
||||||
|
at `0x180014882`. This is byte-for-byte the Experiment-B minidump
|
||||||
|
(`0xC0000005` READ `0x48` at `CardsDLL+0x14882`).
|
||||||
|
- `param_2 == 0` → `FUN_180014610` lists **all** group tiles = the safe "Browse Packs"
|
||||||
|
view. `param_2 == existing ordinal` → resolves. `param_2 == a non-existent ordinal`
|
||||||
|
(e.g. `-1`, which `MYPACK_CATEGORY_ID` becomes when the group is absent) → NULL → crash.
|
||||||
|
|
||||||
|
**Why it crashes with zero packs (ESTABLISHED):** with `unopenedPackIds==[]` and no
|
||||||
|
sentinel, no `mypacks` group exists, so `FUN_180014580(store,0) = -1`,
|
||||||
|
`MYPACK_CATEGORY_ID = -1`, the movie still selects My Packs and echoes `CATEGORY_ID =
|
||||||
|
-1`, and `FUN_1800147f0(store, -1, …)` → `FUN_180014420(-1)=NULL` → crash. The active
|
||||||
|
sentinel (65534) works only because it makes a real `mypacks` ordinal exist to resolve.
|
||||||
|
|
||||||
|
## 8. Zero-pack state client-side (ESTABLISHED)
|
||||||
|
|
||||||
|
The client already holds the correct unopened-pack count in a **data-manager
|
||||||
|
singleton** (the same one the store resolver uses):
|
||||||
|
- Obtain: `seed = FUN_1800d7170()` then `FUN_180009c80(&p, seed)` → `p` (release with
|
||||||
|
`p->vtbl[0x08](p)`). This exact accessor already runs inside `FUN_180014420` and
|
||||||
|
`FUN_1800147f0`, so any store-category hook can reach it.
|
||||||
|
- **Read count: `p->vtbl[0x4d8](p)` → int. Write: `p->vtbl[0x4e0](p, n)`.** Confirmed
|
||||||
|
in `FUN_180019780`, which reads slot `0x4d8`, adds the number of set booleans in a
|
||||||
|
pack response, and writes slot `0x4e0` (it also fetches `FutGetPurchasedItems`).
|
||||||
|
- Representation: plain `int`; **0 = no unopened packs**, `>0` = count. Lifetime: the
|
||||||
|
singleton persists for the session; updated on pack acquire/open.
|
||||||
|
- No dedicated "hasUnopenedPacks" boolean helper was found; `count != 0` is the
|
||||||
|
predicate. (The hub `CentralUnclaimedPack` tile is gated by this same count via
|
||||||
|
`model+0x20950`, written by `FUN_18010cdc0`/`FUN_18011e120` — the hub mirror, not the
|
||||||
|
store gate.)
|
||||||
|
|
||||||
|
## 9. Implementation vehicle (ESTABLISHED — reuse, do not build a new loader)
|
||||||
|
|
||||||
|
OpenFUT **already ships a client hook framework**: `openfut-launcher/openfut-hook`
|
||||||
|
(`crate-type=["cdylib"]`) builds **`version.dll`**, a proxy DLL placed in the game dir
|
||||||
|
(`/mnt/games/FIFA 17/version.dll`, present & active; log `~/.wine/drive_c/openfut_hook.log`).
|
||||||
|
- Load path: Wine/Windows loads `version.dll` from the app dir at process start →
|
||||||
|
`DllMain(DLL_PROCESS_ATTACH)` → `install_hooks()`.
|
||||||
|
- Existing hooks (`lib.rs`): `getaddrinfo` (IAT via `iat::resolve`), `connect`
|
||||||
|
(inline detour), `WSAConnect`, `WSAIoctl`/ConnectEx, origin_spy registry/mutex,
|
||||||
|
crypt32 `CertVerifyCertificateChainPolicy`, **and in-memory byte-patching of the
|
||||||
|
loaded (packed) main exe + EAWebKit** (`ssl_patch`: `GetModuleHandleA` → scan for a
|
||||||
|
unique prologue → `VirtualProtect`+`copy_nonoverlapping`).
|
||||||
|
- Inline-hook primitive (`connect_hook`): `write_hook(target, dest)` lays a 14-byte
|
||||||
|
`FF 25 00000000 <abs64>` JMP; `restore_original` restores saved bytes
|
||||||
|
(unhook → call real → rehook, avoiding trampoline relocation).
|
||||||
|
- Config: `openfut.cfg` beside the DLL (`host`/ports today; a `store_mypacks_fix`
|
||||||
|
flag would be added there).
|
||||||
|
- **Suitability for the Store fix: direct.** The DLL is in-process with full access
|
||||||
|
to the loaded `CardsDLL_Win64_retail.dll`; the store fix is a NEW module
|
||||||
|
(`store_hook.rs`) installed from `install_hooks`, reusing the `ssl_patch`
|
||||||
|
signature-scan and the `connect_hook` inline-detour patterns. No new loader, no ASI,
|
||||||
|
no separate injector.
|
||||||
|
|
||||||
|
## 10. Three strategies re-evaluated against the RE (Task 4)
|
||||||
|
|
||||||
|
### A. Category-selection redirect — **PREFERRED** (best UX, native, targeted)
|
||||||
|
Hook `FUN_18007dab0` (RVA `0x7dab0`) at entry; before the original runs, redirect a
|
||||||
|
zero-pack My-Packs request to Browse Packs:
|
||||||
|
```
|
||||||
|
cat = *(int*)(store + 0x290)
|
||||||
|
mypacks_id = FUN_180014580(store, 0) // -1 when the group is absent
|
||||||
|
if (cat == mypacks_id) { // movie asked for My Packs (incl. cat==-1==id)
|
||||||
|
if (unopened_count() == 0) // singleton vtbl[0x4d8]
|
||||||
|
*(int*)(store + 0x290) = 0; // 0 = FUN_180014610 list-all = Browse Packs
|
||||||
|
}
|
||||||
|
// then call the original FUN_18007dab0(store)
|
||||||
|
```
|
||||||
|
- Uses the real count? **Yes** (singleton `vtbl[0x4d8]`). Removes the fake 65534 tile?
|
||||||
|
**Yes** (server can omit the group). Removes the click-dialog? **Yes** (no placeholder
|
||||||
|
to click). Removes the Browse→My-Packs nav gate? **Yes** (store lands on Browse, not
|
||||||
|
an empty My-Packs). Preserves count>0? **Yes** (`cat==mypacks_id` with count>0 is left
|
||||||
|
untouched → normal My Packs). Affects other categories? **No** (`cat!=mypacks_id`
|
||||||
|
path is unmodified; points/bronze/… unchanged).
|
||||||
|
- Prevents the crash as a side effect (My Packs is never resolved when its group is
|
||||||
|
absent). This is the old "Rank 1" INTENT, implemented at the readable native boundary
|
||||||
|
instead of in packed Scaleform.
|
||||||
|
|
||||||
|
### B. Resolver fallback — acceptable safety net, less targeted
|
||||||
|
In `FUN_1800147f0` (or right after the `CALL 0x14420` at RVA `0x1486b`): if the
|
||||||
|
resolved group is NULL, fall back to list-all (`param_2=0`) instead of dereferencing.
|
||||||
|
- Prevents crash? **Yes.** Fixes default nav / removes fake tile? **Partially** — the
|
||||||
|
movie still believes it is in My Packs, so the view may be an empty/odd My-Packs
|
||||||
|
rather than a clean Browse. Leaves other lookups unchanged? **It changes miss-handling
|
||||||
|
for ALL categories** — a generic NULL fallback that could mask a genuine
|
||||||
|
missing-category protocol bug. Higher risk than A for that reason; keep as a
|
||||||
|
belt-and-braces guard, not the primary UX fix. The resolver does NOT know *why*
|
||||||
|
`mypacks` is missing, which is exactly the concern the task flags.
|
||||||
|
|
||||||
|
### C. Null-guard only — weakest (crash-only)
|
||||||
|
Insert `TEST RAX,RAX; JZ 0x14892` immediately after `CALL 0x14420` (RVA `0x1486b`),
|
||||||
|
before `LEA RDX,[RAX+0x40]`. Needs a trampoline (no inline slack).
|
||||||
|
- Converts the crash into whatever an empty tile-vector renders (unverified; likely a
|
||||||
|
blank/empty category). Does **not** remove the fake tile or fix the default category;
|
||||||
|
the sentinel would still be needed for acceptable UX. Verified as expected-weakest.
|
||||||
|
|
||||||
|
## 11. Concrete hook target for strategy A (Task 6, PROPOSED)
|
||||||
|
```
|
||||||
|
module: CardsDLL_Win64_retail.dll (GetModuleHandleA)
|
||||||
|
function: FUN_18007dab0 (store render / message 0x753f)
|
||||||
|
RVA: 0x7dab0 (static VA 0x18007dab0)
|
||||||
|
calling conv: Microsoft x64 fastcall; single arg store-screen ptr in RCX
|
||||||
|
screen offset: store+0x290 = requested CATEGORY_ID (int)
|
||||||
|
helpers to call: FUN_180014580 (RVA 0x14580) tab→ordinal, arg0=RCX store, arg1=EDX index(0=mypacks)
|
||||||
|
count singleton: FUN_1800d7170 (0xd7370-seed) + FUN_180009c80 (0x9c80), read vtbl[0x4d8]
|
||||||
|
redirect target: set store+0x290 = 0 (FUN_180014610 list-all → Browse Packs)
|
||||||
|
original behavior: zero packs → resolves absent mypacks ordinal → FUN_180014420 NULL → crash at 0x14882
|
||||||
|
desired behavior: zero packs + mypacks requested → store+0x290 forced to 0 → Browse Packs; no crash/dialog/tile
|
||||||
|
```
|
||||||
|
Hook mechanics (reuse `connect_hook`): lay a 14-byte `FF 25` JMP at `base+0x7dab0` to a
|
||||||
|
Rust `hooked_store_render(store)`; inside: apply the redirect, unhook, call real
|
||||||
|
`FUN_18007dab0(store)`, rehook, return its value. Intercepting only the entry means the
|
||||||
|
minimum interception is the 14 JMP bytes; the first instructions of `FUN_18007dab0`
|
||||||
|
(`MOV RAX,RSP; MOV [RAX+8],RCX; PUSH …`) are a standard prologue safe to save/restore.
|
||||||
|
Alt insertion point (earlier): `FUN_18007e7f0` case `0x7551`, where `CATEGORY_ID` is
|
||||||
|
written to `screen+0x290` — redirect there instead of at render. Entry-hook of
|
||||||
|
`FUN_18007dab0` is preferred (single, well-typed arg; runs once per store render).
|
||||||
|
|
||||||
|
Thread/context: the store screen runs on the client's UI/update thread; the hook reads
|
||||||
|
one int and (rarely) writes one int on the same object the callee immediately reads —
|
||||||
|
no new synchronization needed. Called for categories other than My Packs? The FUNCTION
|
||||||
|
is, but the redirect body only fires when `cat==mypacks_id`, so other tabs are
|
||||||
|
untouched.
|
||||||
|
|
||||||
|
## 12. Version / build safety (Task 7, PROPOSED)
|
||||||
|
FIFA17-specific compat code MUST validate the client before hooking, and MUST no-op on
|
||||||
|
any other build (the same `version.dll` is also used for FIFA23):
|
||||||
|
1. **Module gate:** only proceed if `GetModuleHandleA("CardsDLL_Win64_retail.dll")`
|
||||||
|
resolves (FIFA23 has no such module → auto-skip).
|
||||||
|
2. **Build gate (both, belt-and-braces):**
|
||||||
|
- Exact hash/PE gate: on-disk SHA-256 == `4706a881…`, or PE `SizeOfImage==0x31d000`
|
||||||
|
&& `TimeDateStamp==1497050156` (cheap in-memory check).
|
||||||
|
- Signature scan + validation: locate `FUN_18007dab0` by a unique prologue/byte
|
||||||
|
window rather than trusting the RVA, and assert the known bytes at the branch
|
||||||
|
(`85 ff 75 0f` region) and at the resolver `CALL 0x14420` site match before
|
||||||
|
installing. Recommend **both**: hash to reject the wrong game fast, signature to
|
||||||
|
confirm the exact patch site.
|
||||||
|
3. **Failure behavior:** any check fails (unknown/updated build) → **do NOT patch**,
|
||||||
|
log, and leave the **backend P2 active-sentinel (65534) as the fallback**. Never
|
||||||
|
patch or crash an unrecognised build.
|
||||||
|
|
||||||
|
## 13. First controlled client experiment (Task 8, PROPOSED — not executed here)
|
||||||
|
Goal: prove a patched client sends zero-pack Store entry to Browse Packs with **no**
|
||||||
|
active placeholder.
|
||||||
|
- Build `openfut-hook` with strategy-A `store_hook`, gated behind `openfut.cfg`
|
||||||
|
`store_mypacks_fix=1` (opt-in; default off preserves today's behavior).
|
||||||
|
- Test profile: `unopenedPackIds == []`.
|
||||||
|
- Sequence (each variable changed alone; operator drives FIFA; read-only capture):
|
||||||
|
1. Deploy patched `version.dll`; confirm `openfut_hook.log` shows the store hook
|
||||||
|
installed + build gate PASSED.
|
||||||
|
2. **Backend test mode (LATER, separately authorized — NOT in this task):** switch the
|
||||||
|
backend to *empty-no-sentinel* (the Exp-B config that crashed the UNPATCHED client)
|
||||||
|
so the patched client must handle a genuinely-absent `mypacks` group.
|
||||||
|
3. Operator opens Store. **Predicted (patched + zero packs + no sentinel):** Store
|
||||||
|
opens, defaults to Browse Packs, no `mypacks` resolve, **no crash, no dialog, no
|
||||||
|
fake tile**.
|
||||||
|
4. Set `unopenedPackIds=[70]`; reopen. **Predicted:** My Packs works normally
|
||||||
|
(hook body skipped because count>0).
|
||||||
|
5. Revert backend to the active sentinel.
|
||||||
|
- **Backend change eventually required for this experiment: YES** — a controlled
|
||||||
|
empty-no-sentinel test mode to force the absent group. It is NOT performed in this
|
||||||
|
phase and MUST be separately authorized (same experiment discipline: patch the
|
||||||
|
container copy, capture, revert, restart; never synthesize a client request).
|
||||||
|
- **Client rollback:** flip `store_mypacks_fix=0` (hook not installed) or restore the
|
||||||
|
original `version.dll`; the game reverts to depending on the backend sentinel. No FIFA
|
||||||
|
binaries/movies/config are modified on disk — the hook is in-memory only, so rollback
|
||||||
|
is a file/flag swap.
|
||||||
|
|
||||||
|
## 14. Interaction with the backend 65534 fallback (ESTABLISHED + PROPOSED)
|
||||||
|
- **Keep the backend sentinel deployed** until strategy A is implemented AND verified.
|
||||||
|
It remains the required behavior for unpatched retail clients and for any client whose
|
||||||
|
build gate fails.
|
||||||
|
- Once strategy A is verified, the server MAY, **for patched clients only**, omit the
|
||||||
|
`mypacks` group when empty (the safe representation the client will then handle) —
|
||||||
|
but only behind explicit detection/opt-in; do NOT drop the sentinel globally, since
|
||||||
|
unpatched clients still crash without it.
|
||||||
|
|
||||||
|
## 15. ESTABLISHED / PROPOSED / UNKNOWN summary
|
||||||
|
- **ESTABLISHED:** binary hashes/build; the full native category path and addresses
|
||||||
|
(`FUN_18007d880/18007dab0/18007e7f0/1800147f0/180014420/180014580/180014610`); the
|
||||||
|
instruction-level crash (`0x14882`, `[NULL+0x48]`); tab→ordinal map; that My Packs is
|
||||||
|
not special-cased and funnels through `FUN_1800147f0`; the unopened-count singleton
|
||||||
|
and its `vtbl[0x4d8]/[0x4e0]` accessors, reachable from store code; the
|
||||||
|
`openfut-hook`/`version.dll` vehicle and its hook/patch primitives.
|
||||||
|
- **PROPOSED (not implemented):** the strategy-A entry hook and its redirect logic; the
|
||||||
|
build-guard scheme; the opt-in config flag; the first experiment and its backend
|
||||||
|
test-mode requirement; the per-patched-client server relaxation.
|
||||||
|
- **UNKNOWN:** exactly why the packed Scaleform movie selects My Packs on store open
|
||||||
|
(Denuvo-packed, unread) — not needed for strategy A, which intercepts the native
|
||||||
|
result; the precise rendered appearance of `category==0` list-all in this empty
|
||||||
|
configuration (to be observed in the experiment); whether any non-store path also
|
||||||
|
drives `screen+0x290` to a My-Packs ordinal (none found; `FUN_18007e7f0` case `0x7551`
|
||||||
|
and the ctor are the only writers).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# PART III — Final no-sentinel resolver experiment (2026-08-13) — RESULT F3 (CRASH), CONFOUNDED
|
||||||
|
|
||||||
|
Vehicle change: the resolver guard was implemented as an **`autopatch.py` memory patch**
|
||||||
|
(the live FIFA-17 client-patch mechanism), NOT the `version.dll` proxy — Proton loads its
|
||||||
|
builtin `version.dll`, so the earlier `store_hook`/`version.dll` prototype was inert and
|
||||||
|
has been rolled back. Guard: at CardsDLL `0x180014858`, `JNZ 0x14869` (`75 0f`) →
|
||||||
|
`JG 0x14869` (`7f 0f`), orig-verified; routes category `< 0` (and `== 0`) to the safe
|
||||||
|
list-all/Browse path (`FUN_180014610`), category `> 0` to the existing resolver.
|
||||||
|
`TEST EDI,EDI` at `0x180014856` is the flag source (OF cleared ⇒ `JG` = signed `> 0`).
|
||||||
|
|
||||||
|
## Setup (verified)
|
||||||
|
- CLIENT: guard active/enforced — live bytes `85 ff 7f 0f` at `0x180014856` (FIFA pid 547843,
|
||||||
|
autopatch pid 547621; log `ENFORCED guarded store patch @ … (JNZ->JG)`, orig `75 0f` matched).
|
||||||
|
- BACKEND: sentinel 65534 suppressed by a one-line `if not owned_ids:` → `if False:` in the
|
||||||
|
container copy only (committed source `f42279f` untouched; backup `/tmp/utas_server.EXP_ORIG.py`).
|
||||||
|
Genuine `GET /store/purchasegroup` (02:44:39Z) → ids `[1,5,6,7]`, **no 65534, no mypacks group**,
|
||||||
|
normal packs unchanged (evidence: `docs/evidence/store_purchasegroup_capture_client_guard_no_sentinel_2026-08-13.json`).
|
||||||
|
- PROFILE: `unopenedPackIds=[]`, coins 29,876,776, sha `39bb3e83…` — unchanged throughout.
|
||||||
|
|
||||||
|
## Result — F3 (CRASH)
|
||||||
|
Minidump `CrashDump_2026.08.12_20.44.40.302.dmp` (preserved `/tmp/expF_crash.dmp`, sha `4dcb0cb7…`):
|
||||||
|
`0xC0000005` READ of VA `0x48` at `ExceptionAddress 0x6ffffc224882` → **RE `0x180014882`** —
|
||||||
|
the **identical** resolver crash instruction as Experiment B (`FUN_1800147f0`,
|
||||||
|
`MOV R8,[RDX+0x8]` with the group ptr NULL).
|
||||||
|
|
||||||
|
**Mechanism (decisive):** `0x14882` lives in the *resolve* branch, which the guard's `JG`
|
||||||
|
reaches **only when category `> 0`**. Since the guard was verified in place, the client
|
||||||
|
presented a **positive** My-Packs ordinal that no longer resolves (no mypacks group) →
|
||||||
|
`FUN_180014420` returned NULL → crash. The guard's design assumption — *absent mypacks ⇒
|
||||||
|
category `-1`* — did NOT hold on this path.
|
||||||
|
|
||||||
|
## Confound (uncontrolled variable)
|
||||||
|
FIFA was **not relaunched** after the backend flipped to no-sentinel; the client carried
|
||||||
|
**stale store/tab state** from the sentinel-present safe stage, where the mypacks group
|
||||||
|
existed at a *positive* ordinal `N` (`MYPACK_CATEGORY_ID = N`). Reopening the Store reused
|
||||||
|
that stale positive ordinal rather than the `-1` a **fresh** launch publishes
|
||||||
|
(`FUN_18007df60 → FUN_180014580(store,0) = -1` when absent). So the intended clean A/B (client
|
||||||
|
only ever sees the no-sentinel response) was not achieved — the category that reached the
|
||||||
|
resolver was a stale `>0`, exactly the case the negative-only guard does not divert.
|
||||||
|
|
||||||
|
## Conclusion / strategy status
|
||||||
|
- **The guard as-written does NOT handle a positive, now-invalid My-Packs ordinal** — proven
|
||||||
|
by this crash. Diverting only `category < 0` is insufficient when the client presents a
|
||||||
|
stale/positive ordinal for an absent group.
|
||||||
|
- **Not falsified for the fresh-client case.** Whether a fresh no-sentinel launch presents
|
||||||
|
`-1` (guard diverts → Browse, no crash) or still a positive ordinal is **UNKNOWN** and needs
|
||||||
|
a **clean re-test**: launch FIFA fresh with the backend already in no-sentinel mode so the
|
||||||
|
client never sees a mypacks group. That is the proper equivalent of Experiment B.
|
||||||
|
- **Candidate stronger guard** (design only, not implemented): divert to list-all when the
|
||||||
|
resolved group is NULL for *any* category (guard `FUN_180014420`'s NULL return at the
|
||||||
|
`0x14870`/`0x14882` site), not merely when `category < 0`. This covers the positive-invalid
|
||||||
|
ordinal too, at the cost of being a generic miss-fallback (the higher-risk Rank-2 behavior).
|
||||||
|
Do NOT implement without authorization and a clean re-test first.
|
||||||
|
|
||||||
|
**Strategy A / resolver guard status: NOT PROVEN.** Crash-guard installs and is build-validated
|
||||||
|
and dormant-safe with the sentinel present, but the first no-sentinel test CRASHED at the
|
||||||
|
resolver via a positive stale ordinal (confounded by no relaunch). Backend P2 active-sentinel
|
||||||
|
was restored immediately (mandatory rollback; source `f416e71e…`, sentinel `state=active`),
|
||||||
|
and remains the production safety net. Guard left in `autopatch.py` (dormant) pending the
|
||||||
|
clean re-test decision; `autopatch.py.pre-storeguard.bak` available to remove it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# PART IV — Fresh-process no-sentinel retest (2026-08-13) — RESULT R1 (SUCCESS)
|
||||||
|
|
||||||
|
Corrects PART III's confound. This time the mandatory ordering was enforced: the backend
|
||||||
|
entered no-sentinel mode **while FIFA was closed**, then FIFA launched **fresh** (new pid,
|
||||||
|
new autopatch) so the process never saw a sentinel-present Store response.
|
||||||
|
|
||||||
|
## Setup (verified, clean A/B)
|
||||||
|
- BACKEND set no-sentinel at 02:55:09Z with FIFA down; genuine `GET /store/purchasegroup`
|
||||||
|
(02:58:45Z) served to the fresh client = ids `[1,5,6,7]`, **no 65534, no mypacks group**,
|
||||||
|
packs 1/5/6/7 present. This body is **byte-identical** to the PART III (F3) no-sentinel
|
||||||
|
capture — the ONLY changed variable vs F3 is the client process lifetime.
|
||||||
|
Evidence: `docs/evidence/store_purchasegroup_capture_freshretest_no_sentinel_2026-08-13.json`.
|
||||||
|
- CLIENT: NEW FIFA pid 553220, NEW autopatch pid 552999; guard ENFORCED (orig `75 0f`
|
||||||
|
matched → `85 ff 7f 0f` = `TEST EDI,EDI; JG`). Process never saw a sentinel response
|
||||||
|
(0 purchasegroup responses containing 65534 after the no-sentinel restart).
|
||||||
|
- PROFILE unchanged throughout (`39bb3e83…`, `[]`, coins 29,876,776).
|
||||||
|
|
||||||
|
## Result — R1 (operator-observed)
|
||||||
|
- **No crash** (FIFA 553220 alive after the test; no new minidump), **no dialog**, **Store
|
||||||
|
stays open**, opens on **Browse Packs**, Bronze/Gold/Special packs visible and navigable.
|
||||||
|
- Cosmetic-only imperfections (pre-existing, NOT caused by the guard): the six-tab bar is
|
||||||
|
unbound (no tabs), packs render without cover art, and tiles show "0 items". These match
|
||||||
|
the known store tab-bind / list-all rendering quirks (`plan-2026-08-05-store-subsystem.md`
|
||||||
|
§2.1) and are independent of the resolver guard.
|
||||||
|
|
||||||
|
## Causal conclusion (decisive A/B)
|
||||||
|
```
|
||||||
|
server response (no sentinel, no mypacks group) == byte-identical across F3 and R1
|
||||||
|
client original JNZ + this response -> CRASH 0x180014882 (Experiment B)
|
||||||
|
client JG (stale positive ordinal) -> CRASH 0x180014882 (PART III F3, contaminated)
|
||||||
|
client JG (FRESH, category = -1) -> NO CRASH, Browse Packs (PART IV R1) ✅
|
||||||
|
```
|
||||||
|
A **fresh** client publishes `MYPACK_CATEGORY_ID = FUN_180014580(store,0) = -1` for the absent
|
||||||
|
group; the movie echoes `-1`; `TEST EDI,EDI; JG` does **not** take the resolve branch, so the
|
||||||
|
client runs the list-all/Browse path (`FUN_180014610`) — no `FUN_180014420(NULL)` deref, no
|
||||||
|
crash. **PART III's F3 is confirmed as stale-positive-ordinal contamination** (FIFA not
|
||||||
|
relaunched across the sentinel→no-sentinel flip), not a guard failure.
|
||||||
|
|
||||||
|
## Strategy status
|
||||||
|
**Strategy A / resolver guard: PROVEN ON THE TESTED FIFA 17 BUILD** (CardsDLL
|
||||||
|
`4706a881…`) for the clean process-lifetime case — it safely routes the absent My-Packs
|
||||||
|
category to Browse Packs with no crash and no dialog, needing **no** backend sentinel. Scope
|
||||||
|
caveats: (1) tested build only; (2) the negative-only guard does NOT cover a stale/positive
|
||||||
|
invalid ordinal (PART III) — only arises if the client's Store state predates a sentinel→
|
||||||
|
no-sentinel change within one process, which does not happen on a normal launch; a NULL-return
|
||||||
|
guard at `FUN_180014420` would additionally cover that, deferred/not implemented; (3) UX still
|
||||||
|
has the pre-existing no-tabs/no-art/"0 items" cosmetics.
|
||||||
|
|
||||||
|
Backend P2 active-sentinel was restored immediately after capture (mandatory rollback; source
|
||||||
|
`f416e71e…`, sentinel `state=active`) and **remains production default**. The clean UX is only
|
||||||
|
safe to serve when the server knows the client is patched — see PART II §12 rollout options
|
||||||
|
(recommend B: suppress the sentinel only when client patch-capability is known; keep the
|
||||||
|
sentinel universal by default). Guard retained in `autopatch.py` (dormant with the sentinel).
|
||||||
|
|
||||||
|
## INVARIANT — empty-My-Packs capability MUST be session-stable
|
||||||
|
|
||||||
|
F3 vs R1 establish a hard operational invariant for any deployment (sentinel or client
|
||||||
|
guard): **the server MUST NOT switch a running FIFA client between sentinel-present and
|
||||||
|
sentinel-absent for the My Packs group within a single FIFA process lifetime.**
|
||||||
|
|
||||||
|
Rationale: the client resolves and caches the My-Packs group **ordinal** (positive when a
|
||||||
|
group — real or sentinel — is present; `-1` when absent) from the `purchasegroup` response
|
||||||
|
seen at Store-subsystem init. The resolver guard only reclassifies the ordinal *sign*
|
||||||
|
(`≤0` → Browse). If a client that already cached a **positive** ordinal later receives a
|
||||||
|
no-sentinel topology, the stale positive ordinal still takes the resolve branch and
|
||||||
|
`FUN_180014420` returns NULL → crash at `0x180014882` (exactly F3). A **fresh** process that
|
||||||
|
only ever sees the no-sentinel topology caches `-1` and is routed to Browse safely (R1).
|
||||||
|
|
||||||
|
Practical rules:
|
||||||
|
- Choose the My-Packs representation (sentinel-present vs sentinel-absent) **before** a client
|
||||||
|
starts its session, and hold it for that session.
|
||||||
|
- The future patch-capability handshake (PART II §12) MUST therefore be decided at
|
||||||
|
login/session start, not toggled mid-session.
|
||||||
|
- A NULL-return guard at `FUN_180014420` (deferred) is the only thing that would make a
|
||||||
|
mid-session flip crash-safe; until then, session stability is mandatory.
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
# FIFA 17 — verified patched-client capability negotiation
|
||||||
|
|
||||||
|
Goal: let the FIFA 17 backend suppress the synthetic My-Packs sentinel (id 65534)
|
||||||
|
**only when the current FIFA process has positively verified that the CardsDLL
|
||||||
|
resolver guard is active** (JNZ→JG at RVA `0x14858`). Unpatched / unsupported /
|
||||||
|
unknown / failed-patch clients keep receiving the existing P2 active sentinel.
|
||||||
|
|
||||||
|
Core principle: **the capability is not "this launcher supports the patch"; it is
|
||||||
|
"the resolver guard was verified in *this particular FIFA process*."**
|
||||||
|
|
||||||
|
This document is the design + the cross-component contract. It is deliberately
|
||||||
|
additive: the P2 active-sentinel path (`docs/evidence/FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md`)
|
||||||
|
remains the default and the universal fallback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Architecture inventory (as-built, verified by reading the code)
|
||||||
|
|
||||||
|
Data flow today (launch of one FIFA process):
|
||||||
|
|
||||||
|
```
|
||||||
|
LauncherApp::launch_game (openfut-launcher/src/app.rs:450)
|
||||||
|
-> account_sync::sync POST /openfut/account/sync (:8099) [REQUIRED; launch is gated on it]
|
||||||
|
-> ensure_local_services() spawn LSX, then autopatch.py --launcher-pid <launcher_pid>
|
||||||
|
-> game_launch::launch umu-run FIFA17.exe (grandchild; launcher never learns FIFA PID)
|
||||||
|
FIFA process
|
||||||
|
-> autopatch.py self-discovers FIFA by comm=='FIFA17.exe'; patches /proc/<pid>/mem each tick
|
||||||
|
-> FIFA -> backend POST /ut/auth (login) ; GET /store/purchasegroup ; ... (:8099)
|
||||||
|
```
|
||||||
|
|
||||||
|
Facts that shape the design:
|
||||||
|
|
||||||
|
- **Launcher ↔ autopatch IPC = one-way stdout only.** `local_services::spawn`
|
||||||
|
(openfut-launcher/src/local_services.rs:279-296) pipes autopatch stdout/stderr
|
||||||
|
into the launcher `LogBuffer` line-by-line as `[autopatch] <line>`. There is no
|
||||||
|
socket / named pipe / status-file readback. `--launcher-pid` is the *launcher's*
|
||||||
|
own pid (local_services.rs:66), used for liveness, not to identify FIFA.
|
||||||
|
- **Launcher ↔ backend = exactly one control call:** `account_sync::sync`
|
||||||
|
(openfut-launcher/src/account_sync.rs:43) — a tiny stdlib-HTTP `POST
|
||||||
|
/openfut/account/sync` on `openfut_account_sync_port` (default 8099), sent once
|
||||||
|
per launch, *before* FIFA starts, and **launch is blocked unless it succeeds**
|
||||||
|
(utas_server.py:1204). This is the reliable per-FIFA-process session boundary.
|
||||||
|
- **Backend is single-account, stateless-per-request, threaded.** `SID` is a fixed
|
||||||
|
module constant shared by all clients (utas_server.py:32); account identity is one
|
||||||
|
global `ACCOUNT` singleton. There is **no per-session identity** in requests. The
|
||||||
|
only per-connection discriminator available at every handler is
|
||||||
|
`self.client_address[0]` (peer IP), currently unused. Server is
|
||||||
|
`ThreadingHTTPServer` (utas_server.py:3784); module is import-safe (server under
|
||||||
|
`if __name__ == "__main__"`).
|
||||||
|
- **No bridge/proxy in the FIFA-17 path.** FIFA reaches the Python backend's
|
||||||
|
published `:8099` directly (client-side DNAT/hosts redirect); the openfut-bridge is
|
||||||
|
legacy FIFA-23. Docker's iptables DNAT preserves the source IP for external LAN
|
||||||
|
clients. The launcher and FIFA run on the **same** client machine, so the backend
|
||||||
|
observes them under the **same** peer IP regardless of NAT.
|
||||||
|
|
||||||
|
## 2. Capability transport — options and choice
|
||||||
|
|
||||||
|
Ranked against the as-built architecture:
|
||||||
|
|
||||||
|
**autopatch → launcher (chosen: structured stdout line).**
|
||||||
|
1. **Structured stdout line (CHOSEN).** Reuses the existing one-way pipe the
|
||||||
|
launcher already reads. It is *live* (only the current autopatch child's stdout),
|
||||||
|
inherently child-bound, and carries **zero stale-file risk** — a previous
|
||||||
|
launch's capability cannot leak because nothing is persisted. Smallest possible
|
||||||
|
change. Format is a machine-readable token (§4).
|
||||||
|
2. Status file in `$XDG_RUNTIME_DIR` keyed by launcher-pid+FIFA-pid+version+timestamp
|
||||||
|
— works but needs explicit staleness handling and cleanup; more moving parts.
|
||||||
|
3. Unix-domain socket — most capable but overkill; there is no bidirectional need.
|
||||||
|
|
||||||
|
**launcher → backend (chosen: sibling HTTP endpoint on the account-sync port).**
|
||||||
|
- A. **Existing session-init channel (CHOSEN).** Add `POST /openfut/fifa17/capability`
|
||||||
|
next to the existing `/openfut/account/sync` (same port 8099, same tiny stdlib-HTTP
|
||||||
|
client). It cannot ride *inside* account_sync because the capability is only known
|
||||||
|
*after* autopatch verifies (which happens after account_sync + FIFA start), so it is
|
||||||
|
a separate, later call — but on the same proven transport.
|
||||||
|
- B. Blaze/login metadata — rejected: no OpenFUT-owned field is available without
|
||||||
|
risking a field FIFA depends on, and Blaze runs in a separate responder.
|
||||||
|
- C. New local IPC + backend side-channel — unnecessary; A already exists.
|
||||||
|
- D. Server-wide "assume patched" config — dev/testing fallback only; cannot
|
||||||
|
distinguish patched vs unpatched clients, so never the production mechanism.
|
||||||
|
|
||||||
|
## 3. The capability (name + version + VERIFIED semantics)
|
||||||
|
|
||||||
|
- Name: **`fifa17.empty_mypacks_resolver`**, integer version, current **`1`**.
|
||||||
|
- **VERIFIED (v1) means, for THIS FIFA process:** the CardsDLL tested build was
|
||||||
|
recognised AND the live bytes at RVA `0x14858` are `7f 0f` (`JG`) **after
|
||||||
|
autopatch enforcement** — i.e. `guarded_action` returned `"patch"` (was `75 0f`,
|
||||||
|
written, re-read as `7f 0f`) **or** `"noop"` (already `7f 0f`).
|
||||||
|
- It explicitly does **NOT** mean any of: "autopatch.py contains the guard code",
|
||||||
|
"the launcher build is new enough", or "a config flag is set". The signal
|
||||||
|
represents **observed runtime enforcement on the specific process**, nothing less.
|
||||||
|
|
||||||
|
## 4. autopatch verification state + emitted line
|
||||||
|
|
||||||
|
Per-FIFA-pid guard status (fail-closed; never loosens the existing byte guard):
|
||||||
|
|
||||||
|
| state | meaning |
|
||||||
|
|---|---|
|
||||||
|
| `NOT_ATTEMPTED` | CardsDLL not yet mapped / guard not evaluated for this pid |
|
||||||
|
| `VERIFIED` | live bytes == `7f 0f` after enforcement (from `patch` or `noop`) |
|
||||||
|
| `UNSUPPORTED_BUILD` | live bytes are neither the known original nor patched (`guarded_action` → `skip`) |
|
||||||
|
| `WRITE_FAILED` | `/proc/<pid>/mem` write raised |
|
||||||
|
| `VERIFY_FAILED` | post-write re-read != `7f 0f` |
|
||||||
|
|
||||||
|
Only `VERIFIED` advertises capability. On transition to `VERIFIED`, autopatch emits
|
||||||
|
**once per FIFA pid** on stdout:
|
||||||
|
|
||||||
|
```
|
||||||
|
[store-guard] verified capability fifa17.empty_mypacks_resolver=1 fifa_pid=<pid>
|
||||||
|
```
|
||||||
|
|
||||||
|
Any non-verified terminal state emits an explicit, non-advertising status line, e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
[store-guard] guard status=UNSUPPORTED_BUILD fifa_pid=<pid> (no capability advertised)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Launcher per-process capability state
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Fifa17ClientCapabilities { pub empty_mypacks_resolver: Option<u32> }
|
||||||
|
```
|
||||||
|
|
||||||
|
- Starts **UNKNOWN** (`None`) at each launch.
|
||||||
|
- Becomes `Some(1)` when the launcher parses a valid capability line from the
|
||||||
|
**current** autopatch child's stdout (`parse_capability_line`).
|
||||||
|
- **Discarded** when autopatch stops / FIFA exits / launcher exits / next launch. It
|
||||||
|
is never persisted and never reused for a later FIFA process — staleness is
|
||||||
|
structurally impossible.
|
||||||
|
|
||||||
|
On first `Some(v)`, the launcher registers the capability with the backend (§6) once.
|
||||||
|
|
||||||
|
## 6. Launcher → backend registration + binding
|
||||||
|
|
||||||
|
`POST /openfut/fifa17/capability` (port = `openfut_account_sync_port`, 8099), body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"capability":"empty_mypacks_resolver","version":1,"personaId":<id>,"fifaPid":<pid>}
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Binding key = source IP** (`self.client_address[0]`). The registration arrives
|
||||||
|
from the client machine's IP; FIFA's `/store/purchasegroup` requests arrive from
|
||||||
|
the **same** IP (same machine). `personaId`/`fifaPid` are for logging only (the
|
||||||
|
backend is single-account, so persona cannot discriminate clients).
|
||||||
|
- Concurrency: distinct client machines → distinct peer IPs → independent decisions
|
||||||
|
(no global state). Two FIFA processes on **one** machine share an IP — an accepted
|
||||||
|
limitation (the backend is single-account anyway); documented in §Trust.
|
||||||
|
|
||||||
|
## 7. Backend session-stable decision
|
||||||
|
|
||||||
|
Per-IP record (guarded by a lock; threaded server):
|
||||||
|
|
||||||
|
```
|
||||||
|
_FIFA17_STORE[ip] = {"resolver": Option[int], "mode": Option[str]} # mode: None|"sentinel"|"clean-v1"
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Reset (session boundary):** `/openfut/account/sync` from `ip` sets
|
||||||
|
`{resolver: None, mode: None}`. This is the launcher's required per-launch call, so
|
||||||
|
every new FIFA process starts from a clean, unfrozen record — no cross-process leak.
|
||||||
|
- **Register:** `/openfut/fifa17/capability` from `ip` sets `resolver = version`. If
|
||||||
|
`mode` is already frozen, it is logged as late and **ignored for this session**.
|
||||||
|
- **Freeze point = first `/store/purchasegroup`** from `ip` (§9): if `mode is None`,
|
||||||
|
set `mode = "clean-v1"` iff `resolver == 1` else `"sentinel"`, and log once.
|
||||||
|
Thereafter `mode` is immutable for the session.
|
||||||
|
- **Default / fail-closed:** an IP with no record (no account-sync, no capability),
|
||||||
|
an unknown resolver version, a late capability, or a disappeared capability all
|
||||||
|
resolve to (or remain) `"sentinel"`.
|
||||||
|
|
||||||
|
## 8. Freeze point rationale
|
||||||
|
|
||||||
|
Freeze at **first `/store/purchasegroup`**, not at login/account-sync. account-sync
|
||||||
|
fires *before* FIFA starts and *before* autopatch can verify, so freezing there would
|
||||||
|
always be `sentinel`. First Store request is the earliest moment at which a genuine
|
||||||
|
capability can already be registered (autopatch verifies at process start; the user
|
||||||
|
opens the Store later), while still being a single, well-defined topology commit for
|
||||||
|
the session. Once Store topology is served, it must not change (the F3 experiment
|
||||||
|
proved a mid-session flip can leave a stale positive ordinal that crashes even the
|
||||||
|
sign-only guard — see `FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md` PART III/IV and the
|
||||||
|
SESSION-STABLE invariant).
|
||||||
|
|
||||||
|
## 9. Store behaviour (additive switch)
|
||||||
|
|
||||||
|
At `store_catalog`, only the zero-owned-packs branch changes:
|
||||||
|
|
||||||
|
```
|
||||||
|
if not owned_ids:
|
||||||
|
if fifa17_empty_mypacks_mode(client_ip) == "clean-v1":
|
||||||
|
pass # patched client: emit NO mypacks group; guard routes -1 to Browse
|
||||||
|
else:
|
||||||
|
<append active 65534 sentinel exactly as today> # P2 fallback (unchanged)
|
||||||
|
```
|
||||||
|
|
||||||
|
Untouched: real owned-pack rendering, `PACK_CATALOG`, pack 70, normal packs 1/5/6/7,
|
||||||
|
profile state, all store env flags. Default remains sentinel. This lives in the FIFA-17
|
||||||
|
Python backend only — **never** in game-independent OpenFUT Core.
|
||||||
|
|
||||||
|
| client | zero packs | real unopened pack |
|
||||||
|
|---|---|---|
|
||||||
|
| verified v1 | **no sentinel** (clean) | genuine My Packs, no sentinel |
|
||||||
|
| no / unknown capability | **active 65534 sentinel** | genuine My Packs, no sentinel |
|
||||||
|
|
||||||
|
## 10. Trust model (Task 14)
|
||||||
|
|
||||||
|
This is **not** anti-cheat / attestation. OpenFUT assumes the user controls the
|
||||||
|
launcher/client machine and the server is a private preservation environment. The
|
||||||
|
verification exists to prevent *accidents*: a stale capability, an unsupported
|
||||||
|
CardsDLL build, a failed autopatch, the wrong process, or an unpatched client
|
||||||
|
receiving no sentinel and crashing. No signatures / PKI / remote attestation.
|
||||||
|
|
||||||
|
Isolation across *distinct client machines* relies on the backend observing distinct
|
||||||
|
peer IPs (source-IP-preserving publish; Docker's default for external LAN via iptables
|
||||||
|
DNAT). Two FIFA processes on one machine cannot be distinguished by IP — accepted,
|
||||||
|
since the backend is single-account. The single-client production case is unaffected
|
||||||
|
by NAT because launcher and FIFA share one IP.
|
||||||
|
|
||||||
|
## 11. Fail-closed matrix (Task 15) — every failure ⇒ sentinel
|
||||||
|
|
||||||
|
autopatch missing / not run · guard `UNSUPPORTED_BUILD` / `WRITE_FAILED` /
|
||||||
|
`VERIFY_FAILED` · launcher cannot parse the line · registration POST fails ·
|
||||||
|
account-sync never called · unknown capability version · capability arrives after
|
||||||
|
freeze · capability disappears after a sentinel freeze — **all resolve to the active
|
||||||
|
65534 sentinel.** Asserted by tests (matrix A–J) and this document.
|
||||||
|
|
||||||
|
## 12. P2 retained (Task 16)
|
||||||
|
|
||||||
|
The active-sentinel implementation is **not** removed. It is the else-branch of the
|
||||||
|
switch and the universal default for unpatched clients, unsupported builds, failed
|
||||||
|
patches, unknown launchers, and late capabilities. The clean path is purely additive.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Session binding (hardening — supersedes the per-IP prototype)
|
||||||
|
|
||||||
|
**History.** The first implementation keyed the backend capability/store-mode by
|
||||||
|
**source IP alone** (§7 as originally written). That was rejected before deployment:
|
||||||
|
two FIFA processes that share a source IP — concurrent, or a relaunch — would share
|
||||||
|
the key, so an *unverified* process could inherit a *verified* one's `clean-v1`
|
||||||
|
topology and crash on the empty-My-Packs resolver. Source IP is now **auxiliary only**
|
||||||
|
(logging, a fail-closed sid/ip sanity check, and the pending hand-off key). This
|
||||||
|
history is retained deliberately; do not treat per-IP as the design.
|
||||||
|
|
||||||
|
**Authoritative key = the per-login UTAS session id (`X-UT-SID`).** `/ut/auth` now
|
||||||
|
mints a fresh unique SID per login (was a shared constant `OPENFUT-SID-…0001`); the
|
||||||
|
client echoes it on every later call, and it is **live-confirmed present on real
|
||||||
|
`/store/purchasegroup` requests**. The SID uniquely identifies one FIFA process/login:
|
||||||
|
a relaunch re-auths → new SID; two concurrent logins → two SIDs. The legacy constant
|
||||||
|
is still accepted by the retired security-question gate only, and is **never** used to
|
||||||
|
grant `clean-v1`. A store request whose SID was opened on a different source IP is
|
||||||
|
fail-closed to sentinel (sid/ip sanity check).
|
||||||
|
|
||||||
|
**Why not persona alone:** the backend is single-account, so `personaId` cannot
|
||||||
|
distinguish two sessions, and a relaunch keeps the same persona — persona alone would
|
||||||
|
leak a prior session's mode. Persona is used only (with IP) to key the pending hand-off.
|
||||||
|
|
||||||
|
### State machine (per session, keyed by SID)
|
||||||
|
```
|
||||||
|
Capability : Unknown | ResolverV1
|
||||||
|
StoreMode : Unfrozen | Sentinel | CleanV1
|
||||||
|
|
||||||
|
/ut/auth (new SID) : Capability=Unknown, StoreMode=Unfrozen, record {ip,persona}
|
||||||
|
+ consume any pending (ip,persona) -> Capability=ResolverV1
|
||||||
|
capability registered : bind to the one live Unfrozen/Unbound session for (ip,persona)
|
||||||
|
-> Capability=ResolverV1 ; else stage single-use pending ;
|
||||||
|
else (a session exists but is frozen/ambiguous) -> ignored-late
|
||||||
|
first /store/purchasegroup : Unfrozen + ResolverV1 -> freeze CleanV1
|
||||||
|
Unfrozen + otherwise -> freeze Sentinel (consume pending first)
|
||||||
|
late capability : StoreMode already frozen -> unchanged (ignored-late, not staged)
|
||||||
|
capability lost/cleared : after a CleanV1 freeze -> stays CleanV1 (mode is cached)
|
||||||
|
session idle > TTL / reaped: session discarded (a later store with that SID -> Sentinel)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registration order + pending hand-off
|
||||||
|
The verified capability is known only after the FIFA process exists, CardsDLL is
|
||||||
|
loaded, and autopatch confirms the JG bytes — which may land before or after
|
||||||
|
`/ut/auth`, but reliably before the user opens the Store. The launcher cannot know
|
||||||
|
the SID, so its registration is matched to a session by (source_ip, persona) as a
|
||||||
|
**single-use, short-TTL pending** (`FIFA17_PENDING_TTL = 120s`) that is consumed by
|
||||||
|
exactly one session, at whichever of these happens first for that session: its
|
||||||
|
`/ut/auth` (pending predates login), the registration itself (session already live —
|
||||||
|
bound directly), or its first store request (lazy). If the Store is reached before a
|
||||||
|
capability binds, the session freezes **Sentinel** (fail-closed); a later capability
|
||||||
|
does not change it.
|
||||||
|
|
||||||
|
### Session cleanup (Task 10)
|
||||||
|
- **creation:** at `/ut/auth`.
|
||||||
|
- **last activity:** bumped on every `/store/purchasegroup` for the session.
|
||||||
|
- **freeze:** first `/store/purchasegroup`.
|
||||||
|
- **expiry:** lazy sweep on every session op removes sessions idle for
|
||||||
|
`FIFA17_SESSION_TTL = 3600s` and pendings older than `FIFA17_PENDING_TTL`. Explicit
|
||||||
|
Blaze/UTAS teardown is not reliably observable at this handler, so a conservative
|
||||||
|
activity-based TTL is used instead. Reaping only removes *expired* entries and never
|
||||||
|
affects another live session from the same IP/persona (keyed by distinct SIDs).
|
||||||
|
|
||||||
|
### Residual limitation (documented, fail-closed)
|
||||||
|
FIFA carries no launcher-controllable per-process token, so two **simultaneous** logins
|
||||||
|
from the **same (ip, persona)** cannot be disambiguated at the instant a capability is
|
||||||
|
registered while *both* are Unfrozen/Unbound. That ambiguous case resolves to
|
||||||
|
`ignored-late` → **both freeze Sentinel** (safe: an unverified process is never granted
|
||||||
|
clean). The normal one-launcher-per-FIFA and sequential-relaunch flows bind correctly
|
||||||
|
(proven by matrix K/L/M). This is a UX conservativeness, never a safety hole.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Deployment candidate & controlled A/B (overnight reconciliation 2026-08-13)
|
||||||
|
|
||||||
|
**Launcher lineage reconciliation.** The two divergent launcher histories (merge
|
||||||
|
base `87241ac`) were reconciled by a real merge — **not** a rebase/squash/rewrite —
|
||||||
|
in a clean worktree:
|
||||||
|
- `feat/launcher-arming` `13339c1` (client arming + FIFA-17 capability reporting)
|
||||||
|
- `feat/sbc-hook-tracing` `958ff24` (openfut-hook SBC request tracing / RE probes)
|
||||||
|
|
||||||
|
Merged commit **`ca7ce26`** on branch `integration/fifa17-launcher-capability-sbc`
|
||||||
|
retains **both** ancestors (`git merge-base --is-ancestor` true for both `958ff24`
|
||||||
|
and `13339c1`). The only conflict was `src/process.rs` (launcher-arming deleted it +
|
||||||
|
dropped `mod process`; SBC only incidentally tidied it) — resolved **keep-deleted**
|
||||||
|
(orphan module; the SBC feature lives entirely in `openfut-hook/*`). The two features
|
||||||
|
are in disjoint crates/processes (launcher-crate Rust host vs `openfut-hook` Windows
|
||||||
|
DLL) and share no stdout readers, child handles, or lifecycle — no integration code
|
||||||
|
was needed.
|
||||||
|
|
||||||
|
**Gitlink status — DEFERRED (morning blocker).** The superproject gitlink still
|
||||||
|
records the pre-reconciliation `958ff24`. It was **not** bumped to `ca7ce26` because
|
||||||
|
the live submodule checkout carries uncommitted `openfut-hook/*` WIP that overlaps the
|
||||||
|
merged hook content; a non-destructive `git checkout ca7ce26` is refused ("local
|
||||||
|
changes would be overwritten"), and no `-f`/`reset`/`clean` is permitted. The user
|
||||||
|
must first reconcile that WIP against the merged `openfut-hook`, then the gitlink can
|
||||||
|
bump. Preservation artifact: `/tmp/openfut-launcher-overnight-tracked.patch`
|
||||||
|
(sha256 `8e65de2c…`).
|
||||||
|
|
||||||
|
**Validated deployment-candidate tuple** (reproducible from git except the deferred
|
||||||
|
gitlink):
|
||||||
|
```
|
||||||
|
superproject HEAD a82407c (backend per-session + docs)
|
||||||
|
backend guard b0d5e04 fix(fifa17): guard missing store category resolution
|
||||||
|
client proof fc29c2e docs(fifa17): record no-sentinel client resolver proof
|
||||||
|
autopatch report 1c396dd feat(fifa17): report verified client patch capability
|
||||||
|
backend negotiate b25761e feat(fifa17): negotiate clean empty My Packs mode
|
||||||
|
session binding 805d754 fix(fifa17): isolate patched-client capability per session
|
||||||
|
launcher merged HEAD ca7ce26 merge: reconcile launcher capability and SBC tracing
|
||||||
|
(ancestors 13339c1 capability + 958ff24 SBC)
|
||||||
|
launcher gitlink (super) 958ff24 <-- to become ca7ce26 once WIP reconciled
|
||||||
|
```
|
||||||
|
Local build artifacts (NOT deployed): launcher `target/release/openfut-launcher`
|
||||||
|
(sha256 `a390c61d…`); backend image `openfut-fut-backend:candidate-overnight`
|
||||||
|
(`84d280be…`, ships `utas_server.py` `33e0ef3…`). Live `:dev` image and the running
|
||||||
|
container were left untouched.
|
||||||
|
|
||||||
|
### Controlled A/B sequence (execute only in a later authorized deploy task)
|
||||||
|
**A — patched client:** fresh FIFA process → autopatch verifies the JG guard →
|
||||||
|
launcher parses the verified line and registers → `/ut/auth` mints a fresh `X-UT-SID`
|
||||||
|
→ capability binds to that SID → first `/store/purchasegroup` freezes `clean-v1` →
|
||||||
|
backend omits 65534 → Store opens on Browse Packs, no crash.
|
||||||
|
**B — unpatched client, same machine/IP, NEW session:** new `X-UT-SID`, no verified
|
||||||
|
capability → first store freezes `sentinel` → backend emits active 65534 → no crash.
|
||||||
|
Proves same-IP isolation + fail-closed fallback.
|
||||||
|
**C — failed patch (optional):** autopatch reports `UNSUPPORTED_BUILD`/`VERIFY_FAILED`
|
||||||
|
→ launcher never registers → `sentinel`.
|
||||||
|
Production remains the P2 active-sentinel universal default until this A/B passes.
|
||||||
+1
-1
Submodule fifa-blaze updated: eccd46f52b...f4f33969f2
@@ -0,0 +1,16 @@
|
|||||||
|
# Keep the authoritative-tree build context lean: only tools/ and data/ runtime
|
||||||
|
# files (plus the Dockerfile's own entrypoint/manifest) are needed in-image.
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
artifacts
|
||||||
|
captures
|
||||||
|
futmem
|
||||||
|
staging
|
||||||
|
docs
|
||||||
|
FUT-RUNBOOK.md
|
||||||
|
README.md
|
||||||
|
data/memdump
|
||||||
|
**/__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
@@ -9,3 +9,5 @@
|
|||||||
*.log
|
*.log
|
||||||
__pycache__/
|
__pycache__/
|
||||||
captures/
|
captures/
|
||||||
|
staging/
|
||||||
|
tools/fifa17_profile.json
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Raw /proc/PID/mem captures: regenerate with tools/db_dump.py, never commit.
|
||||||
|
# One of these directories reached 2.3GB.
|
||||||
|
data/memdump/*.bin
|
||||||
|
data/memdump/*.raw
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user