From 274838cc2e5e4974c1f9ff9a0e5060c613bae45e Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 21 Aug 2026 22:02:53 +0000 Subject: [PATCH] test(host): pin the ?type= vocabulary to the client's own 30 arms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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,; jmp 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. --- fifa17-recon/docs/ENDPOINT_MAP.md | 27 ++++++++++++- openfut-utas-host/src/lib.rs | 64 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/fifa17-recon/docs/ENDPOINT_MAP.md b/fifa17-recon/docs/ENDPOINT_MAP.md index 13bdfe2..42e822c 100644 --- a/fifa17-recon/docs/ENDPOINT_MAP.md +++ b/fifa17-recon/docs/ENDPOINT_MAP.md @@ -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 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,; 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/` 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/`, `league/`, `newcards`, `consumables`, and the separately-dispatched `staff`. diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index 6ad85d9..f8c889e 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -6580,4 +6580,68 @@ mod tests { 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,; jmp 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()); + } }