test(scripts): seed the staging seller an XI so the FUT hub will open

The A/B identity had owned items but no squad, because the sold-row work only ever
needed the tradePile wire. Every headless check passed -- none of them asks for a
squad -- but the real client refuses to enter the FUT hub with an empty one and shows
a squad update error. A squad is a hub precondition, not a Transfer-List detail.

Seeds via the real PUT /ut/game/fifa17/squad/0, the same request the client sends, so
parse_squad_put/build_squad_write produce exactly what a genuine save would. Writing
Core rows by hand could yield a shape the live path never emits, which is the kind of
divergence that quietly invalidates an experiment.

Picks one owned player per 4-3-3 slot, best rating first, without reusing an instance;
fills the fixed 23-slot array with 0..=10 as the pitch and empty slots as
itemData.id == 0; refuses to write a partial XI and reports any out-of-position
substitution loudly rather than silently reproducing the broken state. Verified
0 -> 11 occupied with no substitutions and a {"id":0} ack.
This commit is contained in:
funman300
2026-08-18 03:07:42 +00:00
parent c71593b286
commit ea92057e53
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Seed the staging seller a valid starting XI so the FIFA hub will open.
The sold-row A/B only ever needed the tradePile wire, so the staging identity was
created with owned items but NO squad. Every headless check passed because none of
them asks for a squad -- but the real client refuses to enter the FUT hub with an
empty one ("squad update error"), because a squad is a hub precondition, not a
Transfer-List detail.
This seeds it through the REAL route -- `PUT /ut/game/fifa17/squad/0`, the same
request the client itself sends -- so the squad is written by
parse_squad_put/build_squad_write exactly as a genuine save would be. Writing
Core/DB rows by hand would risk a shape the live path would never produce, which is
the sort of divergence that invalidates an experiment.
Wire facts (openfut-adapter-fifa17/src/fut/squad.rs): the players array is a fixed 23
slots; 0..=10 are the pitch, 11..=22 bench/reserves, derived from the INDEX alone and
never from the formation token. Empty slots are `itemData.id == 0`. The formation
token is carried verbatim and never interpreted server-side. A successful save
answers `{"id": 0}` and does NOT echo the squad.
python3 scripts/sold-staging-seed-squad.py # seed, then verify
python3 scripts/sold-staging-seed-squad.py --show # read-only
"""
import http.client
import json
import sys
HOST, PORT = "127.0.0.1", 8299
FORBIDDEN = {8099, 8199, 18080, 8443, 42127, 42130, 42131, 4216}
HEADERS = {"X-OpenFUT-Game": "fifa17", "Content-Type": "application/json"}
SLOTS = 23
STARTERS = 11
# 4-3-3, laid out in the conventional FIFA slot order so the pitch reads correctly:
# GK, RB, CB, CB, LB, CM, CM, CM, RW, ST, LW.
LINEUP = ["GK", "RB", "CB", "CB", "LB", "CM", "CM", "CM", "RW", "ST", "LW"]
FORMATION = "f433"
def call(method, path, body=None):
assert PORT not in FORBIDDEN, "refusing to touch a production port"
c = http.client.HTTPConnection(HOST, PORT, timeout=20)
payload = json.dumps(body) if body is not None else None
c.request(method, path, body=payload, headers=HEADERS)
r = c.getresponse()
raw = r.read()
c.close()
try:
return r.status, json.loads(raw)
except Exception:
return r.status, raw.decode(errors="replace")
def club_players():
status, body = call("GET", "/ut/game/fifa17/club?count=200")
assert status == 200, f"/club -> {status}"
items = body.get("itemData", body.get("items", []))
return [i for i in items if i.get("itemType") == "player"]
def pick_xi(players):
"""Choose one owned player per lineup position, best rating first, without
reusing an instance. A position with no candidate is reported rather than
quietly left empty -- an incomplete XI is why the hub errored in the first
place, so silently reproducing it would defeat the point."""
remaining = sorted(players, key=lambda p: -int(p.get("rating") or 0))
chosen, missing = [], []
for want in LINEUP:
hit = next((p for p in remaining if p.get("preferredPosition") == want), None)
if hit is None:
hit = next((p for p in remaining), None) # any spare body, flagged below
if hit is not None:
missing.append(f"{want}->{hit.get('preferredPosition')}")
if hit is None:
missing.append(f"{want}->NONE")
chosen.append(None)
continue
remaining.remove(hit)
chosen.append(hit)
return chosen, missing
def build_put(chosen):
players = []
for idx in range(SLOTS):
occupant = chosen[idx] if idx < STARTERS and chosen[idx] else None
players.append({
"index": idx,
"itemData": {"id": int(occupant["id"]) if occupant else 0, "dream": False},
"kitNumber": idx + 1 if occupant else 0,
})
captain = next((int(p["id"]) for p in chosen if p), 0)
return {
"id": 0,
"squadName": "Staging XI",
"formation": FORMATION,
"captain": captain,
"players": players,
"manager": [],
"kicktakers": [],
}
def show():
status, body = call("GET", "/ut/game/fifa17/squad/0")
filled = [p for p in body.get("players", []) if p.get("itemData", {}).get("id")]
print(f" /squad/0 -> {status} id={body.get('id')} "
f"players={len(body.get('players', []))} occupied={len(filled)}")
for p in filled:
d = p.get("itemData", {})
print(f" index={p.get('index')} id={d.get('id')} res={d.get('resourceId')} "
f"pos={d.get('preferredPosition')} rating={d.get('rating')}")
return len(filled)
def main():
print("=== staging squad, before ===")
before = show()
if "--show" in sys.argv:
return 0
players = club_players()
print(f"=== owned players available: {len(players)} ===")
chosen, missing = pick_xi(players)
if missing:
# Loud, because an out-of-position XI still opens the hub but is not what a
# real save would look like.
print(f" NOTE: substitutions made for {len(missing)} slot(s): {missing}")
if any(c is None for c in chosen):
raise SystemExit("not enough owned players for a starting XI; refusing to "
"write a partial squad")
put = build_put(chosen)
print(f"=== PUT /squad/0 formation={put['formation']} captain={put['captain']} "
f"occupied={sum(1 for p in put['players'] if p['itemData']['id'])} ===")
status, body = call("PUT", "/ut/game/fifa17/squad/0", put)
print(f" -> {status} {json.dumps(body)}")
if status != 200 or body != {"id": 0}:
raise SystemExit(f"squad save did not acknowledge as {{'id':0}}: {status} {body}")
print("=== staging squad, after ===")
after = show()
if after != STARTERS:
raise SystemExit(f"VERIFY FAILED: expected {STARTERS} occupied slots, got {after}")
print(f" OK: {before} -> {after} occupied slots; the hub precondition is satisfied")
return 0
if __name__ == "__main__":
sys.exit(main())