fifa17-recon: club stats LIVE-PROVEN, default ON; S19's verdict was over-scoped

MY CLUB -> ENGLAND -> Premier League now reads 17. First non-zero number ever rendered
on that screen. Nothing froze, no other screen changed, 392 + 61 checks green with the
flag defaulted on and no env override.

S19 concluded "the MY CLUB counter is not server-fixable". That was too broadly scoped.
The nation and league drill-downs ARE server-fixable and are now fixed. S20 records the
corrected scope.

What made it work, from FUN_180043b90 case 3:

  uVar7  = (**(param_2 + 0x18))(param_2, row, "LEAGUE_ID")   THE UI ROW'S OWN ID
  bronze/silver/gold = (+0x7f8)(store, uVar7, 2 / 3 / 4)
  publish("PLAYERS_EMPLOYED", gold + silver + bronze)         COMPUTED, never read
  rare/kits/badges   = (+0x7f8)(store, uVar7, 5 / 0x28 / 0x2d)

Still open and now correctly scoped: the hub tile's "0 TOTAL PLAYERS" and the MY CLUB
summary rows read the GLOBAL bucket via +0x800 in cases 1 and 5. We serve those rows.
The unchanged question is what SELECTS those cases, since the mode tag is copied from
the completed request and the client requests year, consumables, staff, country/<id>
and league/<id> but never club.

The method note, which is the durable part: two rounds of reasoning about this endpoint
produced two wrong bodies; twenty lines of the consumer produced the right one. Reading
the PARSER tells you what is accepted. Only reading the CONSUMER tells you what is used.
That question was answerable from the start and went unasked until live screenshots
forced it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
This commit is contained in:
funman300
2026-08-04 14:50:06 -07:00
parent f65c197942
commit a7ac5a09fd
2 changed files with 70 additions and 1 deletions
+63
View File
@@ -893,3 +893,66 @@ An earlier note in this file argued the counter was server-supplied by
`/club/stats/<mode>`, and the live-evidence argument that the hub tile and the panel
read the same store (both said "players", both zero) was mine and was wrong in its
conclusion. The store is shared; the *selection* is not.
---
## 20. Club stats ARE server-fixable (2026-08-04, live-proven)
§19 concluded the MY CLUB counter was not server-fixable. That conclusion was **too
broadly scoped**. The nation and league drill-down screens are fixable, and are now
fixed and live-proven: the MY CLUB -> ENGLAND -> Premier League row reads **17**, the
first non-zero number ever rendered on that screen.
### What made it work
The per-context getter is `(+0x7f8)(store, contextValue, typeId)`, and the whole thing
turns on where `contextValue` comes from. From `FUN_180043b90` case 3:
```c
uVar7 = (**(param_2 + 0x18))(param_2, row, "LEAGUE_ID"); // THE UI ROW'S OWN ID
bronze = (+0x7f8)(store, uVar7, 2);
silver = (+0x7f8)(store, uVar7, 3);
gold = (+0x7f8)(store, uVar7, 4);
publish("PLAYERS_EMPLOYED", gold + silver + bronze); // COMPUTED, never read
rare = (+0x7f8)(store, uVar7, 5);
kits = (+0x7f8)(store, uVar7, 0x28);
badges = (+0x7f8)(store, uVar7, 0x2d);
```
Three rules follow, and two wrong bodies were shipped before they were understood:
1. **A response must carry a bucket per SCREEN ROW**, because the reader iterates the
rows and looks up each row's own id. Keying every row to the id in the URL fills one
bucket the screen never asks for. That was attempt two.
2. **`PLAYERS_EMPLOYED` is computed** as gold + silver + bronze in the per-context
cases. Sending `players` (type id 1) does nothing there. The tier counts are
mandatory.
3. **The screens nest**: `country/<id>` lists the LEAGUES in that nation (case 3, keyed
by `LEAGUE_ID`); `league/<id>` lists the TEAMS (case 4, keyed by `TEAM_ID`, reading
ids 1, 0x28, 0x2e).
The guard still matters: `contextId == 1` or `5 <= contextId <= 9` forces `contextValue`
to 0, the global bucket. Per-context rows use `contextId 3` purely to sit outside it.
`TODO/CONFIRM` whether contextId means anything more.
Because every response wipes the whole map, each response carries only its own screen's
buckets. That also dodges a real collision: the storage key is `contextValue` alone, so
nation 14 and league 14 would otherwise share a bucket.
`FUT_CLUBSTATS` is now **default ON**, the live-proven value.
### What is still open, and correctly scoped this time
The hub tile's `0 TOTAL PLAYERS` and the MY CLUB summary panel rows are **unchanged**.
They read the GLOBAL bucket through `+0x800` in cases 1 and 5, and we do serve those
rows, so the remaining question is unchanged from §19: what selects those cases. The
mode tag is copied from the completed request, and the client requests `year`,
`consumables`, `staff`, `country/<id>` and `league/<id>` but never `club`.
### The method note, which is the durable part
Two rounds of reasoning about this endpoint produced two wrong bodies. Twenty lines of
the consumer produced the right one. The question **"what does the reader actually look
up"** was answerable from the start and was not asked until live screenshots forced it.
Reading the parser tells you what is accepted; only reading the CONSUMER tells you what
is used.
+7 -1
View File
@@ -997,7 +997,13 @@ ROUTES = [
# The test is unusually clean because the club holds 114 items and ALL of them are
# players: every other row is an honest zero. So if this works, exactly two numbers
# move (Players and Rare Players, 0 -> 114) and nothing else changes.
CLUBSTATS = os.environ.get("FUT_CLUBSTATS") == "1"
# DEFAULT ON since 2026-08-04: LIVE-PROVEN. With this serving, the MY CLUB ->
# ENGLAND -> Premier League row went from 0 to 17, the first non-zero number ever
# rendered on that screen. Nothing froze and no other screen changed. The global
# bucket rows ride along and are harmless; they are what the MY CLUB summary and
# hub tile WOULD read if anything ever selected those cases (see REBUILD_RESEARCH
# S19, still open). FUT_CLUBSTATS=0 reverts to the old {}.
CLUBSTATS = os.environ.get("FUT_CLUBSTATS", "1") == "1"
def _counts_for(players):