docs+tools: dump the CardsDLL route table; narrow Q2 to one candidate by elimination

Re-entry discriminator came back a CONFIRMED BUG: an unlisted transfer-list item does
not survive a fresh FUT session, so our representation cannot reconstruct trade-pile
membership. Evidence acquisition per instruction, corpus and PE first, no guessing.

Adds route_table_dump.py: static read-only dump of CardsDLL's route table from the
on-disk PE, resolving VA->file offset through the real section table instead of
assuming a single .text mapping. Output preserved as evidence. It settles "is
/tradePile the only relevant route?" -- the table holds 45 routes plus 3 empty admin
slots, and row 30 `ut/%s/tradePile` is the ONLY trade-pile route. There is no
trade-pile items route.

That plus three existing PE facts narrows the representation to exactly one candidate
by ELIMINATION rather than choice: the route carries only twelve-atom auction records;
`pile` (0x226) has no arm in the item deserializer so membership is conferred by the
owning list and cannot be added as a field; of the twelve atoms only tradeState
expresses lifecycle; and tradeState's closed vocabulary (active=1 inactive=2
expired=3 closed=4) has exactly one value not already spoken for.

So an unlisted item can only be an auctionInfo record with tradeState "inactive".
Tagged INFERRED-BY-ELIMINATION, not CONFIRMED: the remaining unknown is whether the
Flash Transfer List RENDERS such a record in the unlisted section. Records the
acceptance test (survive a full FUT reload) and the revised invariant that a
transition is complete only when a fresh session reconstructs the same visible state.

