Compare commits
204 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 |
+5
-5
@@ -30,9 +30,9 @@ Thumbs.db
|
||||
|
||||
# Frozen baseline archives / inspects / manifests
|
||||
/docker-backups/
|
||||
gate-evidence/
|
||||
|
||||
# local dev screenshots (not versioned)
|
||||
fifa17-recon/.screens/
|
||||
|
||||
# local hook backup
|
||||
*.pre-storeguard.bak
|
||||
# Raw Fire2 frame captures — forensic evidence, may contain session material.
|
||||
# Sanitize with `blaze-sanitize` before anything leaves this machine.
|
||||
*.ofcap
|
||||
captures/
|
||||
|
||||
Generated
+250
-96
@@ -457,6 +457,29 @@ version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.44.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
"dunce",
|
||||
"fs_extra",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.7.9"
|
||||
@@ -613,19 +636,6 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blaze-ssl-async"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fec08f35919613bda0b3eb3bc772c2f793b3634133923b931874b18e1ac55de"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"num_enum",
|
||||
"rsa",
|
||||
"tokio",
|
||||
"x509-cert",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
@@ -830,6 +840,15 @@ dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cmake"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.11.1"
|
||||
@@ -1062,23 +1081,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"der_derive",
|
||||
"flagset",
|
||||
"pem-rfc7468",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der_derive"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
@@ -1187,6 +1193,12 @@ version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76"
|
||||
|
||||
[[package]]
|
||||
name = "dunce"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "ecolor"
|
||||
version = "0.29.1"
|
||||
@@ -1457,12 +1469,6 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flagset"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
@@ -1547,6 +1553,12 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs_extra"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.33"
|
||||
@@ -2113,9 +2125,9 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http 0.2.12",
|
||||
"hyper 0.14.32",
|
||||
"rustls",
|
||||
"rustls 0.21.12",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.24.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3126,11 +3138,34 @@ version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "openfut-adapter-fifa17"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"openfut-protocol-blaze",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-autopatch"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "openfut-blaze-host"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"openfut-adapter-fifa17",
|
||||
"openfut-protocol-blaze",
|
||||
"rand",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-bridge"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
"axum",
|
||||
"bytes",
|
||||
@@ -3141,13 +3176,13 @@ dependencies = [
|
||||
"hyper-util",
|
||||
"rcgen",
|
||||
"reqwest",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls 0.21.12",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tokio-stream",
|
||||
"tower 0.4.13",
|
||||
"tower-http",
|
||||
@@ -3185,11 +3220,40 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-hook"
|
||||
name = "openfut-host-config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"openfut-common",
|
||||
"windows-sys 0.59.0",
|
||||
"openfut-adapter-fifa17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-http"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "openfut-identity"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-import-fifa17"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"openfut-adapter-fifa17",
|
||||
"openfut-core",
|
||||
"openfut-identity",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3201,11 +3265,73 @@ dependencies = [
|
||||
"dirs",
|
||||
"eframe",
|
||||
"egui",
|
||||
"openfut-common",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-lsx"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"parking_lot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-protocol-blaze"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-redirector-host"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"openfut-adapter-fifa17",
|
||||
"openfut-host-config",
|
||||
"openfut-http",
|
||||
"openfut-tls",
|
||||
"openssl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-roster-host"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"openfut-adapter-fifa17",
|
||||
"openfut-host-config",
|
||||
"openfut-http",
|
||||
"openfut-tls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-tls"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"openssl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openfut-utas-host"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"openfut-adapter-fifa17",
|
||||
"openfut-core",
|
||||
"openfut-http",
|
||||
"openfut-identity",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"reqwest",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.81"
|
||||
@@ -3237,6 +3363,15 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.6.1+3.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46eb8fb9fb3b61ce1c0f8a026c4c1a0714d3a9e138e7fbde78753ce2babc3846"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.117"
|
||||
@@ -3245,6 +3380,7 @@ checksum = "b47e7e6bb2c38cd930d25a23b40fa52e068c10e85f3e03a7f5ba5aaca5713695"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -3685,8 +3821,8 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls 0.21.12",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@@ -3694,7 +3830,7 @@ dependencies = [
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-rustls 0.24.1",
|
||||
"tower-service",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
@@ -3833,10 +3969,26 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring 0.17.14",
|
||||
"rustls-webpki",
|
||||
"rustls-webpki 0.101.7",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring 0.17.14",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki 0.103.13",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.4"
|
||||
@@ -3846,6 +3998,24 @@ dependencies = [
|
||||
"base64 0.21.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.15.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
@@ -3856,6 +4026,18 @@ dependencies = [
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring 0.17.14",
|
||||
"rustls-pki-types",
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.23"
|
||||
@@ -4042,15 +4224,17 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"blaze-proto",
|
||||
"blaze-ssl-async",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"rustls 0.23.43",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tdf",
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.4",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"tracing",
|
||||
@@ -4068,6 +4252,12 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
@@ -4334,8 +4524,8 @@ dependencies = [
|
||||
"once_cell",
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls 0.21.12",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -4789,27 +4979,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b"
|
||||
dependencies = [
|
||||
"tls_codec_derive",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tls_codec_derive"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.53.1"
|
||||
@@ -4854,7 +5023,17 @@ version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls 0.21.12",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||
dependencies = [
|
||||
"rustls 0.23.43",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
@@ -5219,6 +5398,7 @@ dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"serde_core",
|
||||
"sha1_smol",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -6157,18 +6337,6 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd"
|
||||
|
||||
[[package]]
|
||||
name = "x509-cert"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94"
|
||||
dependencies = [
|
||||
"const-oid",
|
||||
"der",
|
||||
"spki",
|
||||
"tls_codec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xcursor"
|
||||
version = "0.3.11"
|
||||
@@ -6393,20 +6561,6 @@ name = "zeroize"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerotrie"
|
||||
|
||||
+22
-1
@@ -2,9 +2,30 @@
|
||||
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",
|
||||
"openfut-launcher/openfut-hook",
|
||||
# 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"]
|
||||
|
||||
@@ -263,48 +263,3 @@ Both matter beyond themselves, because they are the only two routes into a match
|
||||
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.
|
||||
|
||||
## FIFA 17 network-redirect milestone (2026-08-09)
|
||||
|
||||
Hook now installs a GENERIC network redirect on the fifa17 feature path (fifa17.rs
|
||||
install_network_redirect): getaddrinfo IAT patch + inline connect detour + WSAConnect
|
||||
IAT, with a configurable destination (connect_hook::set_target_ipv4) read from
|
||||
openfut.cfg (single-line IP). Deployed DLL md5 bc9e0bc6, cfg=10.10.0.120.
|
||||
|
||||
RESULT of live launch (client 105 -> server 120):
|
||||
- Error changed: "servers shut down" -> "Unable to connect to EA servers / check
|
||||
network". Redirect IS firing (progress).
|
||||
- BLOCKER A: getaddrinfo IAT patched 0+0 -> FIFA 17 does NOT resolve via IAT
|
||||
getaddrinfo in the main exe or EAWebKit.dll. Names resolved via another path
|
||||
(gethostbyname or internal DirtySDK resolver). So no hostname reached 120.
|
||||
- BLOCKER B (architectural): FIFA 17 online = Blaze binary TCP on high ports. Log
|
||||
shows connect 20.51.153.159:42230 sock_type=1 -> wsa_err=10035 (WOULDBLOCK->dead).
|
||||
Port 42230 is NOT in the remap set (443,10041,42127,3216) so it was not redirected.
|
||||
Even if redirected, the Docker bridge only speaks HTTPS on 8443 -- no Blaze
|
||||
listener exists for FIFA 17. This is a server-side build, not a hook tweak.
|
||||
|
||||
NEXT (evidence-first): add gethostbyname (and possibly a DirtySDK resolver) capture
|
||||
to learn the hostname behind 20.51.153.159; widen Blaze port remap; then scope a
|
||||
Blaze-speaking bridge listener before expecting the error to clear.
|
||||
|
||||
## DNS/getaddrinfo fix — RESOLVED (2026-08-09, hook md5 67e3639b)
|
||||
|
||||
Added src/resolver_hook.rs: INLINE detours at ws2_32 export addresses for
|
||||
getaddrinfo + GetAddrInfoW + gethostbyname (same unhook/rehook pattern as
|
||||
connect_hook). Replaces the IAT approach that patched 0 slots on FIFA 17.
|
||||
Wired into fifa17.rs install_network_redirect; hooks.rs gained redirect_ip_cstr()
|
||||
and redirect_ip_str() helpers.
|
||||
|
||||
LIVE RESULT (client 105 -> server 120):
|
||||
- resolver detours 3/3 installed.
|
||||
- getaddrinfo(winter15.gosredirector.ea.com) -> redirect. Game now dials
|
||||
10.10.0.120 (was 20.51.153.159 before). DNS BLOCKER A = SOLVED.
|
||||
|
||||
REMAINING BLOCKER B (architectural, NOT DNS): FIFA 17 online = EA Blaze binary
|
||||
TCP. Game connects 10.10.0.120:42230 (gosredirector/Blaze redirector) ->
|
||||
wsa_err=10035 (nothing listening). Two gaps: (1) connect_hook remap set lacks
|
||||
42230; (2) even remapped, the Docker bridge only serves HTTPS on 8443 — no Blaze
|
||||
listener exists. Clearing Unable to connect requires a Blaze redirector+main
|
||||
server on the bridge side (real server build), not a hook change.
|
||||
NOTE: the 3s TLS-handshake-EOF spam in bridge logs on :8443 is the LAUNCHER health
|
||||
poller, not the game.
|
||||
|
||||
@@ -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: d2a9a01ec9...f4f33969f2
@@ -3,7 +3,7 @@
|
||||
# OPENFUT_ADVERTISE — the address of THIS host as seen from the game machine
|
||||
# (105). The responders advertise it to the client for every next hop (Blaze,
|
||||
# roster, UTAS, POW). Compose refuses to start without it.
|
||||
OPENFUT_ADVERTISE=10.10.0.120
|
||||
OPENFUT_ADVERTISE=203.0.113.10 # <- REPLACE with this host's LAN IP
|
||||
|
||||
# OPENFUT_BIND — address the listeners bind inside the container.
|
||||
# Defaults to 0.0.0.0 (container-facing); the original all-on-localhost flow
|
||||
|
||||
@@ -24,7 +24,7 @@ services:
|
||||
OPENFUT_BIND: "${OPENFUT_BIND:-0.0.0.0}"
|
||||
# Address advertised to the client for the next hop. MUST be this host's
|
||||
# LAN IP as seen from the game machine (105). Required (see .env.example).
|
||||
OPENFUT_ADVERTISE: "${OPENFUT_ADVERTISE:?set OPENFUT_ADVERTISE in .env to this host's LAN IP, e.g. 10.10.0.120}"
|
||||
OPENFUT_ADVERTISE: "${OPENFUT_ADVERTISE:?set OPENFUT_ADVERTISE in .env to this host's LAN IP, e.g. 203.0.113.10}"
|
||||
# POW content advertises port 8080 by default, which collides with the
|
||||
# openfut-core publish on this host. Remap it to 8085 on the host and
|
||||
# advertise the remapped endpoint.
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
# Address behaviour is driven by two env vars (see each responder):
|
||||
# OPENFUT_BIND bind address for every listener (container: 0.0.0.0)
|
||||
# OPENFUT_ADVERTISE address handed to the client for the next hop
|
||||
# (the server's LAN IP, e.g. 10.10.0.120)
|
||||
# (the server's LAN IP, e.g. 203.0.113.10)
|
||||
# ============================================================================
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$(readlink -f "$0")")/tools"
|
||||
|
||||
BIND="${OPENFUT_BIND:-0.0.0.0}"
|
||||
ADV="${OPENFUT_ADVERTISE:?OPENFUT_ADVERTISE must be set to the server LAN IP (e.g. 10.10.0.120)}"
|
||||
ADV="${OPENFUT_ADVERTISE:?OPENFUT_ADVERTISE must be set to the server LAN IP (e.g. 203.0.113.10)}"
|
||||
export OPENFUT_BIND="$BIND"
|
||||
export OPENFUT_ADVERTISE="$ADV"
|
||||
# POW keys advertised by blaze must also point at the server, not loopback.
|
||||
|
||||
@@ -55,34 +55,6 @@ verify_exports() {
|
||||
done
|
||||
}
|
||||
|
||||
# Refuse any DLL that is not a FIFA-17-profile build.
|
||||
#
|
||||
# openfut-hook builds TWO mutually exclusive injection paths from one crate: the
|
||||
# default (FIFA 23) path installs getaddrinfo/connect/ProtoSSL/origin hooks, while
|
||||
# `--features fifa17` installs ONLY the FIFA-17-safe logic (module map, FIFA 17
|
||||
# cert-verify, SBC dispatch, store tab bind). Deploying a default-feature build
|
||||
# into FIFA 17 hijacks the login transport and the client reports "Unable to
|
||||
# connect to the EA servers", with none of the FIFA 17 repairs present.
|
||||
#
|
||||
# That exact mistake happened on 2026-08-19 (artifact 1c71a17a, hand-built without
|
||||
# the feature): two failed launches, diagnosed only by comparing embedded strings.
|
||||
# `build` below passes the feature, but a hand-built DLL can reach `stage`/`deploy`
|
||||
# via OPENFUT_FIFA17_HOOK_DLL, so assert the profile on the bytes themselves.
|
||||
verify_fifa17_profile() {
|
||||
local dll=$1 marker
|
||||
# Markers that MUST be present: the FIFA 17 target module and its repairs.
|
||||
for marker in 'CardsDLL_Win64_retail.dll' 'SBC_DISPATCH'; do
|
||||
grep -qaF -- "$marker" "$dll" ||
|
||||
die "$dll is not a --features fifa17 build (missing $marker); refusing to stage/deploy"
|
||||
done
|
||||
# Markers that MUST be absent: the FIFA-23-only transport hooking.
|
||||
for marker in 'getaddrinfo IAT patched' 'connect: inline-hooked' 'origin_spy'; do
|
||||
if grep -qaF -- "$marker" "$dll"; then
|
||||
die "$dll contains FIFA-23-only hook '$marker'; build with --features fifa17"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
verify_inputs() {
|
||||
command -v sha256sum >/dev/null || die "sha256sum is required"
|
||||
command -v x86_64-w64-mingw32-objdump >/dev/null ||
|
||||
@@ -90,7 +62,6 @@ verify_inputs() {
|
||||
need_file "$hook_dll"
|
||||
need_file "$system_version"
|
||||
verify_pe64 "$hook_dll"
|
||||
verify_fifa17_profile "$hook_dll"
|
||||
}
|
||||
|
||||
inspect() {
|
||||
@@ -158,7 +129,6 @@ deploy() {
|
||||
need_file "$manifest"
|
||||
verify_pe64 "$staged"
|
||||
verify_exports "$staged"
|
||||
verify_fifa17_profile "$staged"
|
||||
local recorded actual
|
||||
recorded="$(awk -F= '$1=="artifact_sha256"{print $2}' "$manifest")"
|
||||
actual="$(sha256 "$staged")"
|
||||
@@ -188,7 +158,7 @@ launch() {
|
||||
local trace_enabled=0
|
||||
local request_trace_enabled=0
|
||||
local notifier_trace_enabled=0
|
||||
local dispatch_enabled=0
|
||||
local commit_enabled=0
|
||||
case "$mode" in
|
||||
baseline)
|
||||
[[ "${OPENFUT_FIFA17_LAUNCH:-}" == "I_ACCEPT_M1_BASELINE_LAUNCH" ]] ||
|
||||
@@ -207,11 +177,14 @@ launch() {
|
||||
request_trace_enabled=1
|
||||
notifier_trace_enabled=1
|
||||
;;
|
||||
dispatch)
|
||||
[[ "${OPENFUT_FIFA17_DISPATCH:-}" == "I_ACCEPT_GUARDED_NATIVE_DISPATCH" ]] ||
|
||||
die "launch-dispatch requires OPENFUT_FIFA17_DISPATCH=I_ACCEPT_GUARDED_NATIVE_DISPATCH"
|
||||
commit)
|
||||
[[ "${OPENFUT_FIFA17_COMMIT:-}" == "I_ACCEPT_POST_PARSE_READY_BYTE" ]] ||
|
||||
die "launch-commit requires OPENFUT_FIFA17_COMMIT=I_ACCEPT_POST_PARSE_READY_BYTE"
|
||||
hook_enabled=1
|
||||
trace_enabled=1
|
||||
request_trace_enabled=1
|
||||
dispatch_enabled=1
|
||||
notifier_trace_enabled=1
|
||||
commit_enabled=1
|
||||
;;
|
||||
*) die "unknown launch mode: $mode" ;;
|
||||
esac
|
||||
@@ -234,7 +207,7 @@ launch() {
|
||||
done
|
||||
mkdir -p "${wine_prefix}/dosdevices"
|
||||
ln -sfn /mnt "${wine_prefix}/dosdevices/w:"
|
||||
note "Launching $mode mode (SBC_HOOK=$hook_enabled; SBC_TRACE=$trace_enabled; SBC_REQUEST_TRACE=$request_trace_enabled; SBC_NOTIFIER_TRACE=$notifier_trace_enabled; SBC_DISPATCH=$dispatch_enabled); log=/tmp/fifa17-hook-m1-launch.log"
|
||||
note "Launching $mode mode (SBC_HOOK=$hook_enabled; SBC_TRACE=$trace_enabled; SBC_REQUEST_TRACE=$request_trace_enabled; SBC_NOTIFIER_TRACE=$notifier_trace_enabled; SBC_COMMIT=$commit_enabled); log=/tmp/fifa17-hook-m1-launch.log"
|
||||
cd "$game_dir"
|
||||
env \
|
||||
GAMEID=fifa17 \
|
||||
@@ -245,8 +218,8 @@ launch() {
|
||||
OPENFUT_SBC_TRACE="$trace_enabled" \
|
||||
OPENFUT_SBC_REQUEST_TRACE="$request_trace_enabled" \
|
||||
OPENFUT_SBC_NOTIFIER_TRACE="$notifier_trace_enabled" \
|
||||
OPENFUT_SBC_DISPATCH="$dispatch_enabled" \
|
||||
OPENFUT_SBC_DISPATCH_TRACE=0 \
|
||||
OPENFUT_SBC_DISPATCH=0 \
|
||||
OPENFUT_SBC_COMMIT="$commit_enabled" \
|
||||
OPENFUT_SBC_ARM_ONLY=0 \
|
||||
OPENFUT_SBC_POPULATE=0 \
|
||||
umu-run _fifa17.exe 2>&1 | tee /tmp/fifa17-hook-m1-launch.log
|
||||
@@ -254,7 +227,7 @@ launch() {
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: fifa17-hook-m1.sh [inspect|build|stage|deploy|launch|launch-resolve|launch-trace|launch-dispatch]
|
||||
Usage: fifa17-hook-m1.sh [inspect|build|stage|deploy|launch|launch-resolve|launch-trace|launch-commit]
|
||||
|
||||
inspect Read-only PE/hash/export preflight (default).
|
||||
build Cross-build the inert FIFA17 hook, then run inspect.
|
||||
@@ -267,11 +240,11 @@ Usage: fifa17-hook-m1.sh [inspect|build|stage|deploy|launch|launch-resolve|launc
|
||||
Start M2 resolve-only mode (guarded reads/logging, no detours/writes); requires:
|
||||
OPENFUT_FIFA17_RESOLVE=I_ACCEPT_M2_RESOLVE_LAUNCH
|
||||
launch-trace
|
||||
Start the M3-M6 passive parser/request/notifier trace; requires:
|
||||
Start the single M3 passive factory/deserializer trace; requires:
|
||||
OPENFUT_FIFA17_TRACE=I_ACCEPT_M3_PASSIVE_TRACE
|
||||
launch-dispatch
|
||||
Trace and repair only a fully validated native status-999 completion; requires:
|
||||
OPENFUT_FIFA17_DISPATCH=I_ACCEPT_GUARDED_NATIVE_DISPATCH
|
||||
launch-commit
|
||||
Trace and arm the SBC cache only after a validated native parse; requires:
|
||||
OPENFUT_FIFA17_COMMIT=I_ACCEPT_POST_PARSE_READY_BYTE
|
||||
|
||||
Optional path overrides:
|
||||
OPENFUT_FIFA17_HOOK_DLL, OPENFUT_FIFA17_GAME_DIR,
|
||||
@@ -287,7 +260,7 @@ case "${1:-inspect}" in
|
||||
launch) launch baseline ;;
|
||||
launch-resolve) launch resolve ;;
|
||||
launch-trace) launch trace ;;
|
||||
launch-dispatch) launch dispatch ;;
|
||||
launch-commit) launch commit ;;
|
||||
-h|--help|help) usage ;;
|
||||
*) usage >&2; die "unknown command: $1" ;;
|
||||
esac
|
||||
|
||||
@@ -15,16 +15,27 @@ reimplementations (the `tdf` crate cloned in this scratchpad), which were used
|
||||
only as a cross-check of *structure*, never copied.
|
||||
NO EA/FIFA leaked source was consulted.
|
||||
|
||||
VALIDATED RULES (byte-exact round-trip against the 219-byte capture)
|
||||
--------------------------------------------------------------------
|
||||
Fire2 frame header, 16 bytes big-endian:
|
||||
[0:4] u32 payload length (bytes after the header)
|
||||
[4:6] u16 always 0 (observed)
|
||||
VALIDATED RULES (the TDF body; byte-exact round-trip against the 219-byte capture)
|
||||
---------------------------------------------------------------------------------
|
||||
Fire2 frame header, 16 bytes big-endian.
|
||||
|
||||
!!! SUPERSEDED — the [10:16] FIELD SEMANTICS below are WRONG for FIFA 17. !!!
|
||||
The "byte-exact round-trip" only proves the payload length and the TDF body
|
||||
encoding: decoding then re-encoding with the SAME (mis)labelled header layout
|
||||
trivially reproduces the capture, so it never tested the header's field
|
||||
boundaries. The authoritative, live-driven layout is
|
||||
`openfut-protocol-blaze::fire2` / `blaze_responder_v3b.py::fire2`:
|
||||
[0:4] u32 payload length (bytes after header + metadata)
|
||||
[4:6] u16 metadata length (0 when absent — what this file called "always 0")
|
||||
[6:8] u16 component
|
||||
[8:10] u16 command
|
||||
[10:12]u16 error / msgId
|
||||
[12] u8 msgType (0x01 ping, 0x02 request, 0x03 pong/response)
|
||||
[13:16]3 reserved bytes (observed 00 00 00)
|
||||
[10:13] u24 msgNum (this file WRONGLY split it as [10:12] msgId + [12] msgType)
|
||||
[13] u8 (msgType << 5) | (userIndex & 0x1F)
|
||||
[14] u8 options
|
||||
[15] u8 reserved
|
||||
There is NO error field in Fire2 (that is Fire v1) and NO jumbo escape — the
|
||||
length is already a full u32. `build_fire2_frame`/`decode_fire2` below keep the
|
||||
old wrong `>IHHHHB3s` layout; they are dead and retained only for history.
|
||||
|
||||
Heat2 field = 3-byte packed tag + 1 type byte + value.
|
||||
|
||||
@@ -359,7 +370,14 @@ MSG_ERROR = 0x05 # UNVERIFIED
|
||||
|
||||
def build_fire2_frame(component: int, command: int, msgType: int,
|
||||
msgId: int, tdf_bytes: bytes) -> bytes:
|
||||
"""16-byte big-endian Fire2 header + TDF payload."""
|
||||
"""16-byte big-endian Fire2 header + TDF payload.
|
||||
|
||||
WRONG HEADER (dead code): the ``>IHHHHB3s`` layout mislabels [10:16] — it
|
||||
puts a u16 msgId at [10:12] and msgType at [12]. FIFA 17's real Fire2 header
|
||||
is [10:13] u24 msgNum, [13] (msgType<<5)|userIndex, [14] options, [15]
|
||||
reserved, and has no error field. Use ``openfut-protocol-blaze::fire2`` or
|
||||
``blaze_responder_v3b.py::fire2``; this is retained only for history.
|
||||
"""
|
||||
tdf_bytes = bytes(tdf_bytes)
|
||||
hdr = struct.pack(">IHHHHB3s", len(tdf_bytes), 0, component & 0xFFFF,
|
||||
command & 0xFFFF, msgId & 0xFFFF, msgType & 0xFF,
|
||||
|
||||
@@ -162,19 +162,6 @@ def log(*a):
|
||||
print("[lsx]", *a, flush=True)
|
||||
|
||||
|
||||
def spawn_parent_watchdog():
|
||||
parent = os.getppid()
|
||||
|
||||
def _watch():
|
||||
while True:
|
||||
time.sleep(1)
|
||||
if os.getppid() != parent:
|
||||
log(f"launcher pid {parent} exited; stopping lsx")
|
||||
os._exit(0)
|
||||
|
||||
threading.Thread(target=_watch, daemon=True).start()
|
||||
|
||||
|
||||
_SECRET_ATTR_RE = re.compile(
|
||||
r'(?i)\b(AuthCode|AuthToken|SessionKey|Token|Sid)="[^"]*"')
|
||||
_AUTH_CODE_ATTR_RE = re.compile(r'(?i)\b(value|Code|Return)="[^"]*"')
|
||||
@@ -585,7 +572,6 @@ def serve(sock, addr):
|
||||
|
||||
|
||||
def main():
|
||||
spawn_parent_watchdog()
|
||||
s = socket.socket()
|
||||
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
s.bind((os.environ.get("OPENFUT_BIND", "127.0.0.1"), 4216))
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dump CardsDLL's 45-row route table from the ON-DISK PE. READ-ONLY, static.
|
||||
|
||||
The transfer-market analysis locates the table at .rdata 0x18021df80 as
|
||||
{char*, char*} rows. This resolves VA->file offset properly through the PE section
|
||||
table rather than assuming a single .text mapping, then prints every row so we can
|
||||
see whether any route other than `tradePile` could own a trade-pile ITEM list.
|
||||
"""
|
||||
import struct, sys
|
||||
|
||||
DLL = "/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll"
|
||||
TABLE_VA = 0x18021DF80
|
||||
MAX_ROWS = 64
|
||||
|
||||
pe = open(DLL, "rb").read()
|
||||
e_lfanew = struct.unpack_from("<I", pe, 0x3C)[0]
|
||||
assert pe[e_lfanew:e_lfanew + 4] == b"PE\0\0", "not a PE"
|
||||
coff = e_lfanew + 4
|
||||
nsec, opt_size = struct.unpack_from("<HH", pe, coff + 2), None
|
||||
num_sections = struct.unpack_from("<H", pe, coff + 2)[0]
|
||||
opt_size = struct.unpack_from("<H", pe, coff + 16)[0]
|
||||
opt = coff + 20
|
||||
magic = struct.unpack_from("<H", pe, opt)[0]
|
||||
assert magic == 0x20B, "expected PE32+"
|
||||
image_base = struct.unpack_from("<Q", pe, opt + 24)[0]
|
||||
sec_off = opt + opt_size
|
||||
|
||||
sections = []
|
||||
for i in range(num_sections):
|
||||
b = sec_off + i * 40
|
||||
name = pe[b:b + 8].rstrip(b"\0").decode("ascii", "replace")
|
||||
vsize, vaddr, rawsize, rawptr = struct.unpack_from("<IIII", pe, b + 8)
|
||||
sections.append((name, vaddr, vsize, rawptr, rawsize))
|
||||
|
||||
print("image_base=%#x sections=%d" % (image_base, num_sections))
|
||||
for s in sections:
|
||||
print(" %-8s rva=%#010x vsize=%#x rawptr=%#010x rawsize=%#x" % s)
|
||||
|
||||
|
||||
def va2off(va):
|
||||
rva = va - image_base
|
||||
for name, vaddr, vsize, rawptr, rawsize in sections:
|
||||
if vaddr <= rva < vaddr + max(vsize, rawsize):
|
||||
off = rva - vaddr + rawptr
|
||||
if off < len(pe):
|
||||
return off
|
||||
return None
|
||||
|
||||
|
||||
def cstr(va, limit=96):
|
||||
off = va2off(va)
|
||||
if off is None:
|
||||
return None
|
||||
end = pe.find(b"\0", off, off + limit)
|
||||
if end < 0:
|
||||
return None
|
||||
try:
|
||||
return pe[off:end].decode("ascii")
|
||||
except UnicodeDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
base = va2off(TABLE_VA)
|
||||
print("\nroute table VA %#x -> file offset %s" % (TABLE_VA, hex(base) if base else None))
|
||||
assert base, "table VA did not resolve"
|
||||
|
||||
print("\n%-4s %-34s %s" % ("#", "field A", "field B"))
|
||||
rows = 0
|
||||
for i in range(MAX_ROWS):
|
||||
a_va, b_va = struct.unpack_from("<QQ", pe, base + i * 16)
|
||||
a, b = cstr(a_va), cstr(b_va)
|
||||
if a is None and b is None:
|
||||
print("-- table ends after %d rows --" % rows)
|
||||
break
|
||||
print("%-4d %-34s %s" % (i, repr(a), repr(b)))
|
||||
rows += 1
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Pure unit test for the empty-My-Packs store resolver guard in autopatch.py.
|
||||
|
||||
Covers the fail-closed guard decision (original -> PATCH, already-patched -> NOOP,
|
||||
unknown -> SKIP) and pins the guarded patch table to the exact RVA/bytes proven on
|
||||
the tested FIFA 17 build (JNZ 0x14869 -> JG 0x14869 at CardsDLL RVA 0x14858).
|
||||
|
||||
Run: python3 test_autopatch_guard.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import autopatch # importable: runtime loop is guarded by `if __name__ == "__main__"`
|
||||
|
||||
GUARD_VA = 0x180014858
|
||||
ORIG = bytes.fromhex("750f") # JNZ 0x14869
|
||||
PATCH = bytes.fromhex("7f0f") # JG 0x14869
|
||||
|
||||
|
||||
def test_table_exact():
|
||||
assert autopatch.STORE_PATCHES_GUARDED == {GUARD_VA: (ORIG, PATCH)}, \
|
||||
autopatch.STORE_PATCHES_GUARDED
|
||||
# Byte-level pin so a bad hex literal cannot slip through.
|
||||
assert ORIG == b"\x75\x0f" and PATCH == b"\x7f\x0f"
|
||||
|
||||
|
||||
def test_decision():
|
||||
assert autopatch.guarded_action(ORIG, ORIG, PATCH) == "patch" # apply
|
||||
assert autopatch.guarded_action(PATCH, ORIG, PATCH) == "noop" # already patched
|
||||
assert autopatch.guarded_action(b"\x00\x00", ORIG, PATCH) == "skip" # build mismatch
|
||||
assert autopatch.guarded_action(b"\x90", ORIG, PATCH) == "skip" # wrong length
|
||||
|
||||
|
||||
def test_guard_state_after():
|
||||
# already patched (7f0f) -> VERIFIED (guarded_action "noop"); write args irrelevant.
|
||||
assert autopatch.guard_state_after(PATCH, ORIG, PATCH, True, PATCH) == autopatch.GUARD_VERIFIED
|
||||
# original (750f) + write ok + reread 7f0f -> VERIFIED (guarded_action "patch").
|
||||
assert autopatch.guard_state_after(ORIG, ORIG, PATCH, True, PATCH) == autopatch.GUARD_VERIFIED
|
||||
# original + write FAILS -> WRITE_FAILED.
|
||||
assert autopatch.guard_state_after(ORIG, ORIG, PATCH, False, ORIG) == autopatch.GUARD_WRITE_FAILED
|
||||
# original + write ok but reread != 7f0f -> VERIFY_FAILED.
|
||||
assert autopatch.guard_state_after(ORIG, ORIG, PATCH, True, ORIG) == autopatch.GUARD_VERIFY_FAILED
|
||||
assert autopatch.guard_state_after(ORIG, ORIG, PATCH, True, b"") == autopatch.GUARD_VERIFY_FAILED
|
||||
# unknown bytes -> UNSUPPORTED_BUILD (guarded_action "skip"); write args irrelevant.
|
||||
assert autopatch.guard_state_after(b"\x00\x00", ORIG, PATCH, True, PATCH) == autopatch.GUARD_UNSUPPORTED_BUILD
|
||||
|
||||
|
||||
def test_capability_constants():
|
||||
assert autopatch.EMPTY_MYPACKS_RESOLVER_VERSION == 1
|
||||
assert autopatch.EMPTY_MYPACKS_RESOLVER_CAPABILITY == "fifa17.empty_mypacks_resolver"
|
||||
# State constant values are the exact tokens carried in the emitted status line.
|
||||
assert autopatch.GUARD_VERIFIED == "VERIFIED"
|
||||
assert autopatch.GUARD_UNSUPPORTED_BUILD == "UNSUPPORTED_BUILD"
|
||||
assert autopatch.GUARD_WRITE_FAILED == "WRITE_FAILED"
|
||||
assert autopatch.GUARD_VERIFY_FAILED == "VERIFY_FAILED"
|
||||
assert autopatch.GUARD_NOT_ATTEMPTED == "NOT_ATTEMPTED"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_table_exact()
|
||||
test_decision()
|
||||
test_guard_state_after()
|
||||
test_capability_constants()
|
||||
print("OK: autopatch guard table + fail-closed decision + guard-state function + capability constants")
|
||||
+301
@@ -0,0 +1,301 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Tests for the FIFA 17 verified-patched-client capability negotiation.
|
||||
|
||||
The additive empty-My-Packs switch on top of the P2 65534 sentinel: the sentinel is
|
||||
suppressed for ONE FIFA session only when the launcher has registered a verified
|
||||
resolver capability (v1) that binds to THAT process's UTAS session (keyed by the
|
||||
per-login-unique X-UT-SID; source IP + persona are auxiliary). Every failure /
|
||||
unknown / late / cross-process / cross-session case is fail-closed to the sentinel.
|
||||
|
||||
The initial prototype keyed by source IP alone; this suite proves the hardened
|
||||
per-session binding, including two sessions that SHARE a source IP.
|
||||
|
||||
Matrix (docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md):
|
||||
A no-capability, zero packs -> sentinel
|
||||
B verified v1, zero packs -> clean (no 65534)
|
||||
C real unopened pack + no capability -> genuine pack, no sentinel
|
||||
D real unopened pack + capability -> genuine pack, no sentinel
|
||||
E unsupported version / capability -> endpoint 400 AND mode sentinel
|
||||
F late capability after sentinel freeze -> stays sentinel
|
||||
G capability disappears after clean freeze -> stays clean (immutable)
|
||||
H two IPs (A verified, B none) -> A clean, B sentinel (no global leak)
|
||||
I new session after reset -> fresh unpatched -> sentinel
|
||||
J autopatch mismatch => never registers -> sentinel
|
||||
K SAME IP, two sessions (A patched, B not) -> A clean, B sentinel
|
||||
L SAME IP+persona relaunch (old ok, new not) -> new session sentinel
|
||||
M SAME IP, failed-patch second session -> first clean, second sentinel
|
||||
N late registration when sessions are frozen -> does not modify active sessions
|
||||
O session cleanup / TTL expiry -> capability gone, sentinel
|
||||
P duplicate registration for a session -> idempotent; no post-freeze change
|
||||
Q register-before-login (pending consumed) -> clean
|
||||
R topology freeze immutable per SID -> no flip either way; new SID fresh
|
||||
|
||||
Standalone unit test in the project style: `python3 test_capability_negotiation.py`.
|
||||
"""
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
TOOLS = os.path.dirname(os.path.abspath(__file__))
|
||||
if TOOLS not in sys.path:
|
||||
sys.path.insert(0, TOOLS)
|
||||
|
||||
SENTINEL_ID = 65534
|
||||
REAL_PACK_ID = 1
|
||||
PERSONA = 111001
|
||||
|
||||
|
||||
class _H:
|
||||
"""Minimal request-handler stand-in: peer IP, optional X-UT-SID, optional body."""
|
||||
|
||||
def __init__(self, ip, body=None, sid=None):
|
||||
self.client_address = (ip, 54321)
|
||||
self.headers = {"X-UT-SID": sid} if sid is not None else {}
|
||||
self._body = json.dumps(body).encode("utf-8") if body is not None else b""
|
||||
|
||||
|
||||
def _ids(catalog):
|
||||
return [p["id"] for p in catalog["purchase"]]
|
||||
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory() as state:
|
||||
os.environ["FUT_ACCOUNT_PATH"] = os.path.join(state, "active_account.json")
|
||||
os.environ["FUT_PROFILE_ROOT"] = os.path.join(state, "accounts")
|
||||
os.environ.pop("FUT_PROFILE", None)
|
||||
|
||||
import fut_account
|
||||
import fut_store
|
||||
import fut_accounts
|
||||
import utas_server
|
||||
importlib.reload(fut_account)
|
||||
importlib.reload(fut_store)
|
||||
importlib.reload(fut_accounts)
|
||||
importlib.reload(utas_server)
|
||||
|
||||
us = utas_server
|
||||
CLEAN, SENT = us.FIFA17_MODE_CLEAN, us.FIFA17_MODE_SENTINEL
|
||||
|
||||
_orig_visible = us.visible_unopened_packs
|
||||
|
||||
def set_zero_packs():
|
||||
us.visible_unopened_packs = lambda: []
|
||||
|
||||
def set_real_pack():
|
||||
us.visible_unopened_packs = lambda: [REAL_PACK_ID]
|
||||
|
||||
def reset_state():
|
||||
us._FIFA17_SESSIONS.clear()
|
||||
us._FIFA17_PENDING.clear()
|
||||
|
||||
def auth(sid, ip, persona=PERSONA):
|
||||
"""Simulate /ut/auth opening a per-login session with a chosen sid."""
|
||||
us.fifa17_open_session(sid, ip, persona)
|
||||
|
||||
def register(ip, version, persona=PERSONA, pid=4242):
|
||||
return us.fifa17_capability_route(_H(ip, {
|
||||
"capability": "empty_mypacks_resolver", "version": version,
|
||||
"personaId": persona, "fifaPid": pid,
|
||||
}))
|
||||
|
||||
def store(sid, ip):
|
||||
status, cat = us.store_catalog(_H(ip, sid=sid))
|
||||
assert status == 200, status
|
||||
return _ids(cat)
|
||||
|
||||
def mode_of(sid):
|
||||
return us._FIFA17_SESSIONS[sid]["mode"]
|
||||
|
||||
try:
|
||||
# ---- A. no capability, zero packs -> sentinel ----------------------
|
||||
reset_state(); set_zero_packs()
|
||||
auth("sidA", "10.0.0.1")
|
||||
assert SENTINEL_ID in store("sidA", "10.0.0.1")
|
||||
assert mode_of("sidA") == SENT
|
||||
print("A no-capability zero-packs -> sentinel: OK")
|
||||
|
||||
# ---- B. verified v1, zero packs -> clean ---------------------------
|
||||
reset_state(); set_zero_packs()
|
||||
auth("sidB", "10.0.0.2")
|
||||
assert register("10.0.0.2", 1)[0] == 200
|
||||
ids = store("sidB", "10.0.0.2")
|
||||
assert SENTINEL_ID not in ids, ids
|
||||
assert mode_of("sidB") == CLEAN
|
||||
print("B verified-v1 zero-packs -> clean: OK")
|
||||
|
||||
# ---- C. real pack + no capability -> genuine, no sentinel ----------
|
||||
reset_state(); set_real_pack()
|
||||
auth("sidC", "10.0.0.3")
|
||||
ids = store("sidC", "10.0.0.3")
|
||||
assert REAL_PACK_ID in ids and SENTINEL_ID not in ids, ids
|
||||
print("C real-pack no-capability -> genuine, no sentinel: OK")
|
||||
|
||||
# ---- D. real pack + capability -> genuine, no sentinel -------------
|
||||
reset_state(); set_real_pack()
|
||||
auth("sidD", "10.0.0.4"); register("10.0.0.4", 1)
|
||||
ids = store("sidD", "10.0.0.4")
|
||||
assert REAL_PACK_ID in ids and SENTINEL_ID not in ids, ids
|
||||
print("D real-pack capability -> genuine, no sentinel: OK")
|
||||
|
||||
# ---- E. unsupported version / capability -> 400 + sentinel ---------
|
||||
reset_state(); set_zero_packs()
|
||||
auth("sidE", "10.0.0.5")
|
||||
assert register("10.0.0.5", 2)[0] == 400
|
||||
assert register("10.0.0.5", 99)[0] == 400
|
||||
assert us.fifa17_capability_route(
|
||||
_H("10.0.0.5", {"capability": "bogus", "version": 1}))[0] == 400
|
||||
assert SENTINEL_ID in store("sidE", "10.0.0.5")
|
||||
assert mode_of("sidE") == SENT
|
||||
print("E unsupported version/capability -> 400 + sentinel: OK")
|
||||
|
||||
# ---- F. late capability after sentinel freeze -> sentinel ----------
|
||||
reset_state(); set_zero_packs()
|
||||
auth("sidF", "10.0.0.6")
|
||||
assert SENTINEL_ID in store("sidF", "10.0.0.6") # freezes sentinel
|
||||
assert register("10.0.0.6", 1)[0] == 200 # session frozen -> ignored-late
|
||||
assert SENTINEL_ID in store("sidF", "10.0.0.6")
|
||||
assert mode_of("sidF") == SENT
|
||||
print("F late capability after sentinel freeze -> sentinel: OK")
|
||||
|
||||
# ---- G. capability disappears after clean freeze -> clean ----------
|
||||
reset_state(); set_zero_packs()
|
||||
auth("sidG", "10.0.0.7"); register("10.0.0.7", 1)
|
||||
assert SENTINEL_ID not in store("sidG", "10.0.0.7") # freezes clean
|
||||
us._FIFA17_SESSIONS["sidG"]["resolver"] = None # capability vanishes
|
||||
assert SENTINEL_ID not in store("sidG", "10.0.0.7")
|
||||
assert mode_of("sidG") == CLEAN
|
||||
print("G capability disappears after clean freeze -> clean: OK")
|
||||
|
||||
# ---- H. two IPs (A verified, B none) -> no global leak -------------
|
||||
reset_state(); set_zero_packs()
|
||||
auth("sidH1", "10.0.1.1"); register("10.0.1.1", 1)
|
||||
auth("sidH2", "10.0.1.2")
|
||||
assert SENTINEL_ID not in store("sidH1", "10.0.1.1")
|
||||
assert SENTINEL_ID in store("sidH2", "10.0.1.2")
|
||||
print("H two IPs (A clean, B sentinel) -> no global leak: OK")
|
||||
|
||||
# ---- I. new session after reset -> fresh unpatched -> sentinel -----
|
||||
reset_state(); set_zero_packs()
|
||||
auth("sidI1", "10.0.1.3"); register("10.0.1.3", 1)
|
||||
assert SENTINEL_ID not in store("sidI1", "10.0.1.3") # A clean
|
||||
us.fifa17_clear_pending("10.0.1.3") # relaunch boundary
|
||||
auth("sidI2", "10.0.1.3") # new SID, autopatch failed
|
||||
assert SENTINEL_ID in store("sidI2", "10.0.1.3")
|
||||
print("I new session after reset -> sentinel (no cross-process leak): OK")
|
||||
|
||||
# ---- J. autopatch mismatch => never registers -> sentinel ----------
|
||||
reset_state(); set_zero_packs()
|
||||
auth("sidJ", "10.0.1.4")
|
||||
assert SENTINEL_ID in store("sidJ", "10.0.1.4")
|
||||
print("J autopatch mismatch (never registers) -> sentinel: OK")
|
||||
|
||||
# ---- K. SAME IP, two sessions: patched A clean, unpatched B sent ---
|
||||
reset_state(); set_zero_packs()
|
||||
IP = "10.0.2.1"
|
||||
auth("sidK_A", IP)
|
||||
assert register(IP, 1)[0] == 200 # A sole candidate -> bound
|
||||
auth("sidK_B", IP) # B joins, never registers
|
||||
assert SENTINEL_ID not in store("sidK_A", IP)
|
||||
assert SENTINEL_ID in store("sidK_B", IP)
|
||||
print("K same-IP two sessions -> A clean, B sentinel: OK")
|
||||
|
||||
# ---- L. SAME IP+persona relaunch: old ok, new not -> new sentinel --
|
||||
reset_state(); set_zero_packs()
|
||||
IP = "10.0.2.2"
|
||||
auth("sidL_old", IP, PERSONA); register(IP, 1, PERSONA)
|
||||
assert SENTINEL_ID not in store("sidL_old", IP)
|
||||
us.fifa17_clear_pending(IP)
|
||||
auth("sidL_new", IP, PERSONA) # same persona, unverified
|
||||
assert SENTINEL_ID in store("sidL_new", IP)
|
||||
print("L same-IP+persona relaunch -> new session sentinel: OK")
|
||||
|
||||
# ---- M. SAME IP, failed-patch second session -----------------------
|
||||
reset_state(); set_zero_packs()
|
||||
IP = "10.0.2.3"
|
||||
auth("sidM1", IP); register(IP, 1)
|
||||
assert SENTINEL_ID not in store("sidM1", IP)
|
||||
auth("sidM2", IP) # autopatch failed
|
||||
assert SENTINEL_ID in store("sidM2", IP)
|
||||
print("M same-IP failed-patch second session -> sentinel: OK")
|
||||
|
||||
# ---- N. late reg when sessions frozen -> no active session change --
|
||||
reset_state(); set_zero_packs()
|
||||
IP = "10.0.2.4"
|
||||
auth("sidN1", IP); register(IP, 1)
|
||||
assert SENTINEL_ID not in store("sidN1", IP) # N1 frozen clean
|
||||
auth("sidN2", IP)
|
||||
assert SENTINEL_ID in store("sidN2", IP) # N2 frozen sentinel
|
||||
assert register(IP, 1)[0] == 200 # late: both frozen -> ignored
|
||||
assert SENTINEL_ID not in store("sidN1", IP) # unchanged
|
||||
assert SENTINEL_ID in store("sidN2", IP) # unchanged
|
||||
print("N late registration does not modify active sessions: OK")
|
||||
|
||||
# ---- O. session cleanup / TTL expiry -> capability gone ------------
|
||||
reset_state(); set_zero_packs()
|
||||
IP = "10.0.2.5"
|
||||
auth("sidO", IP); register(IP, 1)
|
||||
assert SENTINEL_ID not in store("sidO", IP) # clean while live
|
||||
us._FIFA17_SESSIONS["sidO"]["last_seen"] = (
|
||||
us._fifa17_now() - us.FIFA17_SESSION_TTL - 10.0)
|
||||
store("sidUNKNOWN", IP) # any op triggers reap
|
||||
assert "sidO" not in us._FIFA17_SESSIONS, "expired session not reaped"
|
||||
assert SENTINEL_ID in store("sidO", IP) # gone -> sentinel
|
||||
print("O session cleanup / TTL expiry -> sentinel: OK")
|
||||
|
||||
# ---- P. duplicate registration -> idempotent, no post-freeze change
|
||||
reset_state(); set_zero_packs()
|
||||
IP = "10.0.2.6"
|
||||
auth("sidP", IP)
|
||||
assert register(IP, 1)[0] == 200 # bound
|
||||
assert register(IP, 1)[0] == 200 # duplicate -> ignored-late
|
||||
assert SENTINEL_ID not in store("sidP", IP) # still clean
|
||||
assert register(IP, 1)[0] == 200 # after freeze
|
||||
assert SENTINEL_ID not in store("sidP", IP) # unchanged
|
||||
assert mode_of("sidP") == CLEAN
|
||||
print("P duplicate registration -> idempotent: OK")
|
||||
|
||||
# ---- Q. register-before-login: pending consumed at auth -> clean ---
|
||||
reset_state(); set_zero_packs()
|
||||
IP = "10.0.2.7"
|
||||
assert register(IP, 1)[0] == 200 # no session yet -> pending
|
||||
assert (IP, PERSONA) in us._FIFA17_PENDING
|
||||
auth("sidQ", IP, PERSONA) # consumes pending
|
||||
assert (IP, PERSONA) not in us._FIFA17_PENDING # single-use
|
||||
assert SENTINEL_ID not in store("sidQ", IP)
|
||||
assert mode_of("sidQ") == CLEAN
|
||||
print("Q register-before-login pending consumed -> clean: OK")
|
||||
|
||||
# ---- R. topology freeze immutable per SID; new SID decides fresh ----
|
||||
# F3 invariant: once a SID's store topology is decided it NEVER flips,
|
||||
# in either direction, and a different SID may decide differently.
|
||||
reset_state(); set_zero_packs()
|
||||
IP = "10.0.2.8"
|
||||
# frozen Sentinel never becomes Clean, even if a capability appears later
|
||||
auth("sidR_s", IP)
|
||||
assert SENTINEL_ID in store("sidR_s", IP) # freeze Sentinel
|
||||
register(IP, 1)
|
||||
us._FIFA17_SESSIONS["sidR_s"]["resolver"] = 1 # force-present capability
|
||||
assert SENTINEL_ID in store("sidR_s", IP) # STILL Sentinel
|
||||
assert mode_of("sidR_s") == SENT
|
||||
# frozen Clean never becomes Sentinel, even if the capability is wiped
|
||||
auth("sidR_c", IP); register(IP, 1)
|
||||
assert SENTINEL_ID not in store("sidR_c", IP) # freeze Clean
|
||||
us._FIFA17_SESSIONS["sidR_c"]["resolver"] = None # capability vanishes
|
||||
assert SENTINEL_ID not in store("sidR_c", IP) # STILL Clean
|
||||
assert mode_of("sidR_c") == CLEAN
|
||||
# a fresh SID (same IP) decides independently
|
||||
auth("sidR_new", IP)
|
||||
assert SENTINEL_ID in store("sidR_new", IP)
|
||||
print("R topology freeze immutable per SID; new SID fresh: OK")
|
||||
|
||||
finally:
|
||||
us.visible_unopened_packs = _orig_visible
|
||||
|
||||
print("capability negotiation matrix A-R: OK")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Executable
+136
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression tests for the empty-My-Packs FIFA 17 compatibility workaround (bug 6c).
|
||||
|
||||
Pins the behavior store_catalog() now depends on:
|
||||
- unopenedPackIds == [] -> exactly one synthetic active `mypacks` placeholder id 65534
|
||||
- unopenedPackIds == [70] -> no synthetic placeholder; the genuine owned pack is shown
|
||||
- synthetic id 65534 stays economy-safe (non-resolvable, non-openable, non-granting)
|
||||
- normal store packs (1/5/6/7) are untouched by the empty-state behavior
|
||||
|
||||
See docs/evidence/STORE_TILE_6C.md and FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md.
|
||||
Standalone unit test in the project style: `python3 test_empty_mypacks.py`.
|
||||
"""
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
TOOLS = os.path.dirname(os.path.abspath(__file__))
|
||||
if TOOLS not in sys.path:
|
||||
sys.path.insert(0, TOOLS)
|
||||
|
||||
SENTINEL_ID = 65534
|
||||
|
||||
|
||||
def _set_unopened(fut_store, ids):
|
||||
"""Deterministically set the active profile's owned unopened packs."""
|
||||
p = fut_store.STORE.load()
|
||||
p["unopenedPackIds"] = list(ids)
|
||||
fut_store.STORE._save()
|
||||
|
||||
|
||||
def _mypacks(catalog):
|
||||
return [p for p in catalog["purchase"]
|
||||
if (p.get("displayGroup") or {}).get("value") == "mypacks"]
|
||||
|
||||
|
||||
def _fake_request(command, body):
|
||||
class _H:
|
||||
pass
|
||||
h = _H()
|
||||
h.command = command
|
||||
h._body = body
|
||||
return h
|
||||
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory() as state:
|
||||
os.environ["FUT_ACCOUNT_PATH"] = os.path.join(state, "active_account.json")
|
||||
os.environ["FUT_PROFILE_ROOT"] = os.path.join(state, "accounts")
|
||||
os.environ.pop("FUT_PROFILE", None)
|
||||
|
||||
import fut_account
|
||||
import fut_store
|
||||
import fut_accounts
|
||||
import utas_server
|
||||
importlib.reload(fut_account)
|
||||
importlib.reload(fut_store)
|
||||
importlib.reload(fut_accounts)
|
||||
importlib.reload(utas_server)
|
||||
|
||||
fut_accounts.activate({"personaId": 111001, "personaName": "TEST_A"})
|
||||
catalog_ids = [p["id"] for p in fut_store.PACK_CATALOG]
|
||||
|
||||
# ---- A. Empty unopened packs -> one active synthetic 65534 placeholder ----
|
||||
_set_unopened(fut_store, [])
|
||||
utas_server._OPENED_PACK_GRACE.clear()
|
||||
status, cat = utas_server.store_catalog(None)
|
||||
assert status == 200
|
||||
myp = _mypacks(cat)
|
||||
assert len(myp) == 1, "expected exactly one mypacks entry, got %r" % myp
|
||||
s = myp[0]
|
||||
assert s["id"] == SENTINEL_ID, s
|
||||
assert s["state"] == "active", s # the P2 fix: active, not inactive
|
||||
assert (s.get("displayGroup") or {}).get("value") == "mypacks", s
|
||||
assert SENTINEL_ID not in catalog_ids, "65534 must not be in PACK_CATALOG"
|
||||
assert fut_store.pack_by_id(SENTINEL_ID) is None
|
||||
print("A empty-state active placeholder: PASS")
|
||||
|
||||
# ---- D (empty half). Normal packs untouched in empty state ----
|
||||
norm = {p["id"]: p for p in cat["purchase"] if p["id"] in (1, 5, 6, 7)}
|
||||
assert set(norm) == {1, 5, 6, 7}, sorted(norm)
|
||||
assert all(norm[i]["state"] == "active" for i in norm), norm
|
||||
assert norm[1]["packType"] == "BRONZE" and norm[1]["description"] == "Bronze Pack"
|
||||
|
||||
# ---- B. Non-empty unopened packs -> NO synthetic; genuine owned pack shown ----
|
||||
_set_unopened(fut_store, [70])
|
||||
utas_server._OPENED_PACK_GRACE.clear()
|
||||
status, cat = utas_server.store_catalog(None)
|
||||
assert status == 200
|
||||
ids = [p["id"] for p in cat["purchase"]]
|
||||
assert SENTINEL_ID not in ids, "synthetic placeholder must be suppressed when a pack exists"
|
||||
myp = _mypacks(cat)
|
||||
assert len(myp) == 1 and myp[0]["id"] == 70, myp
|
||||
assert myp[0]["state"] == "active" and myp[0]["unopened"] is True, myp[0]
|
||||
# normal packs still intact alongside the owned pack
|
||||
assert {1, 5, 6, 7}.issubset(set(ids)), sorted(ids)
|
||||
print("B non-empty-state genuine pack: PASS")
|
||||
|
||||
# ---- C. Economy safety of the synthetic placeholder ----
|
||||
_set_unopened(fut_store, [])
|
||||
utas_server._OPENED_PACK_GRACE.clear()
|
||||
coins0 = fut_store.STORE.coins()
|
||||
items0 = len(fut_store.STORE.items())
|
||||
next0 = fut_store.STORE.load()["nextItemId"]
|
||||
|
||||
assert fut_store.pack_by_id(SENTINEL_ID) is None
|
||||
|
||||
# store_buy: a confirmed-buy transaction for 65534 must be a no-op {}
|
||||
status, body = utas_server.store_buy(
|
||||
_fake_request("PUT", b'{"packId":65534,"state":"TRANSACTIONCREATED"}'))
|
||||
assert status == 200 and body == {}, (status, body)
|
||||
|
||||
# purchased_items: POST buy for 65534 must not open/grant anything
|
||||
status, body = utas_server.purchased_items(
|
||||
_fake_request("POST", b'{"packId":65534,"useCredits":1,"usePreOrder":0,"currency":"COINS"}'))
|
||||
assert status == 200, (status, body)
|
||||
assert "createPackResponse" not in body, body
|
||||
|
||||
# 65534 cannot enter the owned-pack pile (not a catalog pack)
|
||||
assert fut_store.STORE.grant_unopened_pack(SENTINEL_ID) is False
|
||||
assert SENTINEL_ID not in fut_store.STORE.unopened_packs()
|
||||
|
||||
# nothing mutated
|
||||
assert fut_store.STORE.coins() == coins0, (fut_store.STORE.coins(), coins0)
|
||||
assert len(fut_store.STORE.items()) == items0
|
||||
assert fut_store.STORE.load()["nextItemId"] == next0
|
||||
assert not any(i.get("id") == SENTINEL_ID or i.get("resourceId") == SENTINEL_ID
|
||||
for i in fut_store.STORE.items())
|
||||
print("C economy safety (65534 non-openable / non-granting): PASS")
|
||||
|
||||
print("empty My Packs compatibility: PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read the FIFA 17 TRADING gate byte out of the live client. READ-ONLY.
|
||||
|
||||
Extends tools/gate_byte_probe.py with vtable slot +0x270 (IS_TRADING_ENABLED,
|
||||
displacement 0x1fd2e) plus the two pile-size dwords, which the transfer-market
|
||||
analysis names as the market screen's CardsDLL-supplied inputs.
|
||||
|
||||
Opens /proc/<pid>/mem O_RDONLY and preads. Nothing here can write.
|
||||
"""
|
||||
import os, struct
|
||||
|
||||
pid = None
|
||||
for d in os.listdir('/proc'):
|
||||
if d.isdigit():
|
||||
try:
|
||||
if open('/proc/%s/comm' % d).read().strip() == 'FIFA17.exe':
|
||||
pid = int(d)
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
assert pid, "FIFA17.exe not running"
|
||||
|
||||
base = None
|
||||
for ln in open('/proc/%d/maps' % pid):
|
||||
if 'CardsDLL' in ln:
|
||||
base = int(ln.split('-')[0], 16)
|
||||
assert base, "CardsDLL not mapped (client has not reached Ultimate Team)"
|
||||
slide = base - 0x180000000
|
||||
|
||||
fd = os.open('/proc/%d/mem' % pid, os.O_RDONLY)
|
||||
|
||||
|
||||
def rd(va, n):
|
||||
return os.pread(fd, n, va)
|
||||
|
||||
|
||||
# Control: the FNV atom-hash prologue must match the on-disk PE before any other
|
||||
# address is trusted.
|
||||
pe = open('/mnt/games/FIFA 17/CardsDLL_Win64_retail.dll', 'rb').read()
|
||||
|
||||
|
||||
def f(va):
|
||||
return va - 0x180000000 - 0x1000 + 0x400
|
||||
|
||||
|
||||
ok = pe[f(0x180180d00):f(0x180180d00) + 32] == rd(0x180180d00 + slide, 32)
|
||||
print("pid=%d slide=%#x FNV control=%s" % (pid, slide, "MATCH" if ok else "MISMATCH"))
|
||||
assert ok, "slide not proven; refusing to read further"
|
||||
|
||||
obj = struct.unpack('<Q', rd(0x1802e6398 + slide, 8))[0]
|
||||
vt = struct.unpack('<Q', rd(obj, 8))[0]
|
||||
print("model=%#x vtable(static)=%#x" % (obj, vt - slide))
|
||||
|
||||
SLOTS = [
|
||||
(0x270, 'IS_TRADING_ENABLED '),
|
||||
(0x2b0, 'IS_FRIENDLY_SEASON '),
|
||||
(0x2c8, 'IS_DRAFT_MODE '),
|
||||
(0x2e0, 'packOpeningAnimation '),
|
||||
]
|
||||
print("\n-- gate bytes decoded from their accessor stubs --")
|
||||
for off, name in SLOTS:
|
||||
slot = struct.unpack('<Q', rd(vt + off, 8))[0]
|
||||
stub = rd(slot, 8)
|
||||
if stub[:3] == b'\x0f\xb6\x81':
|
||||
disp = struct.unpack('<I', stub[3:7])[0]
|
||||
val = rd(obj + disp, 1)[0]
|
||||
print(" slot +%#05x %s disp=%#x VALUE=%d" % (off, name, disp, val))
|
||||
else:
|
||||
print(" slot +%#05x %s NOT a movzx stub: %s" % (off, name, stub.hex()))
|
||||
|
||||
print("\n-- market screen inputs --")
|
||||
for disp, name in [(0x1fd1c, 'TRADE_PILE_SIZE'), (0x1fd20, 'watchListSize '),
|
||||
(0x1fd2e, 'tradingEnabled '), (0x1fd2f, 'storeEnabled ')]:
|
||||
print(" model+%#x %s = %d" % (disp, name, rd(obj + disp, 1)[0]))
|
||||
|
||||
os.close(fd)
|
||||
@@ -11,7 +11,7 @@ Rules (from CardsDLL 0x18016D230 / 0x1801a33a0):
|
||||
* body must parse as JSON (else err 0x3E6); 204 + empty body is accepted.
|
||||
* [resp+0x1c] == 0 is the success test; 404 is OK only on the first user GET.
|
||||
"""
|
||||
import copy, datetime, json, os, random, re, sys, http.server
|
||||
import copy, datetime, json, os, random, re, sys, threading, time, http.server
|
||||
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -52,6 +52,173 @@ def visible_unopened_packs():
|
||||
return STORE.unopened_packs() + list(_OPENED_PACK_GRACE)
|
||||
|
||||
|
||||
# ---- FIFA17 empty-My-Packs capability negotiation (PER-SESSION, hardened) ----
|
||||
# The synthetic 65534 sentinel (store_catalog) is the universal P2 fallback. It is
|
||||
# suppressed for ONE FIFA session only when the launcher has registered that THAT
|
||||
# process positively verified the CardsDLL resolver guard (RVA 0x14858 == JG).
|
||||
#
|
||||
# BINDING: the authoritative key is the per-login-unique UTAS session id (X-UT-SID),
|
||||
# minted fresh at every /ut/auth and echoed by the client on every later call incl.
|
||||
# /store/purchasegroup (live-confirmed present on real store requests). The initial
|
||||
# prototype keyed on source IP ALONE; that was rejected because two FIFA processes
|
||||
# (concurrent or relaunched) share an IP, so an unverified process could inherit a
|
||||
# verified one's clean topology and crash. IP + persona are retained only as
|
||||
# auxiliary data: a fail-closed sid/ip sanity check and the (ip,persona) key for the
|
||||
# short-lived launcher->session hand-off.
|
||||
#
|
||||
# The launcher verifies out-of-band (autopatch) and cannot know the SID, so its
|
||||
# registration is staged as a SINGLE-USE, short-TTL PENDING keyed by (ip,persona)
|
||||
# and bound to exactly one FIFA session (directly if that session already exists,
|
||||
# else consumed at the session's login or its first store request). Fail-closed
|
||||
# everywhere: unknown / expired / absent / ambiguous / late => sentinel.
|
||||
# See docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md (§Session binding).
|
||||
FIFA17_EMPTY_MYPACKS_RESOLVER_VERSION = 1
|
||||
FIFA17_MODE_SENTINEL = "sentinel"
|
||||
FIFA17_MODE_CLEAN = "clean-v1"
|
||||
FIFA17_SESSION_TTL = 3600.0 # reap a FIFA session after this many idle seconds
|
||||
FIFA17_PENDING_TTL = 120.0 # a launcher capability may await its session this long
|
||||
|
||||
# sid -> {"ip","persona","resolver": Optional[int],"mode": Optional[str],"created","last_seen"}
|
||||
_FIFA17_SESSIONS = {}
|
||||
# (ip, persona) -> {"resolver": int, "ts"}: single-use launcher->session hand-off.
|
||||
_FIFA17_PENDING = {}
|
||||
_FIFA17_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _fifa17_now():
|
||||
return time.monotonic()
|
||||
|
||||
|
||||
def _fifa17_client_ip(h):
|
||||
"""Peer IP for the handler, or None when unavailable (e.g. h is None)."""
|
||||
try:
|
||||
return h.client_address[0]
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _fifa17_sid(h):
|
||||
"""The client's UTAS session id (X-UT-SID) for this request, or None."""
|
||||
try:
|
||||
return h.headers.get("X-UT-SID")
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _fifa17_sidlog(sid):
|
||||
"""A short, non-secret tag for correlating a session in logs."""
|
||||
return ("\u2026" + sid[-6:]) if sid else "-"
|
||||
|
||||
|
||||
def _fifa17_mint_sid():
|
||||
"""A fresh, per-login-unique UTAS session id (same shape/length as the legacy
|
||||
constant). Uniqueness -- not unpredictability -- is what the binding needs."""
|
||||
return "OPENFUT-SID-%016X" % random.getrandbits(64)
|
||||
|
||||
|
||||
def _fifa17_reap_locked(now):
|
||||
for sid in [s for s, r in _FIFA17_SESSIONS.items()
|
||||
if now - r["last_seen"] > FIFA17_SESSION_TTL]:
|
||||
del _FIFA17_SESSIONS[sid]
|
||||
for key in [k for k, p in _FIFA17_PENDING.items()
|
||||
if now - p["ts"] > FIFA17_PENDING_TTL]:
|
||||
del _FIFA17_PENDING[key]
|
||||
|
||||
|
||||
def _fifa17_take_pending_locked(ip, persona, now):
|
||||
"""Single-use: remove and return a fresh pending resolver for (ip,persona)."""
|
||||
p = _FIFA17_PENDING.get((ip, persona))
|
||||
if p is not None and now - p["ts"] <= FIFA17_PENDING_TTL:
|
||||
del _FIFA17_PENDING[(ip, persona)]
|
||||
return p["resolver"]
|
||||
return None
|
||||
|
||||
|
||||
def fifa17_session_known(sid):
|
||||
"""True if sid is a live session (or the legacy constant, accepted by the
|
||||
retired security-question gate ONLY -- never used to grant clean store mode)."""
|
||||
if sid == SID:
|
||||
return True
|
||||
with _FIFA17_LOCK:
|
||||
return sid in _FIFA17_SESSIONS
|
||||
|
||||
|
||||
def fifa17_open_session(sid, ip, persona):
|
||||
"""/ut/auth: open a per-login session and bind any pending launcher capability
|
||||
for (ip,persona) that arrived before login."""
|
||||
if not sid:
|
||||
return
|
||||
now = _fifa17_now()
|
||||
with _FIFA17_LOCK:
|
||||
_fifa17_reap_locked(now)
|
||||
resolver = _fifa17_take_pending_locked(ip, persona, now)
|
||||
_FIFA17_SESSIONS[sid] = {"ip": ip, "persona": persona, "resolver": resolver,
|
||||
"mode": None, "created": now, "last_seen": now}
|
||||
log("[fifa17-store] session opened %s (ip=%s persona=%s resolver=%s)"
|
||||
% (_fifa17_sidlog(sid), ip, persona, resolver))
|
||||
|
||||
|
||||
def fifa17_clear_pending(ip):
|
||||
"""/openfut/account/sync hygiene: drop any stale pending for this machine so a
|
||||
new launch's unverified session cannot inherit a leftover capability."""
|
||||
now = _fifa17_now()
|
||||
with _FIFA17_LOCK:
|
||||
_fifa17_reap_locked(now)
|
||||
for key in [k for k in _FIFA17_PENDING if k[0] == ip]:
|
||||
del _FIFA17_PENDING[key]
|
||||
|
||||
|
||||
def fifa17_register_capability(ip, persona, version):
|
||||
"""Launcher registration. Returns one of:
|
||||
"bound" exactly one live, unfrozen, unbound session for (ip,persona)
|
||||
existed (registration after login -- the common case): bound now.
|
||||
"pending" no session for (ip,persona) yet (before login): staged single-use.
|
||||
"ignored-late" a session for (ip,persona) exists but is frozen or ambiguous
|
||||
(>1 unbound): NOT staged, so no later/unverified process can
|
||||
inherit it. Fail-closed.
|
||||
Never authorizes more than one session."""
|
||||
now = _fifa17_now()
|
||||
with _FIFA17_LOCK:
|
||||
_fifa17_reap_locked(now)
|
||||
sessions = [r for r in _FIFA17_SESSIONS.values()
|
||||
if r["ip"] == ip and r["persona"] == persona]
|
||||
candidates = [r for r in sessions if r["mode"] is None and r["resolver"] is None]
|
||||
if len(candidates) == 1:
|
||||
candidates[0]["resolver"] = version
|
||||
return "bound"
|
||||
if sessions:
|
||||
return "ignored-late"
|
||||
_FIFA17_PENDING[(ip, persona)] = {"resolver": version, "ts": now}
|
||||
return "pending"
|
||||
|
||||
|
||||
def fifa17_empty_mypacks_mode(sid, ip):
|
||||
"""Freeze (once) and return the empty-My-Packs mode for FIFA session `sid`.
|
||||
Freeze point = the first /store/purchasegroup of the session. Fail-closed: an
|
||||
unknown session, or a sid presented from a different IP than it was opened on,
|
||||
resolves to the sentinel."""
|
||||
now = _fifa17_now()
|
||||
with _FIFA17_LOCK:
|
||||
_fifa17_reap_locked(now)
|
||||
rec = _FIFA17_SESSIONS.get(sid)
|
||||
if rec is None:
|
||||
return FIFA17_MODE_SENTINEL
|
||||
rec["last_seen"] = now
|
||||
if rec["ip"] is not None and ip is not None and rec["ip"] != ip:
|
||||
log("[fifa17-store] sid %s ip mismatch (session %s != request %s) -> sentinel"
|
||||
% (_fifa17_sidlog(sid), rec["ip"], ip))
|
||||
return FIFA17_MODE_SENTINEL
|
||||
if rec["mode"] is None:
|
||||
if rec["resolver"] is None:
|
||||
rec["resolver"] = _fifa17_take_pending_locked(rec["ip"], rec["persona"], now)
|
||||
rec["mode"] = (FIFA17_MODE_CLEAN
|
||||
if rec["resolver"] == FIFA17_EMPTY_MYPACKS_RESOLVER_VERSION
|
||||
else FIFA17_MODE_SENTINEL)
|
||||
log("[fifa17-store] session %s empty-mypacks mode frozen: %s"
|
||||
% (_fifa17_sidlog(sid), rec["mode"]))
|
||||
return rec["mode"]
|
||||
|
||||
|
||||
def now():
|
||||
return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
@@ -102,7 +269,7 @@ def security_question_route(h):
|
||||
well-formed value without retaining or comparing it. Account selection has
|
||||
already initialized the server-owned verified compatibility state.
|
||||
"""
|
||||
if h.headers.get("X-UT-SID") != SID:
|
||||
if not fifa17_session_known(h.headers.get("X-UT-SID")):
|
||||
log("[FUT] security-question request has no matching OpenFUT session")
|
||||
return 400, {"reason": "invalid_session"}
|
||||
|
||||
@@ -193,11 +360,19 @@ def auth_body(h=None):
|
||||
except Exception as e: # adoption must never break auth
|
||||
log(" AUTH: adopt failed (%s: %s) -- keeping %s/%r"
|
||||
% (type(e).__name__, e, before[0], before[1]))
|
||||
return {"protocol": 1, "sid": SID, "serverTime": now(), "lastOnlineTime": now()}
|
||||
sid = _fifa17_mint_sid()
|
||||
fifa17_open_session(sid, _fifa17_client_ip(h), ACCOUNT.persona_id)
|
||||
return {"protocol": 1, "sid": sid, "serverTime": now(), "lastOnlineTime": now()}
|
||||
|
||||
|
||||
def account_sync_route(h):
|
||||
"""Launcher-only active-profile selection, before LSX/Blaze login starts."""
|
||||
# Pre-launch hygiene: drop any stale launcher capability still pending for this
|
||||
# machine so a new launch's unverified FIFA session cannot inherit it. The real
|
||||
# per-process session is opened later, at /ut/auth (keyed by the minted X-UT-SID).
|
||||
ip = _fifa17_client_ip(h)
|
||||
fifa17_clear_pending(ip)
|
||||
log(" ACCOUNT: cleared stale FIFA17 pending capability for ip %s" % ip)
|
||||
try:
|
||||
body = json.loads(h._body.decode("utf-8")) if getattr(h, "_body", b"") else {}
|
||||
account = activate_account(body)
|
||||
@@ -209,6 +384,33 @@ def account_sync_route(h):
|
||||
return 200, {"account": account, "status": "OK"}
|
||||
|
||||
|
||||
def fifa17_capability_route(h):
|
||||
"""POST /openfut/fifa17/capability -- launcher registers a verified resolver
|
||||
capability for the current FIFA process (bound to the peer IP). Fail-closed:
|
||||
anything but capability==empty_mypacks_resolver && version==current is a 400
|
||||
that records NOTHING (the session stays on the sentinel fallback)."""
|
||||
try:
|
||||
body = json.loads(h._body.decode("utf-8")) if getattr(h, "_body", b"") else {}
|
||||
except Exception:
|
||||
return 400, {"error": "unsupported capability"}
|
||||
if not isinstance(body, dict):
|
||||
return 400, {"error": "unsupported capability"}
|
||||
try:
|
||||
version = int(body.get("version"))
|
||||
except (TypeError, ValueError):
|
||||
return 400, {"error": "unsupported capability"}
|
||||
if (body.get("capability") != "empty_mypacks_resolver"
|
||||
or version != FIFA17_EMPTY_MYPACKS_RESOLVER_VERSION):
|
||||
return 400, {"error": "unsupported capability"}
|
||||
ip = _fifa17_client_ip(h)
|
||||
persona = body.get("personaId")
|
||||
fifa_pid = body.get("fifaPid", "?")
|
||||
status = fifa17_register_capability(ip, persona, version)
|
||||
log("[fifa17-store] capability empty_mypacks_resolver=%s ip=%s persona=%s "
|
||||
"fifa_pid=%s -> %s" % (version, ip, persona, fifa_pid, status))
|
||||
return 200, {"status": "OK"}
|
||||
|
||||
|
||||
def current_squad():
|
||||
"""The squad the client should see: the persisted one (item refs re-embedded
|
||||
from the club) or the seed ladder squad on first run.
|
||||
@@ -1203,6 +1405,10 @@ ROUTES = [
|
||||
# Launcher control-plane endpoint. It is intentionally outside /ut so FIFA
|
||||
# never calls it; launch is blocked unless this succeeds first.
|
||||
(re.compile(r"^/openfut/account/sync$"), lambda m, h: account_sync_route(h)),
|
||||
# Launcher registers a verified per-FIFA-process resolver capability (bound to
|
||||
# peer IP). Adjacent to account/sync, above the generic /ut routes; FIFA never
|
||||
# calls it. Fail-closed: absent/late/wrong-version => sentinel (store_catalog).
|
||||
(re.compile(r"^/openfut/fifa17/capability$"), lambda m, h: fifa17_capability_route(h)),
|
||||
# ---- FUT item-definition endpoints (must precede generic /item, /user) ----
|
||||
(re.compile(G + r"/item/resource"), lambda m, h: defs_route(h)),
|
||||
(re.compile(G + r"/defid"), lambda m, h: defs_route(h)),
|
||||
@@ -3426,12 +3632,42 @@ def store_catalog(h):
|
||||
if owned:
|
||||
packs.append(_pack_body(owned, idx, owned=True))
|
||||
if not owned_ids:
|
||||
# GOTO_STORE_MYPACK resolves the hard-coded `mypacks` group before it
|
||||
# renders rows. If the group is absent FIFA falls back to Bronze and
|
||||
# shows the empty-category dialog over the wrong tab. Retain an inactive
|
||||
# zero-item sentinel so the destination resolves, while state != active
|
||||
# keeps it out of the visible row list. Its id is deliberately absent
|
||||
# from PACK_CATALOG, so both purchase/open handlers reject it as well.
|
||||
# ADDITIVE capability switch (see docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md
|
||||
# §7/§9). This is the session-freeze point: the empty-mypacks decision for
|
||||
# this FIFA session (keyed by its X-UT-SID) is committed here at the first
|
||||
# /store/purchasegroup and is immutable for the session thereafter.
|
||||
mode = fifa17_empty_mypacks_mode(_fifa17_sid(h), _fifa17_client_ip(h))
|
||||
if mode == FIFA17_MODE_CLEAN:
|
||||
# Verified patched client: emit NO mypacks group; the CardsDLL resolver
|
||||
# guard (RVA 0x14858 JG) routes the -1 ordinal to Browse instead of
|
||||
# dereferencing a null group. (append nothing)
|
||||
pass
|
||||
else:
|
||||
# EMPTY MY PACKS -- FIFA 17 client-compatibility workaround (bug 6c, P2).
|
||||
#
|
||||
# The 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).
|
||||
# CardsDLL FUN_1800147f0 then dereferences the resolved group with NO null
|
||||
# guard, so if no `mypacks` group exists the client CRASHES
|
||||
# (CardsDLL_Win64_retail.dll+0x14882, read of [NULL+0x48] -- confirmed by
|
||||
# minidump). We therefore MUST emit a `mypacks` group when empty.
|
||||
#
|
||||
# state="inactive" avoids the crash but makes the client report the pack
|
||||
# unavailable immediately on Store entry and bounce to the Hub. state="active"
|
||||
# keeps the group structurally valid AND lets the Store open normally; the
|
||||
# empty tile renders as "0 items" and an explicit open is rejected
|
||||
# CLIENT-SIDE ("This pack is no longer available") -- it sends NO backend
|
||||
# request and mutates nothing.
|
||||
#
|
||||
# id 65534 is deliberately ABSENT from PACK_CATALOG, so pack_by_id() returns
|
||||
# None and store_buy()/purchased_items() cannot open it, grant items/coins,
|
||||
# or add it to unopenedPackIds. This is a compatibility shim for FIFA 17
|
||||
# client behavior, NOT an EA-authentic empty-My-Packs representation, and it
|
||||
# is FIFA17-specific (do not lift into game-independent Core). A fully clean
|
||||
# zero-pack UX requires a client-side fix -- see
|
||||
# docs/plans/FIFA17_EMPTY_MYPACKS_CLIENT_FIX.md and the evidence in
|
||||
# docs/evidence/STORE_TILE_6C.md / FIFA17_EMPTY_MYPACKS_CLIENT_CONTRACT.md.
|
||||
sentinel = {
|
||||
"id": 65534,
|
||||
"name": "",
|
||||
@@ -3441,7 +3677,7 @@ def store_catalog(h):
|
||||
"specialChance": 0.0,
|
||||
}
|
||||
empty = _pack_body(sentinel, 1, owned=True)
|
||||
empty["state"] = "inactive"
|
||||
empty["state"] = "active"
|
||||
empty["unopened"] = False
|
||||
packs.append(empty)
|
||||
return 200, {"purchase": packs, "timestamp": 1596326400}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user