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:
Executable
+147
@@ -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())
|
||||
Reference in New Issue
Block a user