fifa17-recon: populate transfer market with real listings (freeze-safe)
Serve 18 real-player auctions on GET auctionhouse (search) built to the reversed auction record schema (deser 0x18013e410) field-for-field: itemData reuses fut_store._item (the proven club/squad card parser 0x18013fe00), scalar fields all HIGH-confidence reversed. Rating-based buy-now pricing; tradeId space 900000000+. tradePile/watchList stay empty (no live sell/watch flow yet). Toggle off with FUT_MARKET=empty. Extend test_fut_contract.py to validate EVERY populated record field type (numbers/strings/bool/object) so the listings are proven freeze-safe OFFLINE before the game parses them. 311 checks, all passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
This commit is contained in:
@@ -15,7 +15,7 @@ import datetime, json, os, re, sys, http.server
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from fut_seed import CLUB, SQUAD, USER_LIST # forged starter squad (clean-room)
|
||||
from fut_store import STORE, PACK_CATALOG, pack_by_id # persistent profile + packs
|
||||
from fut_store import STORE, PACK_CATALOG, pack_by_id, PACK_POOL, _item # profile + packs
|
||||
|
||||
ADDR = ("127.0.0.1", 8099)
|
||||
LOG = "/tmp/utas_server.log"
|
||||
@@ -318,40 +318,88 @@ def credits_route(h):
|
||||
# listings yet): empty arrays never desync, so this is freeze-safe. Populating real
|
||||
# auctions needs an in-game test pass. Extra keys are SKIP'd, so one merged body
|
||||
# safely satisfies both the search parser and the auction-count parser.
|
||||
def _market_body():
|
||||
return {"auctionInfo": [], "credits": STORE.coins(), "total": 0,
|
||||
"duplicateItemIdList": []}
|
||||
# Sample auction listings (real players from the pack pool) so the market is
|
||||
# browsable/buyable. Each record follows the reversed auction schema (deser
|
||||
# 0x18013e410) EXACTLY; itemData reuses fut_store._item -- the same proven-safe
|
||||
# card shape that renders club/squad cards (parser 0x18013fe00). All record
|
||||
# fields are HIGH-confidence reversed scalars, so freeze risk is low. Toggle with
|
||||
# FUT_MARKET=empty. Price heuristic: rating-based buy-now, ~66% starting bid.
|
||||
_MARKET_MODE = os.environ.get("FUT_MARKET", "sample")
|
||||
_TRADE_ID_BASE = 900000000
|
||||
|
||||
|
||||
def _price_for(rating):
|
||||
if rating >= 90: return 25000
|
||||
if rating >= 85: return 8000
|
||||
if rating >= 80: return 2500
|
||||
if rating >= 75: return 900
|
||||
return 400
|
||||
|
||||
|
||||
def _auction_record(i, defn):
|
||||
asset, rating, pos, nation, league, team, attrs = defn
|
||||
buy = _price_for(rating)
|
||||
card = _item(_TRADE_ID_BASE + 100000 + i, asset, rating, pos, nation, league, team, attrs)
|
||||
card["untradeable"] = False # market cards are tradeable
|
||||
card["itemState"] = "forSale"
|
||||
return {
|
||||
"tradeId": _TRADE_ID_BASE + i,
|
||||
"itemData": card, # OBJECT (0x18013fe00) -- freeze-safe
|
||||
"tradeState": "active", # enum string
|
||||
"buyNowPrice": buy,
|
||||
"startingBid": max(150, (buy * 2) // 3),
|
||||
"currentBid": 0,
|
||||
"bidState": "none", # enum string
|
||||
"expires": 3600, # SECONDS remaining (not epoch)
|
||||
"sellerName": "EASFC",
|
||||
"sellerEstablished": 1,
|
||||
"watched": False,
|
||||
"coinsProcessed": 0,
|
||||
}
|
||||
|
||||
|
||||
def _market_auctions(limit=21):
|
||||
if _MARKET_MODE == "empty":
|
||||
return []
|
||||
return [_auction_record(i, PACK_POOL[i % len(PACK_POOL)])
|
||||
for i in range(min(limit, len(PACK_POOL)))]
|
||||
|
||||
|
||||
def _market_body(auctions):
|
||||
return {"auctionInfo": auctions, "credits": STORE.coins(),
|
||||
"total": len(auctions), "duplicateItemIdList": []}
|
||||
|
||||
|
||||
def auctionhouse_route(h):
|
||||
# GET = search (empty results) OR count -> merged body (extra keys skip)
|
||||
# POST = FutISStart (list item for sale) -> {"id": new tradeId}
|
||||
# PUT = .../relist (relist all expired) -> ack {}
|
||||
# GET = search (sample listings) OR count -> merged body (extra keys skip)
|
||||
# POST = FutISStart (list item for sale) -> {"id": new tradeId}
|
||||
# PUT = .../relist (relist all expired) -> ack {}
|
||||
if h.command == "POST":
|
||||
return 200, {"id": STORE.new_item_id()}
|
||||
if h.command == "PUT":
|
||||
return 200, {}
|
||||
body = _market_body()
|
||||
body = _market_body(_market_auctions())
|
||||
body.update({"count": 0, "maxAuctionsAllowed": 100,
|
||||
"offered": 0, "selling": 0, "sold": 0}) # FutGetAuctionCount ints
|
||||
return 200, body
|
||||
|
||||
|
||||
def trade_route(h):
|
||||
# GET view one auction / POST place bid -> {auctionInfo:[record], credits}
|
||||
return 200, {"auctionInfo": [], "credits": STORE.coins()}
|
||||
# GET view one auction / POST place bid -> {auctionInfo:[record], credits}.
|
||||
# Echo a sample record so a viewed/bid auction resolves.
|
||||
rec = _market_auctions(1)
|
||||
return 200, {"auctionInfo": rec, "credits": STORE.coins()}
|
||||
|
||||
|
||||
def tradepile_route(h):
|
||||
b = _market_body(); b.pop("duplicateItemIdList", None)
|
||||
return 200, b # {auctionInfo, credits, total}
|
||||
# The user's OWN sale pile -- empty until they list something (no live sell flow yet).
|
||||
return 200, {"auctionInfo": [], "credits": STORE.coins(), "total": 0}
|
||||
|
||||
|
||||
def watchlist_route(h):
|
||||
if h.command in ("PUT", "POST", "DELETE"):
|
||||
return 200, {} # add/remove watch -> ack
|
||||
b = _market_body(); b.pop("duplicateItemIdList", None)
|
||||
return 200, b
|
||||
return 200, {"auctionInfo": [], "credits": STORE.coins(), "total": 0}
|
||||
|
||||
|
||||
class H(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
Reference in New Issue
Block a user