market: stop advertising unlisted pile members as tradeState:"inactive"
RE of the FUT front-end closed the question the Actions-panel investigation left open, and the answer retracts Q2 rather than completing it. `tradeState` reaches exactly ONE native branch in CardsDLL — `cmp …,0x4` at `0x18013e619`, "is it closed?" — and `inactive`(2) and `expired`(3) take the same edge, producing bit-identical `flagA`/`flagB` (exhaustive 22-site census of `[reg+0x88]` reads across the PE; confirmed live, both classes read glow=0 inbox=0). The value is then handed to the movie verbatim as the Flash property `STATE`, and the action gate lives in the APT/ActionScript FUT front-end: the trade-pile class partitions rows with `getCardsInAuction`/`isInActiveAuction` (traces `initPile() - IN AUCTION:` / `- NOT IN AUCTION:`) and only auction rows reach `PreCheckCardOptions` -> `handleTradeCardAction`. A non-auction row renders and can never be acted on, which is exactly what the operator saw. So the rows were never usable. "LIVE-CONFIRMED" established that they RENDER, which is not the same claim, and I treated it as if it were. The corpus said this before any of it was built — `plan-2026-08-06-transfer-market.md:731-733`: "`inactive` decodes but no client path treats it specially; do not emit it." The earlier note explaining that the warning "was written about the PRESENTATION function" was motivated reasoning. This also fires the corpus's own pre-registered falsifier E3 (:368-373). Removed: the `inactive` projection from `GET …/tradePile` and `…/trade/status`, `UnlistedCandidate`, `resolve_unlisted_pile`, `unlisted_record`, `Server::resolve_trade_pile`, and the two helpers that existed only to feed them (`MarketStore::blocking_core_items`, `Fifa17IdentityResolver::wire_for_owned_id`). Unlisted trade-pile membership is now internal state with no wire expression. Nothing is stranded: `/club` excludes only items with an ACTIVE listing, so an unlisted pile member stays visible in the club, which is where the client can act on it. Verified live after deploy — `/tradePile` total 7 -> 1 with zero `inactive` rows, `/trade/status` resolving only the real auction, coins unchanged at 29,843,976, and all six former rows present in `/club` (1965 items). Tests: 126 pass, fmt + clippy clean. Two guards replace the three tests that pinned the old behaviour: `the_trade_pile_advertises_only_real_auctions` and `trade_status_answers_only_about_real_auctions`. NOT fixed here, deliberately: `itemData.itemState: "listFS"` is not a FIFA 17 token (0 occurrences in CardsDLL md5 4de3493131d7d2ff7f8b360c5ac9b655, 0 in 4.26 GiB of process memory, decodes to -1; the real value is `forSale` = 5, and the Python oracle emits `listFS` too — which is why the differential never caught it). `CARD_OFFERSTATE` is one of three unresolved action-gate candidates and every actionable row observed carried -1, so that change ships alone with its own live A/B.
This commit is contained in:
@@ -29,7 +29,6 @@
|
||||
//! `CHECK` constraint even though it is a transient intermediate — omitting it
|
||||
//! would make [`MarketStore::reserve_listing`] fail the constraint.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -285,10 +284,7 @@ impl MarketStore {
|
||||
.iter()
|
||||
.map(|r| r.get::<String, _>("name"))
|
||||
.collect();
|
||||
for (col, decl) in [
|
||||
("item_json", "TEXT"),
|
||||
("duration_secs", "INTEGER"),
|
||||
] {
|
||||
for (col, decl) in [("item_json", "TEXT"), ("duration_secs", "INTEGER")] {
|
||||
if !existing.iter().any(|c| c == col) {
|
||||
sqlx::query(&format!("ALTER TABLE listings ADD COLUMN {col} {decl}"))
|
||||
.execute(&pool)
|
||||
@@ -620,30 +616,6 @@ impl MarketStore {
|
||||
.map_err(db)?
|
||||
.rows_affected())
|
||||
}
|
||||
|
||||
/// Core owned-instance ids whose listing state FORBIDS advertising them as an
|
||||
/// unlisted pile member: `active` (a real auction is shown instead), `reserved`
|
||||
/// (a sale is in flight) and `sold` (the card is gone).
|
||||
///
|
||||
/// `cancelled` is deliberately absent: a cancelled listing means the card came
|
||||
/// back to the pile and IS an unlisted member again.
|
||||
///
|
||||
/// Needed because a pile row outlives its auction — after a sale the seller's
|
||||
/// `trade` row is stale, and filtering on the active set alone would re-advertise
|
||||
/// a sold card as an owned unlisted item.
|
||||
pub async fn blocking_core_items(&self) -> Result<HashSet<String>, MarketError> {
|
||||
let rows = sqlx::query(
|
||||
"SELECT DISTINCT core_item_id FROM listings \
|
||||
WHERE core_item_id IS NOT NULL AND state IN ('active','reserved','sold')",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(db)?;
|
||||
Ok(rows
|
||||
.iter()
|
||||
.filter_map(|r| r.get::<Option<String>, _>("core_item_id"))
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -769,7 +741,18 @@ mod tests {
|
||||
|
||||
async fn seed(store: &MarketStore, id: &str) -> Listing {
|
||||
store
|
||||
.create_listing(id, "card_pl_001", None, None, None, 900, 2500, None, None, None)
|
||||
.create_listing(
|
||||
id,
|
||||
"card_pl_001",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
900,
|
||||
2500,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
@@ -800,7 +783,18 @@ mod tests {
|
||||
seed(&store, "900000001").await;
|
||||
assert!(matches!(
|
||||
store
|
||||
.create_listing("900000001", "card_pl_002", None, None, None, 1, 2, None, None, None)
|
||||
.create_listing(
|
||||
"900000001",
|
||||
"card_pl_002",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
2,
|
||||
None,
|
||||
None,
|
||||
None
|
||||
)
|
||||
.await,
|
||||
Err(MarketError::Conflict)
|
||||
));
|
||||
|
||||
Reference in New Issue
Block a user