022634704a
Adds the manager reference, the last remaining difference from the squad the same client demonstrably accepts. Staging's squad shape is now identical to production's: zero missing keys, zero type differences, zero empty-vs-populated mismatches. The manager looked unfixable. Production points at instance 100000427 while the staging club holds 11 players and zero staff, so there was apparently nothing to reference, and inventing an id would have pointed at a non-existent item. Checking production properly dissolved the problem: 100000427 is absent from production's OWN club listing too. /club/staff returns 1975 items spanning ids 100000001..100004826 and 100000427 is not among them, and the type=staff/type=manager filters are ignored (200 players either way). Production's manager reference is dangling and the client accepts that squad anyway, which proves the client does not validate the manager id against the club -- only a populated array matters. So the reference is mirrored verbatim, dangling id included. That replicates the known-good state exactly and is better than pointing the manager slot at a player, which would have been a guess dressed up as a fix. Method note: every step here came from diffing against production rather than reading the client. The host reported squad-active 200 outcome=ok throughout, and 200 with the right players was never evidence the client accepted the body.
188 lines
8.5 KiB
Python
Executable File
188 lines
8.5 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 squad-manager reference, mirrored verbatim; it resolves to nothing in
|
|
# production either (absent from its own /club/staff listing), which is exactly why
|
|
# copying it is safe.
|
|
PRODUCTION_MANAGER_REF = 100000427
|
|
# 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)],
|
|
# Mirrors production's manager reference verbatim, including the fact that
|
|
# the id resolves to nothing.
|
|
#
|
|
# This looked unfixable at first: production points at instance 100000427
|
|
# while the staging club holds 11 players and zero staff, so there was
|
|
# apparently no manager to reference. Then checking production properly
|
|
# showed 100000427 is absent from its OWN club listing too --
|
|
# /club/staff returns 1975 items spanning ids 100000001..100004826 and
|
|
# 100000427 is not one of them. Production's manager reference is dangling
|
|
# and the client accepts that squad regardless, which proves the client does
|
|
# not validate the manager id against the club. Only a populated array
|
|
# matters, so replicating the known-good state exactly is both faithful and
|
|
# sufficient -- and it beats pointing the manager slot at a player.
|
|
"manager": [{"id": PRODUCTION_MANAGER_REF, "dream": False}],
|
|
}
|
|
|
|
|
|
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())
|