ea92057e53
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.
148 lines
5.9 KiB
Python
Executable File
148 lines
5.9 KiB
Python
Executable File
#!/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())
|