feat(host): serve owned non-player content from Core's ownership truth

Follows Core's kit designations becoming generic active-item slots: the host
reads `GET /club/active-items` (five always-present slots) instead of the
removed `/club/kits`.

Adds the consumables route and widens the club families to every content kind,
all resolved from Core ownership + the FIFA catalog. An item the client sees is
now an item Core actually owns.
This commit is contained in:
funman300
2026-08-21 19:49:59 +00:00
parent 6c7d0856b6
commit 802f0f580f
4 changed files with 781 additions and 41 deletions
+17 -12
View File
@@ -28,6 +28,7 @@ use serde_json::{json, Value};
use openfut_adapter_fifa17::fut::entities::ReverseEntityResolver;
use openfut_adapter_fifa17::fut::item::{shape_item, ItemIdentityResolver};
use openfut_adapter_fifa17::fut::item_state;
use openfut_adapter_fifa17::fut::non_economy;
use openfut_adapter_fifa17::fut::squad::SquadWireResolver;
@@ -81,7 +82,7 @@ fn trade_id_from_path(path: &str) -> Option<String> {
/// snapshot persisted at listing time; a row written before snapshots existed
/// degrades to the stub (honest, not fabricated).
///
/// `item_state` overrides the card's `itemState`. FIFA 17's vocabulary is the
/// `state` overrides the card's `itemState`. FIFA 17's vocabulary is the
/// 12-row `{const char*, int}` table at `0x180229cc0`, and `forSale` (5) is its
/// value for an item offered for sale. The Python oracle stamps `listFS` on the
/// seller's own pile instead — a token that does NOT EXIST in FIFA 17 (zero
@@ -89,8 +90,8 @@ fn trade_id_from_path(path: &str) -> Option<String> {
/// memory) and therefore decodes to `-1` through `FUN_180166660`, i.e. the client
/// is handed an unrecognised `CARD_OFFERSTATE`. Where the binary contradicts the
/// oracle, the binary wins.
fn auction_record_as(l: &Listing, item_state: &str) -> Value {
auction_record_tuned(l, item_state, None, 0)
fn auction_record_as(l: &Listing, state: &str) -> Value {
auction_record_tuned(l, state, None, 0)
}
/// [`auction_record_as`] with the two fields the staging sold experiment varies.
@@ -105,7 +106,7 @@ fn auction_record_as(l: &Listing, item_state: &str) -> Value {
/// what makes the client's reaction attributable to the token.
fn auction_record_tuned(
l: &Listing,
item_state: &str,
state: &str,
sold_bid_state: Option<&str>,
coins_processed: i64,
) -> Value {
@@ -152,7 +153,7 @@ fn auction_record_tuned(
.map(|mut card| {
// Keep the wire identity and presentation state authoritative here.
card["id"] = json!(item_id);
card["itemState"] = json!(item_state);
card["itemState"] = json!(state);
card["untradeable"] = json!(false);
card
})
@@ -160,7 +161,7 @@ fn auction_record_tuned(
json!({
"id": item_id,
"resourceId": resource,
"itemState": item_state,
"itemState": state,
"untradeable": false,
})
});
@@ -204,9 +205,9 @@ fn auction_record_tuned(
/// closed/sold echoes the buy path returns.
fn auction_record(l: &Listing) -> Value {
let state = if l.state == "active" {
"forSale"
item_state::FOR_SALE
} else {
"free"
item_state::FREE
};
auction_record_as(l, state)
}
@@ -444,7 +445,7 @@ pub async fn handle_market_query(
};
let mut auctions: Vec<Value> = listings
.iter()
.map(|l| auction_record_as(l, "forSale"))
.map(|l| auction_record_as(l, item_state::FOR_SALE))
.collect();
// STAGING ONLY. FIFA 17's bulk `DELETE …/trade/sold` verb only makes sense if
// sold rows persist in the seller's pile until acknowledged, so the experiment
@@ -456,7 +457,7 @@ pub async fn handle_market_query(
for l in &sold {
auctions.push(auction_record_tuned(
l,
"forSale",
item_state::FOR_SALE,
exp.bid_state,
exp.coins_processed,
));
@@ -607,7 +608,7 @@ pub async fn handle_market_status(
};
let auctions: Vec<Value> = listings
.iter()
.map(|l| auction_record_tuned(l, "forSale", exp.bid_state, exp.coins_processed))
.map(|l| auction_record_tuned(l, item_state::FOR_SALE, exp.bid_state, exp.coins_processed))
.collect();
eprintln!(
"utas-host owner=RUST route=market-status requested={} returned={} query={}",
@@ -723,7 +724,7 @@ pub async fn handle_market_buy(
rec["tradeState"] = json!("closed");
rec["bidState"] = json!("highest");
rec["currentBid"] = json!(price);
rec["itemData"]["itemState"] = json!("free");
rec["itemData"]["itemState"] = json!(item_state::FREE);
ok_json(&json!({ "auctionInfo": [rec], "credits": new_balance }))
}
// Insufficient funds surfaced by Core (concurrent debit) -> 461.
@@ -1155,6 +1156,10 @@ mod tests {
let pile = handle_market_query("active", &econ, &store, SoldExperiment::OFF).await;
let rec = parse(&pile)["auctionInfo"][0].clone();
assert_eq!(rec["itemData"]["itemState"], "forSale");
assert!(
item_state::is_recovered(rec["itemData"]["itemState"].as_str().unwrap()),
"every emitted itemState must be in FIFA 17's own 12-row table"
);
assert_eq!(rec["itemData"]["rating"], 84);
assert_eq!(rec["itemData"]["id"], 100004617i64);
assert_eq!(rec["itemData"]["resourceId"], 169193);