#!/usr/bin/env python3 """Convert decklist.mgt -> Forge .dck format. python3 tools/to_dck.py --deck decks/reaper-king-no-more python3 tools/to_dck.py --deck decks/reaper-king-no-more [decklist.mgt] [-o ~/forge/decks/.dck] Forge sections are [Commander], [Main], [Sideboard] (verified in DeckSection.java). Card names must match Forge's own database, so this validates against res/cardsfolder and reports anything Forge won't recognise BEFORE you sim. Output filename and [metadata] Name= are both derived from the deck (slug / config.yaml `name:`) -- never a hardcoded "reaper.dck" / deck title. """ import sys, os, re, zipfile sys.path.insert(0, os.path.dirname(__file__)) from cards import load_decklist from deckctx import DeckContext from collections import Counter def load_subs(subs_file, enabled=True): """Simulation-only card swaps for cards Forge's AI genuinely cannot pilot. Returns {real_card: stand_in}. See /sim_substitutions.yaml for the rationale and the deviations each swap introduces. """ if not enabled or not subs_file or not os.path.exists(subs_file): return {} try: import yaml except ImportError: print("warning: pyyaml missing, substitutions skipped", file=sys.stderr) return {} d = yaml.safe_load(open(subs_file)) or {} return d.get("substitutions") or {} FORGE = os.path.expanduser("~/forge") def forge_card_names(): """Every card name Forge knows, from its cardsfolder (dir or .zip).""" names = set() folder = os.path.join(FORGE, "res", "cardsfolder") zpath = os.path.join(folder, "cardsfolder.zip") if os.path.exists(zpath): with zipfile.ZipFile(zpath) as z: for n in z.namelist(): if not n.endswith(".txt"): continue with z.open(n) as f: for line in f: line = line.decode("utf-8", "ignore").strip() if line.startswith("Name:"): names.add(line[5:].strip()); break for root, _, files in os.walk(folder): for fn in files: if not fn.endswith(".txt"): continue try: with open(os.path.join(root, fn), encoding="utf-8", errors="ignore") as f: for line in f: if line.startswith("Name:"): names.add(line[5:].strip()); break except OSError: pass return names def main(): ctx = DeckContext.from_args() args = [a for a in sys.argv[1:]] out = args[args.index("-o") + 1] if "-o" in args else \ os.path.join(FORGE, "decks", ctx.dck_filename()) src = next((a for a in args if a.endswith(".mgt")), ctx.decklist_path) commander, deck = load_decklist(src) subs_file = ctx.path(ctx.config.get("sim_substitutions_file", "sim_substitutions.yaml")) subs = load_subs(subs_file, enabled="--no-subs" not in args) applied = {} if subs: present = set(deck) swapped = [] # NB: not `out` -- that's the output path for n in deck: t = subs.get(n) # never substitute into a duplicate: that would break singleton if t and t not in present: applied[n] = t present.add(t) swapped.append(t) else: if t: print(f"note: not substituting {n} -> {t} " f"({t} already in deck)", file=sys.stderr) swapped.append(n) deck = swapped if applied: print(f"substituted {len(applied)} AI-unpilotable card(s) for simulation:", file=sys.stderr) for a, b in applied.items(): print(f" {a} -> {b}", file=sys.stderr) print(" (per-card stats for these describe the STAND-IN, not the real card)", file=sys.stderr) known = forge_card_names() if known: missing = sorted({n for n in [commander] + deck if n not in known}) if missing: print(f"!! {len(missing)} card(s) Forge does not recognise:", file=sys.stderr) for m in missing: print(f" {m}", file=sys.stderr) print(" (fix the name in decklist.mgt, or Forge will drop it)\n", file=sys.stderr) else: print("warning: could not read Forge's card list; skipping validation", file=sys.stderr) lines = ["[metadata]", f"Name={ctx.forge_metadata_name()}", "", "[Commander]", f"1 {commander}", "", "[Main]"] for name, count in sorted(Counter(deck).items()): lines.append(f"{count} {name}") lines += ["", "[Sideboard]", ""] os.makedirs(os.path.dirname(out), exist_ok=True) open(out, "w").write("\n".join(lines) + "\n") print(f"wrote {out} ({len(deck)} + 1 commander)") return 0 if __name__ == "__main__": sys.exit(main())