fix(staging): fill the real club's bench to the client's 18-player minimum

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.
This commit is contained in:
funman300
2026-08-21 18:12:59 +00:00
parent 33300f2ad1
commit 106cb83988
+87
View File
@@ -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: