fix(market): pin auctionInfo to FIFA 17's twelve atoms, add the real auction clock
Corrects the record against the CLIENT BINARY rather than library hearsay, using
the project's own reverse-engineering record
(fifa17-recon/docs/plan-2026-08-06-transfer-market.md, read out of the on-disk PE).
REVERTED (refuted): `tradeOwner`, `sellerId`, `offers`. FIFA 17's auctionInfo
deserializer (0x18013e410) reads exactly TWELVE atoms -- bidState, buyNowPrice,
currentBid, expires, itemData, sellerEstablished, sellerName, startingBid,
coinsProcessed, tradeId, tradeState, watched -- and value-SKIPs everything else at
0x180135ff0. Those three fields were added last commit on the strength of
contemporaneous FIFA 17 libraries; the PE says the client never reads them, so they
were inert and could not have been the Actions-panel gate. A preservation emulator
must not emit fields the client does not consume. New test pins the exact set.
ADDED: the auction clock. `expires` is SECONDS REMAINING (never an epoch) and the
client renders a LIVE COUNTDOWN it expects to reach 0. We hardcoded 3600, so no
auction ever aged or ran out. Now `duration` is taken from the ISStart body
(additive `duration_secs` column, defaulting to 3600) and `expires` is derived from
created_at + duration - now, clamped at 0. An active listing whose clock has run
out projects as `expired`/`none`/`expires: 0` -- FIFA 17's relistable state, per the
lifecycle table (active=1 inactive=2 expired=3 closed=4; none=0 outbid=1 highest=2
buyNow=3, both closed vocabularies). Pure projection: no row is mutated, so no
sweeper and no race with the economy.
ADDED: `duplicateItemIdList: []` on GetTradePile, which shares one deserializer
(0x18013e7f0) with ISSearch/ISWatchList over four members and we were omitting one.
CONFIRMED by the same source, so kept: `GET ut/{ns}/trade/status?tradeIds=a,b,c` is
real (ISVIEWTRADE) and my handler matches it exactly, including the comma list.
`ISREMOVETRADE` is `DELETE ut/delete/{ns}/trade/{tradeId}` -- our ORIGINAL spelling
was right. The plain-DELETE arm stays because the same source advises dispatching
on path and being method-agnostic (HTTP verbs are not statically recoverable).
Differential returns to strict key-set parity, with a comment recording WHY parity
is not sufficient: a field absent from both sides is invisible to it.
333 tests pass, 0 failed, clippy clean. Verified live: the twelve-atom record, the
four-member envelope, and the listing correctly reading expires=0 / expired after
aging past its hour.
This commit is contained in:
@@ -910,34 +910,21 @@ fn run_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
|
||||
k.sort();
|
||||
k
|
||||
};
|
||||
// Our record is a deliberate SUPERSET of the oracle's. The oracle omits the
|
||||
// FIFA 17 ownership fields entirely, which is exactly why parity could not
|
||||
// catch the Actions-panel bug: the field was missing on BOTH sides, because the
|
||||
// oracle's own remove flow was never driven by a real client either. So assert
|
||||
// (a) we cover every key the oracle emits, and (b) the extra keys are precisely
|
||||
// the ownership set we added on purpose — a NEW unexplained divergence still
|
||||
// fails here.
|
||||
let (ok, rk) = (keys(o_rec), keys(r_rec));
|
||||
for k in &ok {
|
||||
assert!(rk.contains(k), "rust tradePile record is missing oracle key `{k}`");
|
||||
}
|
||||
let extra: Vec<&String> = rk.iter().filter(|k| !ok.contains(k)).collect();
|
||||
// Both sides emit exactly FIFA 17's twelve auctionInfo atoms, so this is a
|
||||
// strict key-set equality. NOTE the limit of that: parity here proves we match
|
||||
// the oracle, NOT that either side is complete -- a field absent from BOTH is
|
||||
// invisible to this check. That is exactly how the Transfer List Actions-panel
|
||||
// bug hid, and the client binary's atom table is the authority that settled it
|
||||
// (see docs/FIFA17_TRANSFER_MARKET_WIRE.md).
|
||||
assert_eq!(
|
||||
extra,
|
||||
vec!["offers", "sellerId", "tradeOwner"],
|
||||
"the ONLY keys we add beyond the oracle are the FIFA 17 ownership fields"
|
||||
);
|
||||
// The ownership story must be internally consistent on our side.
|
||||
assert_eq!(r_rec["tradeOwner"], true, "own pile listing is owned by us");
|
||||
assert_eq!(
|
||||
r_rec["sellerId"], PERSONA_ID,
|
||||
"sellerId agrees with tradeOwner"
|
||||
keys(o_rec),
|
||||
keys(r_rec),
|
||||
"tradePile auction-record key set parity"
|
||||
);
|
||||
for f in [
|
||||
"sellerName",
|
||||
"bidState",
|
||||
"currentBid",
|
||||
"expires",
|
||||
"sellerEstablished",
|
||||
"watched",
|
||||
"coinsProcessed",
|
||||
@@ -948,6 +935,12 @@ fn run_differential(core_base: &str, oracle: &Oracle, dir: &std::path::Path) {
|
||||
r_rec["sellerName"], PERSONA_DISPLAY_NAME,
|
||||
"the player's own listing is sold BY the player, never by EA"
|
||||
);
|
||||
// `expires` is seconds remaining on a live clock, so it need not equal the
|
||||
// oracle's constant; it must be a positive 64-bit count for an active auction.
|
||||
assert!(
|
||||
r_rec["expires"].as_i64().is_some_and(|e| e > 0),
|
||||
"an active auction has positive seconds remaining"
|
||||
);
|
||||
// itemData must be the full shaped card on both sides; a stub cannot render.
|
||||
assert_eq!(
|
||||
o_rec["itemData"]["itemState"], r_rec["itemData"]["itemState"],
|
||||
|
||||
Reference in New Issue
Block a user