96ca7c0484
First seed sent only squadName/formation/captain/players. The host logged squad-active 200 outcome=ok and /squad/0 showed 11 occupied slots, yet the client still threw a FUT squad update error -- a 200 with the right players is not proof the client accepts the body. Diffed against production's known-good squad, which the same client accepts, comparing key presence and JSON TYPES rather than values. Staging returned null for exactly the five fields the PUT never carried, because the extension stored nothing for them: squadType (a string enum), chemistry, rating, starRating (ints) and custom (the opaque 33-int tactics array the client definitely parses). kicktakers was empty where production carries five. Now sends all of them: squadType REGULAR_SQUAD, chemistry, rating/starRating derived from the XI's mean rating, production's custom array verbatim (opaque server-side, only its shape matters), and five kicktakers. Re-diff leaves exactly one difference -- manager, which production points at owned staff instance 100000427 while the staging club holds 11 players and zero staff. Left empty rather than inventing an id that references a non-existent item; recorded in the code as the one known remaining gap. Also fixes a KeyError from the rewrite dropping the players key.
175 lines
7.6 KiB
Python
Executable File
175 lines
7.6 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"
|
|
SQUAD_TYPE = "REGULAR_SQUAD"
|
|
CHEMISTRY = 50
|
|
# Production's opaque 33-int tactics array. Never parsed server-side; reused because
|
|
# only the shape (a JSON-encoded int array, not null) matters to the client.
|
|
CUSTOM = ("[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,"
|
|
"50,50,0,50,40,65,0,65,50,50,1]")
|
|
|
|
|
|
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)
|
|
ratings = [int(p.get("rating") or 0) for p in chosen if p]
|
|
mean_rating = sum(ratings) // len(ratings) if ratings else 0
|
|
return {
|
|
"id": 0,
|
|
"squadName": "Staging XI",
|
|
"formation": FORMATION,
|
|
"captain": captain,
|
|
"players": players,
|
|
# A first attempt sent only name/formation/captain/players and the client
|
|
# STILL refused the hub, while the host logged squad-active 200 ok -- the
|
|
# route was fine and the body was not. Diffing against production's
|
|
# known-good squad showed staging returning null for exactly these five,
|
|
# because the PUT never carried them and the extension stored nothing.
|
|
# Types matter here: the client wants scalars, not null.
|
|
"squadType": SQUAD_TYPE,
|
|
"chemistry": CHEMISTRY,
|
|
"rating": mean_rating,
|
|
"starRating": mean_rating,
|
|
# Opaque 33-int tactics array, carried verbatim and never interpreted
|
|
# server-side. Reused from production because only its SHAPE matters.
|
|
"custom": CUSTOM,
|
|
# Production carries five, all pointing at one player. Mirrored so the
|
|
# array is populated rather than empty.
|
|
"kicktakers": [{"index": i, "id": captain, "dream": False} for i in range(5)],
|
|
# Left empty deliberately: production references an owned staff instance
|
|
# (100000427) and the staging club holds 11 players and zero staff, so
|
|
# there is no manager to point at. Inventing an id would reference a
|
|
# non-existent item. This is the one remaining shape difference.
|
|
"manager": [],
|
|
}
|
|
|
|
|
|
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())
|