fifa17-recon: the /settings gate bytes were never zero, and the plan built on that is dead
Yesterday's settings-gate plan asserted that IS_FRIENDLY_SEASON_ENABLED and
IS_DRAFT_MODE_ENABLED "have never been set to true by anything, on any run", and
proposed spending a launch on that premise. Measured against the running client,
both read 1, and so does packOpeningAnimationEnabled, while /settings has only ever
been answered {"configs": []}.
disp 0x1fd3a (friendlySeasonsEnabled) value = 1
disp 0x1fd3d (enableDraftMode) value = 1
disp 0x1fd45 (packOpeningAnimationEnabled) value = 1
Reproduced on two separate launches and two different pids.
Where the reasoning went wrong: the finding that FUN_18011dc50 is the only writer of
those bytes, and that the FutDataManagerImpl constructor never touches them, was
correct. The inference was not. The applier runs whether or not the configs array has
content, and the struct it is handed defaults these fields to 1, so the bytes were
being written all along. "Nothing populates the array" was treated as "nothing writes
the byte". Only the first of those was ever established.
Seasons therefore does not refuse because its gate byte is false. Its gate byte is
true. That diagnosis restarts, and the live test in section 3 should not be run as
written. The doc keeps the wrong turn on the record rather than quietly deleting it.
tools/gate_byte_probe.py makes this repeatable instead of a one-off. It is read-only
(O_RDONLY + pread), resolves the pid by comm, re-derives the CardsDLL slide from
/proc/<pid>/maps rather than caching it across launches, proves the slide against the
FNV prologue at 0x180180d00 read from the on-disk PE before trusting any address, and
decodes each gate displacement out of its accessor stub (0f b6 81 <disp32>) rather
than reading it from a table. Needs the client at the FUT hub, since CardsDLL loads
only then.
Also carries the two /settings changes that were pending from before: the mode
defaults to `off` (the live-proven baseline, since nothing here has faced the game)
and the transfer-pile probe is 77 rather than 100, because 100 is a stock-looking
number that would prove nothing if it showed up in game.
Live: 439 contract checks pass. check_settings_flags.py passes in all four modes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# The /settings feature gate - live-test script
|
||||
|
||||
> **CORRECTION, 2026-08-05 evening. Section 1 of this document is FALSE and the
|
||||
> test in section 3 should not be run as written.**
|
||||
>
|
||||
> Section 1 claims `IS_FRIENDLY_SEASON_ENABLED` and `IS_DRAFT_MODE_ENABLED` "have
|
||||
> never been set to true by anything, on any run". They are measured as **1**, on
|
||||
> two separate launches, while `/settings` was answering `{"configs": []}`:
|
||||
>
|
||||
> ```
|
||||
> disp 0x1fd3a (friendlySeasonsEnabled) value = 1
|
||||
> disp 0x1fd3d (enableDraftMode) value = 1
|
||||
> disp 0x1fd45 (packOpeningAnimationEnabled) value = 1
|
||||
> ```
|
||||
>
|
||||
> Reproduce with `tools/gate_byte_probe.py` (needs the client at the FUT hub, since
|
||||
> CardsDLL loads only then): it resolves the pid by comm,
|
||||
> re-derives the CardsDLL slide from `/proc/<pid>/maps`, proves it against the FNV
|
||||
> prologue at `0x180180d00` read from disk, walks the model singleton at
|
||||
> `DAT_1802e6398`, and decodes each displacement out of its accessor stub
|
||||
> (`0f b6 81 <disp32>`) rather than assuming it.
|
||||
>
|
||||
> **Where the reasoning went wrong.** The finding that `FUN_18011dc50` is the only
|
||||
> writer and that the `FutDataManagerImpl` constructor never touches those bytes was
|
||||
> correct. The inference drawn from it was not. The applier runs whether or not the
|
||||
> configs array has content, and the settings struct it is handed defaults these
|
||||
> fields to 1, so the bytes were being written all along. "Nothing populates the
|
||||
> array" was treated as "nothing writes the byte". Those are different claims and
|
||||
> only the first one was established.
|
||||
>
|
||||
> Seasons therefore does not refuse because its gate byte is false. Its gate byte is
|
||||
> true. The mechanism is still unknown and needs a fresh diagnosis. Everything below
|
||||
> the correction is kept as the record of a wrong turn, not as a plan.
|
||||
|
||||
Written 2026-08-05, after reversing `FutGetSettingsServerResponse` end to end.
|
||||
Nothing here has been in front of the game yet. The code default is `off`, which
|
||||
serves the exact historical `{"configs": []}`, so the tree is currently at the
|
||||
proven baseline and this test is opt-in.
|
||||
|
||||
Full schema, atom ids, gate bytes and accessor addresses are in `ENDPOINT_MAP.md`
|
||||
under `FutGetSettingsServerResponse`. This file is only the experiment.
|
||||
|
||||
---
|
||||
|
||||
## 1. The claim being tested
|
||||
|
||||
`GET /settings` is requested 11 times a session and has always been answered with
|
||||
an empty array. The array is not decoration:
|
||||
|
||||
- Each element is `{"type": "<name>", "value": <scalar>}`. The client hashes the
|
||||
**string value** of `type` through the atom hasher and switches on it, 42 arms
|
||||
wide, so a flag is a row rather than a key.
|
||||
- `FUN_18011dc50` is the **only** writer of the `IS_*` UI gate bytes inside
|
||||
`FutDataManagerImpl`, and every line of it is `byte = (field == 1)`.
|
||||
- The `FutDataManagerImpl` constructor never touches those bytes. The whole
|
||||
16620-char decompile was scanned for the block; it is absent.
|
||||
|
||||
So `IS_FRIENDLY_SEASON_ENABLED` and `IS_DRAFT_MODE_ENABLED` have never been set
|
||||
to true by anything, on any run, in the whole history of this project.
|
||||
|
||||
That is a mechanism for the standing bug in which **Seasons refuses while making
|
||||
zero requests to any of the four servers.** No response shape could ever explain
|
||||
that. A UI key evaluated from a byte nobody wrote does.
|
||||
|
||||
**The store is the control that makes this readable.** `IS_STORE_ENABLED` is the
|
||||
same kind of byte read the same way, and the store screen works. It works because
|
||||
`storeEnabled` and its siblings already reach the client through the **Blaze**
|
||||
client-config store (`FUT_RS4_CONFIG`). That list contains no seasons flag, no
|
||||
draft flag, no tournament flag. Same mechanism, one populated, one blank.
|
||||
|
||||
This is a hypothesis with a mechanism, not a demonstrated cause.
|
||||
|
||||
---
|
||||
|
||||
## 2. Pre-flight, from the terminal, costs nothing
|
||||
|
||||
```bash
|
||||
cd fifa17-recon/tools
|
||||
FUT_SETTINGS=gates python3 check_settings_flags.py # expect: 14 rows, PASS
|
||||
python3 check_settings_flags.py # expect: mode=off, PASS
|
||||
```
|
||||
|
||||
The checker asserts every shipped flag name against **both** the atom table and
|
||||
the recovered switch arms. Both are needed: `enableSquadBuildingSetsFeature` is a
|
||||
genuine atom with no arm in this switch, so the atom table alone would wave
|
||||
through a flag that does nothing. A misnamed flag is silently inert and looks
|
||||
exactly like a failed fix, which is the failure mode this guards.
|
||||
|
||||
---
|
||||
|
||||
## 3. The run
|
||||
|
||||
Budget: **one launch.**
|
||||
|
||||
```bash
|
||||
cd fifa17-recon/tools
|
||||
FUT_SETTINGS=gates ./openfut-fut.sh start
|
||||
~/Desktop/launch-fifa17.sh
|
||||
```
|
||||
|
||||
Then, in order, and write down what each one does:
|
||||
|
||||
1. **Store.** Open it. This is the control and it goes first, because if
|
||||
populating the array broke the store then the applier demonstrably ran and
|
||||
everything after this reads differently.
|
||||
2. **Transfer list capacity.** Transfers → Transfer List. Read the capacity
|
||||
number. We send `maximumTradePileSize = 77`, a number FUT would never choose
|
||||
on its own.
|
||||
3. **Seasons.** Single-player Seasons, the exact path that has been refusing.
|
||||
4. **FUT Draft.** Both the offline and online entries.
|
||||
5. **Tournaments**, for `tournamentQuitEnabled`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Reading the result
|
||||
|
||||
The control in step 2 is what makes a negative result informative, so read it
|
||||
before concluding anything about steps 3 to 5.
|
||||
|
||||
| Store (1) | Capacity (2) | Seasons (3) | Reading |
|
||||
|---|---|---|---|
|
||||
| works | **77** | opens | Confirmed. The gate was the empty array. Make `gates` the default and move to the `/match` shape, which has been blocked behind this. |
|
||||
| works | **77** | still refuses | The array reached the consumer and the flag was applied, so the gate is **upstream of the UI key**. The settings line is then dead for Seasons and the next move is a live probe of the refusal path, not more response work. This is a real result, not a null one. |
|
||||
| works | not 77 | still refuses | The array never reached the consumer at all. Everything above is untested rather than refuted. Suspect the massinfo `settings` member (the deser's other caller) is what the client actually reads, and check which of the two paths fires in `/tmp/utas.log`. |
|
||||
| **breaks** | any | any | The applier ran and re-asserting the store flags did not hold them. Fall back to `FUT_SETTINGS=keep`, which sends only the already-working flags plus the control. If `keep` also breaks the store, populating the array is harmful in itself and the whole approach is wrong. |
|
||||
|
||||
`keep` exists precisely so that "populating the array at all" and "the new gates"
|
||||
can be separated without guessing, and it costs one restart to use.
|
||||
|
||||
---
|
||||
|
||||
## 5. What would make this whole plan wrong
|
||||
|
||||
**The gate might not be a UI key at all.** Seasons could be refusing on an
|
||||
entitlement, a persona attribute, or a Blaze session property evaluated inside
|
||||
the Denuvo-packed executable, in which case no `/settings` body reaches it. The
|
||||
step-2 control is what tells these apart: it distinguishes "the flag did not
|
||||
help" from "the array was never consumed", and no boolean flag can do that alone.
|
||||
|
||||
**The store control could be weaker than it looks.** The argument assumes
|
||||
`IS_STORE_ENABLED` currently comes from the Blaze store rather than from a
|
||||
default. If it turns out the store screen does not read that key at all, then it
|
||||
is not a control for anything and the reasoning in §1 loses its anchor.
|
||||
|
||||
**Draft has a second known suspect.** `GET ut/%s/squad/mode/draft/state` is still
|
||||
answered by the generic `/squad` handler with a full active-squad object, which
|
||||
is a textbook type-desync candidate. If Draft still fails while Seasons opens,
|
||||
that route is the next thing to look at, not the flag.
|
||||
|
||||
**A negative result here is worth having.** The settings array has been the
|
||||
standing suspect for the greyed-out entry points for two rounds without anyone
|
||||
sending a single flag. Ruling it out costs one launch and removes it from the
|
||||
backlog permanently.
|
||||
Reference in New Issue
Block a user