From 106cb839886c3753f1643954ae4af560ead4b4c1 Mon Sep 17 00:00:00 2001 From: funman300 Date: Fri, 21 Aug 2026 18:12:59 +0000 Subject: [PATCH] fix(staging): fill the real club's bench to the client's 18-player minimum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FIFA refuses to kick off with "your squad must have at least 11 players and 7 subs … currently below the minimum number of players (18)". The imported club's squad carries ONLY its starting XI, so a freshly installed real club is unplayable until someone fills the bench by hand in the hub. `fill_bench_to_minimum` tops the squad up with the club's best spare players, writing EMPTY bench slots from index 11 upward. The 23 slots are 0..10 pitch and 11..22 bench/reserves, derived from the index alone, so the starting XI — and any bench the operator has already chosen — is never touched and a re-run is a no-op. Only real PLAYER definitions are eligible: a kit, a manager or a consumable in a squad slot is nonsense the client would drop anyway. Selection is best-rating-first with a stable id tiebreak, so the same bench comes back on a re-run rather than shuffling. Scoped to the REAL club on purpose. The 14-item fixture is a market test bed with a single spare player; demanding 18 there would abort a bring-up that never needed to kick off. Fixture mode therefore does not call this at all. Verified against a copy of the club snapshot (the live staging database was left alone, since it currently holds a squad the operator saved by hand): 11 -> 18 players, slots 0..17, no duplicate instance in two slots, every filled pick a real player definition, and a second run filling nothing. This is NOT a diagnosis of the failure the operator just hit — that squad had already been filled to 23 valid players before the match was created, and the host log shows the client issued no request at all after `match-create` beyond account-sync, so that refusal is decided entirely client-side. It removes the variable: after a clean bring-up the club is now playable without hand-editing. --- scripts/sold-staging-up.py | 87 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/scripts/sold-staging-up.py b/scripts/sold-staging-up.py index b90653e..160f688 100755 --- a/scripts/sold-staging-up.py +++ b/scripts/sold-staging-up.py @@ -748,6 +748,78 @@ def verify_blaze_patch(lay: Layout) -> None: # --- seeding ------------------------------------------------------------------------ +# FIFA refuses to kick off unless the squad has 11 starters AND 7 substitutes: +# "your squad must have at least 11 players and 7 subs … currently below the +# minimum number of players (18)". The imported club's squad has only its 11 +# starters, so a freshly installed real club is UNPLAYABLE until the bench is +# filled. The 23 slots are 0..10 pitch, 11..22 bench/reserves, derived from the +# index alone. +CLIENT_MIN_SQUAD = 18 +SQUAD_SLOTS = 23 + + +def fill_bench_to_minimum(conn, club_id: str, squad_id: str, lay: Layout) -> int: + """Top the squad up to the client's minimum with the club's best spare + players, filling empty bench slots from index 11 upward. + + Only EMPTY slots are written, so the starting XI — and any bench the + operator has already chosen — is never touched, and a re-run is a no-op. + Only real PLAYER definitions are eligible: a kit, a manager or a consumable + in a squad slot would be nonsense, and the client would drop it anyway. + """ + taken = { + row[0] + for row in conn.execute( + "SELECT owned_card_id FROM squad_players WHERE squad_id = ?", (squad_id,) + ) + } + used_slots = { + row[0] + for row in conn.execute( + "SELECT position_index FROM squad_players WHERE squad_id = ?", (squad_id,) + ) + } + if len(taken) >= CLIENT_MIN_SQUAD: + return 0 + + with open(safe_path(lay.catalog)) as fh: + catalog = json.load(fh)["cards"] + with open(safe_path(lay.cards)) as fh: + overall = {c["id"]: c.get("overall", 0) for c in json.load(fh)} + + # Best first, then a stable id tiebreak so a re-run picks the same bench. + candidates = [ + (overall.get(card_id, 0), owned_id, card_id) + for owned_id, card_id in conn.execute( + "SELECT id, card_id FROM owned_cards WHERE club_id = ? AND is_loan = 0", + (club_id,), + ) + if owned_id not in taken + and catalog.get(card_id, {}).get("kind", "player") == "player" + ] + candidates.sort(key=lambda c: (-c[0], c[1])) + + free_slots = [i for i in range(11, SQUAD_SLOTS) if i not in used_slots] + need = CLIENT_MIN_SQUAD - len(taken) + if need > len(free_slots) or need > len(candidates): + raise Fatal( + f"cannot reach the client's {CLIENT_MIN_SQUAD}-player minimum: need " + f"{need} more, but {len(free_slots)} free bench slots and " + f"{len(candidates)} eligible spare players" + ) + + rows = [ + (f"sp-bench-{slot}", squad_id, owned_id, slot, 0, 1) + for slot, (_ovr, owned_id, _card) in zip(free_slots, candidates[:need]) + ] + conn.executemany( + "INSERT INTO squad_players (id, squad_id, owned_card_id, position_index, " + "is_captain, is_on_bench) VALUES (?, ?, ?, ?, ?, ?)", + rows, + ) + return len(rows) + + def seed_core_db(lay: Layout, real_club: dict | None) -> None: @@ -847,6 +919,15 @@ def seed_core_db(lay: Layout, real_club: dict | None) -> None: "updated_at) VALUES (?, ?, ?)", (squad_row[0], STAGING_MANAGER["owned_id"], TS), ) + + # Only the REAL club is meant to be played. The 14-item fixture is a + # market test bed with a single spare player, so demanding 18 there + # would abort a bring-up that never needed to kick off. + filled = ( + fill_bench_to_minimum(conn, seller_club, squad_row[0], lay) + if real_club is not None + else 0 + ) finally: conn.close() if real_club is None: @@ -863,6 +944,12 @@ def seed_core_db(lay: Layout, real_club: dict | None) -> None: f"{len(STAGING_KITS)} active kits, the manager " f"{STAGING_MANAGER['label']} and Buyer B ({BUYER_COINS} coins)" ) + if filled: + ok( + f"filled {filled} empty bench slot(s) with the club's best spare " + f"players: FIFA refuses to kick off below {CLIENT_MIN_SQUAD} " + "(11 starters + 7 subs), and the imported squad carries only its XI" + ) def db_summary(lay: Layout) -> str: