//! The FIFA 17 **`itemState` vocabulary** — the complete recovered set, and the //! only place these strings are written down. //! //! Twelve entries in one NUL-terminated `{const char* name, u32 value}` table at //! `0x180229cc0` (stride 0x10), walked in full from both disk and live memory. //! `FUN_180166660` is a linear walk over that table and returns `0xffffffff` for //! anything not in it, so an invented token is not a cosmetic slip: it decodes to //! "unrecognised state" and the client acts on garbage. Every shaper in this //! crate therefore takes its `itemState` from a constant here, and //! [`is_recovered`] is asserted over every emitted value by the tests. //! //! **Omitting `itemState` is NOT the same as sending [`FREE`].** The record //! constructor zero-initialises `+0x50..+0x5f` from `_DAT_1801f66a0`, so an //! absent key leaves `0` = [`INVALID`], and an item left at `0` fails the squad //! builder's `state == 1 || state == 2` acceptance test. Always send it. //! //! **The casing is a CONTRACT, not a convention** — measured, not assumed. The //! table lookup compares through a slot the host fills at runtime //! (`FUN_180008190` is just `mov rax,[DAT_1802ddfd8]; mov r9,[rax+0x248]; jmp r9`), //! so this was long recorded as unresolvable without a live process. Resolved //! read-only against the running client on 2026-08-21 //! (`fifa17-recon/tools/service_ptr_probe.py`): the slot forwards through two //! FIFA17.exe thunks into `msvcr120.dll+0x3c330`, whose body is `strncmp` — a //! plain byte compare (`cmp al,[rcx+rdx]`) with NO case folding anywhere. So a //! mis-cased token does not "mostly work": it matches nothing, decodes to //! [`INVALID`], and the item fails the squad builder. Emit these strings //! verbatim. //! //! (Source: `fifa17-recon/docs/plan-2026-08-06-card-subsystem.md` §4, which also //! corrects `CARD_SYSTEM.md`'s earlier ten-row reading — that one started at //! `0x180229d20`, the MIDDLE of the table, and so missed `invalid`, `free`, //! `WAITING_FOR_GAME`, `inGame`, `forSale` and `offered`.) /// `0` — what an item gets when `itemState` is OMITTED. No consumer found; it /// fails the squad builder. Never emit it deliberately. pub const INVALID: &str = "invalid"; /// `1` — the normal owned state: accepted by the squad builder, and what the /// unequip path writes back. pub const FREE: &str = "free"; /// `2` — alias of [`IN_GAME`] (both decode to 2). pub const WAITING_FOR_GAME: &str = "WAITING_FOR_GAME"; /// `2` — accepted by the squad builder. pub const IN_GAME: &str = "inGame"; /// `5` — an item offered for sale. Never TESTED anywhere in CardsDLL, but it is /// in the table, so it decodes; the transfer market emits it. pub const FOR_SALE: &str = "forSale"; /// `6` — never tested anywhere in CardsDLL. pub const OFFERED: &str = "offered"; /// `100` — equipped badge; drives the `IS_ACTIVE` tick. pub const ACTIVE_BADGE: &str = "activeBadge"; /// `101` — equipped home kit. pub const ACTIVE_HOME_KIT: &str = "activeHomeKit"; /// `102` — equipped away kit. pub const ACTIVE_AWAY_KIT: &str = "activeAwayKit"; /// `103` — equipped ball; the unequip path writes [`FREE`] back over it. pub const ACTIVE_BALL: &str = "activeBall"; /// `104` — equipped stadium. pub const ACTIVE_STADIUM: &str = "activeStadium"; /// `255` — no consumer found. pub const ACTIVE: &str = "active"; /// The complete recovered vocabulary, in table order. pub const ALL: [&str; 12] = [ INVALID, FREE, WAITING_FOR_GAME, IN_GAME, FOR_SALE, OFFERED, ACTIVE_BADGE, ACTIVE_HOME_KIT, ACTIVE_AWAY_KIT, ACTIVE_BALL, ACTIVE_STADIUM, ACTIVE, ]; /// Whether `state` is one of the twelve recovered tokens. Case-sensitive, as the /// client's own lookup is a `strcmp` walk. pub fn is_recovered(state: &str) -> bool { ALL.contains(&state) } #[cfg(test)] mod tests { use super::*; #[test] fn the_table_is_the_twelve_recovered_rows_and_nothing_else() { assert_eq!(ALL.len(), 12); for s in ALL { assert!(is_recovered(s), "{s} must be in its own table"); } // Tokens this project has actually seen invented or mis-cased. `listFS` // in particular is the Python oracle's own token and appears NOWHERE in // the client (zero occurrences in the DLL and in 4.26 GiB of live // process memory), so it decodes to -1. for s in [ "listFS", "free ", "Free", "activehomekit", "sold", "won", "equipped", "", ] { assert!(!is_recovered(s), "{s:?} is not a FIFA 17 itemState"); } } }