No behaviour change in this commit.
This commit is contained in:
funman300
2026-08-17 20:10:26 +00:00
parent 7f37b37be3
commit 2e97ff1461
3 changed files with 207 additions and 0 deletions
+70
View File
@@ -396,3 +396,73 @@ state, and it is reachable in two ways, both now closed:
Tests: `returning_an_expired_listing_to_the_club_ends_its_auction`,
`a_pile_move_never_disturbs_a_sale_in_flight`,
`club_excludes_listed_items_and_paginates_the_visible_set`.
---
## Q2 representation — narrowed to ONE candidate by elimination (2026-08-17)
Re-entry discriminator result: **CONFIRMED BUG.** After fully leaving and re-entering
FUT, the active auction reconstructs correctly under LISTED ITEMS, but the unlisted
TRANSFER LIST section is **empty** — item `100000059` does not survive. The
immediate post-move visibility was client-local only, so our representation cannot
durably reconstruct trade-pile membership.
The representation is now pinned by elimination over PROVEN facts, not chosen:
1. **Only one route can own a trade-pile list.** The complete CardsDLL route table
(`.rdata 0x18021df80`, 45 routes + 3 empty admin slots) is dumped verbatim at
`docs/evidence/market-lifecycle-2026-08-17/cardsdll-route-table.txt` via
`fifa17-recon/tools/route_table_dump.py` (static, read-only, VA→offset resolved
through the real PE section table). Row 30 `ut/%s/tradePile` is the ONLY
trade-pile route. There is no trade-pile *items* route.
2. **That route carries only auction records.** `FutGetTradePile` (`0x180170810`)
deserializes the shared IS-list body (`0x18013e7f0`), whose `auctionInfo`
elements go through `0x18013e410` — the twelve-atom auction record.
3. **`pile` cannot be set from the wire.** `pile` (atom 0x226) has NO arm in the item
deserializer `FUN_18013fe00` (checked in all four dispatch forms); `item+0x60` is
assigned by the OWNING LIST, reading 1 for every `/club` item and 6 for every
`/purchased` item. So membership is conferred by *arriving in the list*, never by
a field we can add.
4. **Of the twelve atoms, only `tradeState` can express lifecycle.**
5. **`tradeState`'s vocabulary is closed and has exactly one unused value.** The table
walk at `0x180229e40` decodes `active=1 inactive=2 expired=3 closed=4`, anything
else `-1`. `active`, `expired` and `closed` are all already spoken for by the
observed lifecycle.
**Therefore an unlisted trade-pile item can only be an `auctionInfo` record with
`tradeState: "inactive"`.** That is the sole encoding the client's own parser can
accept for "in the pile, no auction" — reached by elimination, not invention.
Status: **INFERRED-BY-ELIMINATION**, not yet CONFIRMED. One binary question remains,
and it is about client *rendering*, not encoding: does the Flash Transfer List place
an `inactive` record in the unlisted section? The corpus's warning that "`inactive`
decodes but no client path treats it specially" was established for the market
PRESENTATION function (`flagA`/`flagB`), where `inactive` is indeed unremarkable —
that is consistent with, and does not contradict, using it for list membership.
Ruled out on evidence, do not retry: adding `itemData.pile` (inert, no deserializer
arm), a second route (none exists), and `tradeId: 0` / invented `itemState` values
(unnecessary — `tradeState` alone carries the distinction).
### Acceptance test for whatever lands
```text
move item Club -> Transfer List
leave FUT entirely
re-enter FUT
item appears in the unlisted TRANSFER LIST section
item can be returned to Club
no auction exists unless explicitly listed
```
Plus the active lifecycle must remain unchanged: active listing under LISTED ITEMS,
`expires` counting down, active non-selectable, expiry making it actionable, relist
working.
### Revised domain invariant
Core pile membership is NOT equivalent to `/tradePile` visibility. Core ownership/pile
state and auction lifecycle are separate authoritative facts; the wire may expose them
through different FIFA 17 resources. **A transition is complete only when a fresh FIFA
session can reconstruct the same user-visible state** — re-entry is the acceptance
test, not the immediate post-mutation response.
@@ -0,0 +1,61 @@
image_base=0x180000000 sections=7
.text rva=0x00001000 vsize=0x1e3f62 rawptr=0x00000400 rawsize=0x1e4000
.rdata rva=0x001e5000 vsize=0xa4094 rawptr=0x001e4400 rawsize=0xa4200
.data rva=0x0028a000 vsize=0x65c08 rawptr=0x00288600 rawsize=0x54000
.pdata rva=0x002f0000 vsize=0x132cc rawptr=0x002dc600 rawsize=0x13400
.tls rva=0x00304000 vsize=0x9 rawptr=0x002efa00 rawsize=0x200
.rsrc rva=0x00305000 vsize=0x4e8 rawptr=0x002efc00 rawsize=0x600
.reloc rva=0x00306000 vsize=0x16bc4 rawptr=0x002f0200 rawsize=0x16c00
route table VA 0x18021df80 -> file offset 0x21d380
# field A field B
0 'ut/%s/auctionhouse' 'AUCTIONHOUSE'
1 'ut/%s/clubUser' 'CLUB_USER'
2 'ut/%s/user/list' 'CLUB_INFO'
3 'ut/%s/club' 'CLUB'
4 'ut/%s/defid' 'DREAM'
5 'ut/%s/squad' 'SQUAD'
6 'ut/delete/%s/squad' 'DELETE_SQUAD'
7 'ut/%s/leaderboards/options' 'LBOPTIONS'
8 'ut/%s/leaderboards' 'LBDEFAULT'
9 'ut/%s/activeMessage' 'PAFPRACTICE'
10 'ut/%s' 'UT'
11 'ut/%s/user' 'USER'
12 'ut/delete/%s/user' 'DELETEUSER'
13 'ut/%s/item' 'ITEMS'
14 'ut/%s/item/resource' 'ITEMS_BY_RES'
15 'ut/delete/%s/item' 'DELETEITEMS'
16 'ut/%s/match' 'MATCH'
17 'ut/%s/sbs' 'SBC'
18 'ut/%s/tournament' 'TOURNAMENT'
19 'ut/%s/tournament/user' 'TOURNAMENTUSER'
20 'ut/delete/%s/tournament/user' 'TOURNAMENTQUIT'
21 'ut/%s/season' 'SEASON'
22 'ut/%s/season/user' 'SEASONUSER'
23 'ut/%s/season/%%s/user' 'SEASONUSER_ALTER'
24 'ut/%s/season/%%s/reset' 'SEASONRESET'
25 'ut/%s/season/friendly' 'FRIENDLYSEASON'
26 'ut/%s/purchased' 'PURCHASED'
27 'ut/%s/store' 'STORE'
28 'ut/%s/watchList' 'WATCHLIST'
29 'ut/delete/%s/watchList' 'DELETEWATCHLIST'
30 'ut/%s/tradePile' 'TRADEPILE'
31 'ut/%s/trade' 'TRADE'
32 'ut/delete/%s/trade' 'DELETETRADE'
33 'ut/%s/marketdata' 'MARKETDATA'
34 'ut/%s/clientdata' 'CLIENTDATA'
35 'ut/auth' 'AUTH'
36 'ut/delete/auth' 'DELETE_AUTH'
37 'ut/%s/phishing' 'PHISHING'
38 'ut/%s/captcha' 'CAPTCHA'
39 'ut/%s/tfa' 'TFA'
40 'ut/%s/squad/mode' 'SQUADMODE'
41 'ut/%s/draft/mode' 'DRAFT'
42 'ut/%s/champion' 'CHAMPIONS'
43 'ut/v2/%s/store' 'V2STORE'
44 'ut/%s/livemessage' 'LIVEMESSAGE'
45 '' 'ADMIN'
46 '' 'DEBUG'
47 '' 'MAINTENANCE'
-- table ends after 48 rows --