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:
@@ -748,6 +748,78 @@ def verify_blaze_patch(lay: Layout) -> None:
|
|||||||
|
|
||||||
|
|
||||||
# --- seeding ------------------------------------------------------------------------
|
# --- 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:
|
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 (?, ?, ?)",
|
"updated_at) VALUES (?, ?, ?)",
|
||||||
(squad_row[0], STAGING_MANAGER["owned_id"], TS),
|
(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:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
if real_club is None:
|
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"{len(STAGING_KITS)} active kits, the manager "
|
||||||
f"{STAGING_MANAGER['label']} and Buyer B ({BUYER_COINS} coins)"
|
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:
|
def db_summary(lay: Layout) -> str:
|
||||||
|
|||||||
Reference in New Issue
Block a user