Three fixes that prevented precon decks from loading into actual gameplay:
1. ui.js: window.startGame() now calls startGameWithDecks() when
setupDeckChoices has entries (the 3-tab selector stores choices there).
Previously it called initGame() which looked for old deck-sel-{i}
dropdowns that no longer exist.
2. engine.js: loadPreconForPlayer() now fetches .dck file content from
the backend (/api/lab/precons/deck?fileName=...) when precon.decklist
is empty. Falls back to fetching precon-decks/{file} directly.
PRECON_DATA entries only have metadata (name, commander, set) — the
actual card list lives in the .dck files.
3. engine.js: parseDeckList() now handles .dck bracket-style section
headers like [Commander], [Main], [metadata]. Previously only matched
bare text like 'COMMANDER:' without brackets. Also skips [metadata]
lines (e.g. 'Name=Elven Empire').
4. routes/precon.py: Added GET /api/lab/precons/deck?fileName=... endpoint
to serve raw .dck file content for the LAN client.
131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
"""
|
|
routes/precon.py
|
|
================
|
|
Precon deck management endpoints:
|
|
GET /api/lab/precons
|
|
POST /api/lab/precons/install
|
|
POST /api/lab/precons/install-batch
|
|
POST /api/lab/precons/refresh
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from fastapi import APIRouter, HTTPException
|
|
|
|
from models.state import CFG
|
|
from services.precon_service import download_precon_database, PRECON_INDEX
|
|
from services.logging import log
|
|
from services.precon_service import _get_precon_dir
|
|
|
|
router = APIRouter(tags=["precon"])
|
|
|
|
|
|
@router.get("/api/lab/precons")
|
|
async def list_precons():
|
|
"""List all available precon decks."""
|
|
return {"precons": PRECON_INDEX} # reads current value every call
|
|
|
|
|
|
@router.get("/api/lab/precons/deck")
|
|
async def get_precon_deck(fileName: str):
|
|
"""Return the raw decklist content of a precon .dck file.
|
|
Query: ?fileName=Elven_Empire.dck
|
|
"""
|
|
if not fileName:
|
|
raise HTTPException(400, "fileName query parameter is required")
|
|
# Sanitize: only allow .dck files, no path traversal
|
|
if '..' in fileName or '/' in fileName or '\\' in fileName:
|
|
raise HTTPException(400, "Invalid fileName")
|
|
if not fileName.endswith('.dck'):
|
|
raise HTTPException(400, "Only .dck files are supported")
|
|
|
|
src = _get_precon_dir() / fileName
|
|
if not src.exists():
|
|
raise HTTPException(404, f"Precon not found: {fileName}")
|
|
|
|
try:
|
|
content = src.read_text(encoding='utf-8')
|
|
except OSError as e:
|
|
raise HTTPException(500, f"Failed to read deck file: {e}")
|
|
|
|
return {"fileName": fileName, "decklist": content}
|
|
|
|
|
|
@router.post("/api/lab/precons/install")
|
|
async def install_precon(req: dict):
|
|
"""Install a precon deck to the Forge decks directory.
|
|
Body: {"fileName": "Elven_Empire.dck"} or {"name": "Elven_Empire.dck"}
|
|
"""
|
|
file_name = req.get("fileName", "") or req.get("name", "")
|
|
if not file_name:
|
|
raise HTTPException(400, "fileName is required")
|
|
|
|
src = _get_precon_dir() / file_name
|
|
if not src.exists():
|
|
raise HTTPException(404, f"Precon not found: {file_name}")
|
|
|
|
decks_dir = CFG.forge_decks_dir
|
|
if not decks_dir or not os.path.isdir(decks_dir):
|
|
raise HTTPException(500, f"Forge decks directory not found: {decks_dir}")
|
|
|
|
dst = Path(decks_dir) / file_name
|
|
try:
|
|
shutil.copy2(str(src), str(dst))
|
|
except OSError as e:
|
|
log.error(f"Failed to copy deck file {file_name}: {e}")
|
|
raise HTTPException(500, f"Failed to copy deck file: {e}")
|
|
|
|
deck_name = file_name.replace(".dck", "")
|
|
return {
|
|
"installed": True,
|
|
"deckName": deck_name,
|
|
"destination": str(dst),
|
|
"message": f"Installed {deck_name} to Forge decks",
|
|
}
|
|
|
|
|
|
@router.post("/api/lab/precons/install-batch")
|
|
async def install_precons_batch(req: dict):
|
|
"""Install multiple precon decks at once.
|
|
Body: {"fileNames": ["Elven_Empire.dck", ...]} or {"names": ["Elven_Empire.dck", ...]}
|
|
"""
|
|
file_names = req.get("fileNames", []) or req.get("names", [])
|
|
if not file_names:
|
|
raise HTTPException(400, "fileNames list is required")
|
|
|
|
decks_dir = CFG.forge_decks_dir
|
|
if not decks_dir or not os.path.isdir(decks_dir):
|
|
raise HTTPException(500, f"Forge decks directory not found: {decks_dir}")
|
|
|
|
results = []
|
|
for file_name in file_names:
|
|
src = _get_precon_dir() / file_name
|
|
if not src.exists():
|
|
results.append({"fileName": file_name, "installed": False, "error": "not found"})
|
|
continue
|
|
dst = Path(decks_dir) / file_name
|
|
try:
|
|
shutil.copy2(str(src), str(dst))
|
|
deck_name = file_name.replace(".dck", "")
|
|
results.append({"fileName": file_name, "installed": True, "deckName": deck_name})
|
|
except OSError as e:
|
|
log.error(f"Failed to copy deck file {file_name}: {e}")
|
|
results.append({"fileName": file_name, "installed": False, "error": str(e)})
|
|
|
|
return {"results": results}
|
|
|
|
|
|
@router.post("/api/lab/precons/refresh")
|
|
async def refresh_precons():
|
|
"""Force re-download of all Commander precon decks from GitHub."""
|
|
result = download_precon_database(force=True)
|
|
if result.get("error"):
|
|
raise HTTPException(502, result["error"])
|
|
return {
|
|
"message": f"Downloaded {result['downloaded']} Commander precon decks",
|
|
"total": result.get("total", 0),
|
|
}
|