test(host): pin the ?type= vocabulary to the client's own 30 arms
Decoded the vocabulary from the binary rather than trusting a case count: FUN_18012ec50 is `cmp ecx,0x1d` plus a 30-entry jump table at 0x18012ed9c, each case `mov ecx,<atom>; jmp <atom->string>`. Resolving those atoms against fut_atoms.tsv yields the exact token list, and it matches club_type_filter one-for-one — 30 implemented, none missing, none invented. That is worth a test rather than a note. A MISSING arm answers a real tab with unsupported_type and an empty screen; an INVENTED arm is worse, because it is dead code that looks like coverage. Mutation-checked: renaming the leaguelogos arm fails the test. Two facts fall out that were previously guesswork. There is no `playergoalkeeper` token — the client has only DEF/MID/FWD tabs — so a goalkeeper appearing under playerdefender is CORRECT and not a filter bug, which I had flagged as suspicious while sweeping. And `healing`/`contract`/`training` exist as ?type= arms even though consumables have their own route. Also completes the last unapplied item of plan section 7: the full vocabulary is now written into ENDPOINT_MAP.md with how it was derived.
This commit is contained in:
@@ -979,7 +979,32 @@ bronze/silver/gold/any; `sort` = asc/desc; `rare` = the literal string `SP`, not
|
|||||||
a boolean; `state` = the itemState names plus `any` — and note the REQUEST spells
|
a boolean; `state` = the itemState names plus `any` — and note the REQUEST spells
|
||||||
it `onSale` where the RESPONSE value is `forSale`.
|
it `onSale` where the RESPONSE value is `forSale`.
|
||||||
|
|
||||||
`?type=` has 30 values (`FUN_18012ec50`: 29 cases plus a default of `any`).
|
`?type=` has 30 values. Decoded 2026-08-21 from the jump table itself rather
|
||||||
|
than from a case count: `FUN_18012ec50` is `cmp ecx,0x1d` + a 30-entry table at
|
||||||
|
`0x18012ed9c`, and each case is `mov ecx,<atom>; jmp 0x180180cd0` (atom → string).
|
||||||
|
Resolving those atoms against `fut_atoms.tsv` gives the vocabulary in table order:
|
||||||
|
|
||||||
|
```
|
||||||
|
0 any 1 player 2 manager 3 headcoach
|
||||||
|
4 fitnesscoach 5 physio 6 development 7 custom
|
||||||
|
8 unlocks 9 gkcoach 10 staff 11 badge
|
||||||
|
12 kit 13 stadium 14 ball 15 equippables
|
||||||
|
16 leaguelogos 17 offlinetrophy 18 onlinetrophy 19 featuredofflinetrophy
|
||||||
|
20 featuredonlinetrophy 21 allofflinetrophy
|
||||||
|
22 allonlinetrophy 23 healing 24 contract
|
||||||
|
25 training 26 misc 27 playerdefender
|
||||||
|
28 playermidfielder 29 playerforward
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes worth having: there is **no `playergoalkeeper`** — the client has only
|
||||||
|
DEF/MID/FWD tabs, so goalkeepers belong to `playerdefender`, and a GK appearing
|
||||||
|
there is correct rather than a filter bug. `healing`, `contract` and `training`
|
||||||
|
exist here as `?type=` arms even though consumables have their own
|
||||||
|
`club/consumables/<cat>` route. Six of the thirty are trophy arms.
|
||||||
|
|
||||||
|
`openfut-utas-host`'s `club_type_filter` implements all 30 with no extras; a unit
|
||||||
|
test pins the list so a missing arm (an empty real tab) or an invented one (dead
|
||||||
|
code that looks like coverage) fails the build.
|
||||||
|
|
||||||
**`/club/stats` has exactly seven forms**: `club`, `year`, `country/<id>`,
|
**`/club/stats` has exactly seven forms**: `club`, `year`, `country/<id>`,
|
||||||
`league/<id>`, `newcards`, `consumables`, and the separately-dispatched `staff`.
|
`league/<id>`, `newcards`, `consumables`, and the separately-dispatched `staff`.
|
||||||
|
|||||||
@@ -6580,4 +6580,68 @@ mod tests {
|
|||||||
Some(1)
|
Some(1)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `?type=` vocabulary must match the CLIENT's exactly — no arm missing,
|
||||||
|
/// and no arm invented.
|
||||||
|
///
|
||||||
|
/// The client resolves the token in `FUN_18012ec50`, a 30-case jump table
|
||||||
|
/// (`cmp ecx,0x1d`) where each case is `mov ecx,<atom>; jmp <atom->string>`.
|
||||||
|
/// Decoding that table against `docs/fut_atoms.tsv` on 2026-08-21 produced
|
||||||
|
/// exactly the list below, and it matched this function one-for-one.
|
||||||
|
///
|
||||||
|
/// A MISSING arm answers a real tab with `unsupported_type` and an empty
|
||||||
|
/// screen. An INVENTED arm is worse: it is a token the client cannot send,
|
||||||
|
/// so it is dead code that looks like coverage.
|
||||||
|
#[test]
|
||||||
|
fn club_type_vocabulary_matches_the_clients_thirty_arms() {
|
||||||
|
// FUN_18012ec50 cases 0..=29, in table order.
|
||||||
|
const CLIENT_TOKENS: [&str; 30] = [
|
||||||
|
"any",
|
||||||
|
"player",
|
||||||
|
"manager",
|
||||||
|
"headcoach",
|
||||||
|
"fitnesscoach",
|
||||||
|
"physio",
|
||||||
|
"development",
|
||||||
|
"custom",
|
||||||
|
"unlocks",
|
||||||
|
"gkcoach",
|
||||||
|
"staff",
|
||||||
|
"badge",
|
||||||
|
"kit",
|
||||||
|
"stadium",
|
||||||
|
"ball",
|
||||||
|
"equippables",
|
||||||
|
"leaguelogos",
|
||||||
|
"offlinetrophy",
|
||||||
|
"onlinetrophy",
|
||||||
|
"featuredofflinetrophy",
|
||||||
|
"featuredonlinetrophy",
|
||||||
|
"allofflinetrophy",
|
||||||
|
"allonlinetrophy",
|
||||||
|
"healing",
|
||||||
|
"contract",
|
||||||
|
"training",
|
||||||
|
"misc",
|
||||||
|
"playerdefender",
|
||||||
|
"playermidfielder",
|
||||||
|
"playerforward",
|
||||||
|
];
|
||||||
|
for token in CLIENT_TOKENS {
|
||||||
|
assert!(
|
||||||
|
club_type_filter(Some(token)).is_some(),
|
||||||
|
"the client can send type={token} and this host has no arm for it"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// A token outside the taxonomy stays unsupported — the honest, loud answer.
|
||||||
|
for bogus in ["playergoalkeeper", "trophies", "consumable", ""] {
|
||||||
|
assert!(
|
||||||
|
club_type_filter(Some(bogus)).is_none(),
|
||||||
|
"type={bogus} is not one of the client's 30 arms and must not be \
|
||||||
|
silently mapped onto a real set"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Absent = the main club screen, which is live-proven to be the players.
|
||||||
|
assert!(club_type_filter(None).is_some());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user