From 4075cf4ebd43be7a909d1512452af395d3335d1b Mon Sep 17 00:00:00 2001 From: Kenearos <86194771+Kenearos@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:29:34 +0200 Subject: [PATCH] StarCraft-Kampagnen-MCP-Server ("Missions-Baumeister") (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Projekt-Spezifikation für StarCraft-Kampagnen-MCP-Server in README festhalten Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01NaZBZofC1on2gkSMb5UWZy * StarCraft-Kampagnen-MCP-Server: vollstaendige Implementierung FastMCP-Server (stdio + Streamable HTTP) mit 13 sc_-Tools fuer das Lesen und Schreiben von StarCraft-Brood-War-Karten (.scm/.scx) ueber RichChk: - sc_list_maps, sc_describe_map, sc_list_locations, sc_list_triggers (lesen) - sc_create_location, sc_rename_location, sc_set_player_setup - sc_add_trigger (Kern-Tool: 10 Condition- und 20 Action-Typen), sc_remove_trigger, sc_clear_triggers - sc_embed_wav (Voiceover/Sound-Einbettung), sc_save_map, sc_reset_map Tolerante Pydantic-Eingaben, hilfreiche Fehlermeldungen, readOnly/destructive-Hints. Karten-Sitzung im Speicher pro Basis-Karte; Basis-Karte bleibt unveraendert. Deployment: Dockerfile (python:3.12-slim), docker-compose, Caddyfile, run.sh. Beispiel-Basis-Karte in data/maps. Selbsttest beweist die ganze Pipeline. Verifiziert: Pipeline + alle Tools + WAV-Einbettung + Streamable-HTTP-Handshake (echter MCP-Client), unter Python 3.11 und 3.12. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01NaZBZofC1on2gkSMb5UWZy --------- Co-authored-by: Claude --- .dockerignore | 12 + .gitignore | 12 + Caddyfile | 16 + Dockerfile | 28 ++ README.md | 226 ++++++++++++- data/maps/.gitkeep | 0 data/maps/base-map.scx | Bin 0 -> 103346 bytes docker-compose.yml | 37 +++ requirements.txt | 4 + run.sh | 40 +++ selftest.py | 155 +++++++++ starcraft_mcp/__init__.py | 21 ++ starcraft_mcp/enums.py | 111 +++++++ starcraft_mcp/richchk_logging.yaml | 2 + starcraft_mcp/server.py | 515 +++++++++++++++++++++++++++++ starcraft_mcp/triggers.py | 452 +++++++++++++++++++++++++ starcraft_mcp/workspace.py | 256 ++++++++++++++ 17 files changed, 1886 insertions(+), 1 deletion(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 Caddyfile create mode 100644 Dockerfile create mode 100644 data/maps/.gitkeep create mode 100755 data/maps/base-map.scx create mode 100644 docker-compose.yml create mode 100644 requirements.txt create mode 100755 run.sh create mode 100755 selftest.py create mode 100644 starcraft_mcp/__init__.py create mode 100644 starcraft_mcp/enums.py create mode 100644 starcraft_mcp/richchk_logging.yaml create mode 100644 starcraft_mcp/server.py create mode 100644 starcraft_mcp/triggers.py create mode 100644 starcraft_mcp/workspace.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..27050ae --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.venv/ +__pycache__/ +*.pyc +.git/ +.gitignore +README.md +*.md +data/maps/*.scx +data/maps/*.scm +data/maps/*.wav +# Die Beispiel-Basis-Karte wird zur Laufzeit ueber das Volume bereitgestellt, +# nicht ins Image gebacken (Volume ueberlagert /data/maps ohnehin). diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2e5f2ea --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Python +.venv/ +__pycache__/ +*.pyc +*.egg-info/ +.pytest_cache/ + +# Generierte Missionen / WAVs im Karten-Verzeichnis (Basis-Karte wird behalten, +# siehe Negation unten). +data/maps/* +!data/maps/base-map.scx +!data/maps/.gitkeep diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..e2cd3ad --- /dev/null +++ b/Caddyfile @@ -0,0 +1,16 @@ +# Caddy-Reverse-Proxy fuer den StarCraft-MCP-Server. +# +# Fuege diesen Block in deine bestehende Caddyfile ein (oder importiere die Datei). +# Caddy holt automatisch ein TLS-Zertifikat fuer die Subdomain. +# +# Voraussetzung: Caddy und der sc-mcp-Container haengen im selben Docker-Netzwerk +# (siehe docker-compose.yml -> networks). Dann ist der Service unter "sc-mcp:8000" +# erreichbar. + +sc-mcp.pixel-by-design.de { + reverse_proxy sc-mcp:8000 +} + +# Hinweis: Der MCP-Endpunkt (Streamable HTTP) liegt unter dem Pfad /mcp, also +# https://sc-mcp.pixel-by-design.de/mcp -- genau diese URL traegst du in Claude +# als Custom Connector ein. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..aa8a661 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.12-slim + +# StormLib (von RichChk mitgeliefert) braucht keine extra System-Libs auf glibc-Basis. +# 'slim' (Debian) ist glibc -> kompatibel mit der mitgelieferten StormLib .so. + +ENV PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + SC_TRANSPORT=http \ + SC_HOST=0.0.0.0 \ + SC_PORT=8000 \ + SC_MAPS_DIR=/data/maps + +WORKDIR /app + +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +COPY starcraft_mcp /app/starcraft_mcp +COPY selftest.py /app/selftest.py + +# Karten-Volume (Basis-Karten, WAVs, fertige Missionen). +RUN mkdir -p /data/maps +VOLUME ["/data/maps"] + +EXPOSE 8000 + +# Streamable-HTTP-Endpunkt unter http://0.0.0.0:8000/mcp +CMD ["python", "-m", "starcraft_mcp.server"] diff --git a/README.md b/README.md index 5b63c80..9f79f1e 100644 --- a/README.md +++ b/README.md @@ -1 +1,225 @@ -# Star-Edit \ No newline at end of file +# Star-Edit — StarCraft-Kampagnen-MCP-Server ("Missions-Baumeister") + +Ein **MCP-Server**, mit dem Claude StarCraft-Brood-War-Karten (`.scm`/`.scx`) liest und +schreibt. Du lieferst die Kreativ-Seite (Story, Dialoge, Voiceover, Sounds, Ablauf), +Claude baut daraus echte, **spielbare Missionsdateien** — komplette Logik, Texte und +Sound-Einbettung. + +> **Das Gelände wird NICHT generiert.** Jede Mission startet von einer vorhandenen +> Basis-Karte als Leinwand; der Server bearbeitet nur deren Daten-/Logik-Sektionen +> (Trigger, Locations, Player-Setup, Sounds). + +Die ganze Karten-Manipulation läuft über die Python-Bibliothek +[RichChk](https://github.com/sethmachine/richchk). + +--- + +## In 3 Schritten loslegen + +### 1. Server starten (ein Befehl) + +Auf dem VPS, im Projektverzeichnis: + +```bash +./run.sh +``` + +Das baut das Docker-Image (beim ersten Mal) und startet den Container `sc-mcp`. +Er lauscht intern auf Port **8000** mit Streamable-HTTP unter dem Pfad **`/mcp`**. + +Weitere Befehle: + +```bash +./run.sh logs # Live-Logs ansehen +./run.sh selftest # den eingebauten Selbsttest laufen lassen +./run.sh stop # Container stoppen +``` + +### 2. Subdomain via Caddy (TLS) + +Caddy terminiert TLS und leitet die Subdomain an den Container weiter. Trage den Block +aus der [`Caddyfile`](./Caddyfile) in deine bestehende Caddy-Konfiguration ein: + +```caddy +sc-mcp.pixel-by-design.de { + reverse_proxy sc-mcp:8000 +} +``` + +Voraussetzung: Caddy und der `sc-mcp`-Container hängen im selben Docker-Netzwerk (in +`docker-compose.yml` als externes Netzwerk `caddy` eingetragen — passe den Namen an +dein Setup an). + +### 3. In Claude als Custom Connector eintragen + +Die URL, die du in Claude einträgst: + +``` +https://sc-mcp.pixel-by-design.de/mcp +``` + +In Claude (Web/Desktop): **Einstellungen → Connectors → Custom Connector hinzufügen** → +obige URL als Streamable-HTTP-/Remote-MCP-Endpunkt eintragen. Danach stehen die +`sc_`-Tools im Chat zur Verfügung. + +--- + +## Wo liegen Karten, WAVs und Missionen? + +Alles im Verzeichnis **`./data/maps`** (im Container als `/data/maps` gemountet): + +- **Basis-Karten** (`.scx`/`.scm`) — die Leinwand für neue Missionen. +- **WAV-Dateien** (`.wav`) — Voiceover/Sounds, die eingebettet werden. +- **Fertige Missionen** — schreibt der Server hierhin. + +Eine **Beispiel-Basis-Karte** liegt bereits unter +[`data/maps/base-map.scx`](./data/maps/base-map.scx) (256×256, Jungle), damit du sofort +die erste Mission bauen kannst. + +--- + +## Die Tools (Prefix `sc_`) + +| Tool | Zweck | +|------|-------| +| `sc_list_maps` | listet alle Karten/Missionen im Verzeichnis *(nur lesen)* | +| `sc_describe_map` | Übersicht: Tileset, Größe, Player-Setup, Locations, Trigger-Zusammenfassung *(nur lesen)* | +| `sc_list_locations` | Locations auflisten *(nur lesen)* | +| `sc_create_location` | Location anlegen (Mittelpunkt + Größe in Pixeln) | +| `sc_rename_location` | Location umbenennen | +| `sc_set_player_setup` | Owner-Typ, Rasse und Force je Spieler setzen (+ Force-Namen) | +| `sc_add_trigger` | **Kern-Tool:** Trigger mit Bedingungen + Aktionen hinzufügen | +| `sc_list_triggers` | Trigger mit Klartext-Zusammenfassung auflisten *(nur lesen)* | +| `sc_remove_trigger` | einzelnen Trigger entfernen *(destruktiv)* | +| `sc_clear_triggers` | alle Trigger entfernen *(destruktiv)* | +| `sc_embed_wav` | WAV einbetten und referenzierbar machen | +| `sc_save_map` | aktuellen Stand als neue `.scx`/`.scm` schreiben | +| `sc_reset_map` | nicht gespeicherte Änderungen verwerfen *(destruktiv)* | + +**Arbeitsmodell:** Die Bearbeitung läuft pro Basis-Karte als Sitzung im Speicher des +Servers. Mehrere Tool-Aufrufe (Location anlegen, Trigger hinzufügen, WAV einbetten) +sammeln sich an; erst `sc_save_map` schreibt eine neue Datei. Die Basis-Karte bleibt +unverändert. `sc_reset_map` verwirft den Zwischenstand. + +### Das Kern-Tool `sc_add_trigger` + +Ein Trigger besteht aus **Bedingungen** (`conditions`, UND-verknüpft) und **Aktionen** +(`actions`), plus den **Spielern** (`players`), für die er läuft. Werte dürfen tolerant +angegeben werden: per Bezeichner (`TERRAN_MARINE`), Anzeigename (`Terran Marine`) oder +Zahl (`0`). + +**Unterstützte Bedingungen (`type`):** `always`, `never`, `elapsed_time`, +`countdown_timer`, `bring`, `command`, `kill`, `deaths`, `accumulate`, `opponents`. + +**Unterstützte Aktionen (`type`):** `display_text`, `set_mission_objectives`, +`play_wav`, `create_unit`, `kill_unit_at_location`, `remove_unit_at_location`, +`move_unit`, `give_units`, `set_resources`, `set_deaths`, `set_countdown_timer`, +`pause_timer`, `unpause_timer`, `run_ai_script`, `run_ai_script_at_location`, +`center_view`, `minimap_ping`, `set_alliance_status`, `victory`, `defeat`. + +Beispiel (Spielstart: Text + 4 Marines an der Location "Basis"): + +```json +{ + "map": "base-map.scx", + "players": ["PLAYER_1"], + "conditions": [{ "type": "always" }], + "actions": [ + { "type": "display_text", "text": "Mission 1 – Start" }, + { "type": "create_unit", "player": "PLAYER_1", "amount": 4, + "unit": "TERRAN_MARINE", "location": "Basis" } + ] +} +``` + +**Funksprüche / Voiceover:** Zuerst `sc_embed_wav` aufrufen (gibt einen `wav_path` +zurück), dann im Trigger `play_wav` (mit diesem `wav_path`) zusammen mit `display_text` +und optional `center_view` verwenden. + +--- + +## Selbsttest (beweist die ganze Pipeline) + +```bash +./run.sh selftest # im Container +# oder lokal: +python selftest.py +``` + +Der Test: Basis-Karte laden → Location anlegen → Trigger (Text + Einheiten bei Start) → +als neue `.scx` speichern → neu laden → Trigger + Location bestätigt. + +--- + +## Lokal testen (ohne Docker, stdio) + +```bash +python3 -m venv .venv && . .venv/bin/activate +pip install -r requirements.txt +SC_MAPS_DIR=./data/maps python -m starcraft_mcp.server # stdio-Transport +``` + +Für HTTP lokal: `SC_TRANSPORT=http SC_PORT=8000 SC_MAPS_DIR=./data/maps python -m starcraft_mcp.server` +→ Endpunkt `http://127.0.0.1:8000/mcp`. + +### Konfiguration (Umgebungsvariablen) + +| Variable | Standard | Bedeutung | +|----------|----------|-----------| +| `SC_TRANSPORT` | `stdio` | `http` für Streamable-HTTP-Betrieb | +| `SC_HOST` | `0.0.0.0` (HTTP) | Bind-Adresse | +| `SC_PORT` | `8000` | Port | +| `SC_MAPS_DIR` | `/data/maps` | Karten-/WAV-/Missions-Verzeichnis | + +--- + +## Gewählte Defaults + +- **Beispiel-Basis-Karte:** die MIT-lizenzierte `base-map.scx` aus dem RichChk-Repo + (256×256, Jungle). Liegt in `data/maps/`. +- **Locations** werden als Box um einen Mittelpunkt angelegt (Standard 96×96 px; + 32 px = 1 Kachel). +- **`preserve=true`** ist Standard bei `sc_add_trigger` (Trigger bleibt nach Auslösen + bestehen — passt für fast alle Kampagnen-Trigger). +- **RichChk-Logging** ist auf `CRITICAL` gesetzt, damit die Server-Ausgabe sauber bleibt. +- **Netzwerk:** Der Container wird **nicht** direkt nach außen exponiert; Caddy spricht + ihn intern über `sc-mcp:8000` an (sicherer). Zum reinen Lokaltest sind die `ports:` + in der `docker-compose.yml` auskommentiert vorbereitet. + +--- + +## Bekannte Einschränkungen / TODO + +- **Transmission mit Portrait (`Transmission`/`TalkingPortrait`):** RichChk hat in dieser + Version **kein** High-Level-Modell für diese Aktion. Funksprüche baust du deshalb aus + `play_wav` + `display_text` (+ optional `center_view`) zusammen. → TODO, sobald RichChk + es unterstützt. +- **Mission-Briefing (`sc_set_briefing`, MBRF):** RichChk bietet (noch) keinen + High-Level-Editor für die MBRF-Sektion. Für v1 bewusst weggelassen. → TODO. +- **Schalter (Switches) in Bedingungen/Aktionen:** für v1 nicht exponiert. → TODO. + +--- + +## Projektstruktur + +``` +starcraft_mcp/ + server.py FastMCP-Server mit allen sc_-Tools + workspace.py Karten-Sitzung im Speicher (Laden/Bearbeiten/Speichern, WAV-Einbettung) + triggers.py Pydantic-Schemas + Builder für Conditions/Actions (Herzstück-Doku) + enums.py tolerante Resolver (Einheiten, Spieler, Vergleiche, …) + richchk_logging.yaml setzt RichChk-Logging auf CRITICAL +selftest.py End-to-End-Selbsttest durch die echten Tools +data/maps/ Karten-Volume (Basis-Karte, WAVs, fertige Missionen) +Dockerfile, docker-compose.yml, Caddyfile, run.sh Betrieb +``` + +--- + +## Hinweis zur Erstellung + +Code und gesamte Pipeline wurden in einer Linux-Umgebung gebaut und **getestet** +(RichChk-Lese-/Schreib-Pipeline, alle 13 Tools, WAV-Einbettung, sowie der +Streamable-HTTP-Endpunkt per echtem MCP-Client-Handshake). Das tatsächliche Deployment +auf deinem Hetzner-VPS (Docker-Build, Caddy-Subdomain, DNS) führst du mit `./run.sh` +selbst aus — das konnte aus der Build-Umgebung heraus nicht für deinen VPS erfolgen. diff --git a/data/maps/.gitkeep b/data/maps/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/data/maps/base-map.scx b/data/maps/base-map.scx new file mode 100755 index 0000000000000000000000000000000000000000..5cef42373ef8408f4be56558270c5dc0cfaf4165 GIT binary patch literal 103346 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Ξ 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.0 diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..19a9173 --- /dev/null +++ b/run.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Ein-Befehl-Start/Neustart des StarCraft-Kampagnen-MCP-Servers. +# +# ./run.sh -> baut (falls noetig) und startet/neustartet den Container +# ./run.sh logs -> zeigt die Live-Logs +# ./run.sh selftest -> fuehrt den Selbsttest im Container aus +# ./run.sh stop -> stoppt den Container +set -euo pipefail +cd "$(dirname "$0")" + +# docker compose (v2) bevorzugen, sonst docker-compose (v1). +if docker compose version >/dev/null 2>&1; then + DC="docker compose" +else + DC="docker-compose" +fi + +cmd="${1:-up}" +case "$cmd" in + up|"") + $DC up -d --build + echo + echo "Server laeuft. Streamable-HTTP-Endpunkt intern: http://sc-mcp:8000/mcp" + echo "Oeffentlich (via Caddy): https://sc-mcp.pixel-by-design.de/mcp" + ;; + logs) + $DC logs -f sc-mcp + ;; + selftest) + $DC run --rm sc-mcp python selftest.py + ;; + stop|down) + $DC down + ;; + *) + echo "Unbekannter Befehl: $cmd" + echo "Nutze: ./run.sh [up|logs|selftest|stop]" + exit 1 + ;; +esac diff --git a/selftest.py b/selftest.py new file mode 100755 index 0000000..e06ec83 --- /dev/null +++ b/selftest.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Selbsttest: beweist die komplette Pipeline durch die echten MCP-Tools. + +Ablauf (Definition of Done): + 1. Basis-Karte aus dem Karten-Verzeichnis laden. + 2. Eine Location anlegen. + 3. Trigger hinzufuegen: bei Spielstart Text "Mission 1 - Start" einblenden UND + ein paar Einheiten an der Location erzeugen. + 4. Als neue .scx speichern. + 5. Neue Datei erneut laden und bestaetigen, dass Trigger + Location drin sind. + +Start: python selftest.py [basis-karte.scx] +Exit-Code 0 = bestanden. +""" + +from __future__ import annotations + +import asyncio +import json +import os +import shutil +import sys +import tempfile + + +def _data(result) -> dict: + """Hole das strukturierte Ergebnis aus einem FastMCP call_tool-Resultat.""" + # FastMCP gibt (content_blocks, structured_result) zurueck. + if isinstance(result, tuple) and len(result) == 2: + structured = result[1] + if isinstance(structured, dict): + return structured + # Fallback: erstes TextContent als JSON parsen. + blocks = result[0] if isinstance(result, tuple) else result + for b in blocks: + text = getattr(b, "text", None) + if text: + try: + return json.loads(text) + except json.JSONDecodeError: + return {"text": text} + return {} + + +async def run(base_map: str) -> bool: + workdir = tempfile.mkdtemp(prefix="sc_selftest_") + os.environ["SC_MAPS_DIR"] = workdir + shutil.copyfile(base_map, os.path.join(workdir, os.path.basename(base_map))) + base_name = os.path.basename(base_map) + + # Workspace-Modul liest SC_MAPS_DIR beim Import -> erst jetzt importieren. + from starcraft_mcp import workspace # noqa: E402 + from starcraft_mcp.server import mcp # noqa: E402 + + workspace.MAPS_DIR = workdir # falls Modul bereits importiert war + + print(f"[1] Basis-Karte: {base_name} (Verzeichnis: {workdir})") + res = _data(await mcp.call_tool("sc_list_maps", {})) + assert base_name in res["maps"], f"Basis-Karte nicht gelistet: {res}" + + print("[2] Location 'Basis' anlegen ...") + res = _data( + await mcp.call_tool( + "sc_create_location", + {"map": base_name, "name": "Basis", "center_x": 512, "center_y": 512}, + ) + ) + assert res.get("ok"), f"create_location fehlgeschlagen: {res}" + print(f" -> Index {res['index']}, Box {res['box']}") + + print("[3] Trigger hinzufuegen (Text + 4 Marines bei Spielstart) ...") + res = _data( + await mcp.call_tool( + "sc_add_trigger", + { + "map": base_name, + "players": ["PLAYER_1"], + "conditions": [{"type": "always"}], + "actions": [ + {"type": "display_text", "text": "Mission 1 - Start"}, + { + "type": "create_unit", + "player": "PLAYER_1", + "amount": 4, + "unit": "TERRAN_MARINE", + "location": "Basis", + }, + ], + }, + ) + ) + assert res.get("ok"), f"add_trigger fehlgeschlagen: {res}" + print(f" -> {res['summary']}") + + print("[4] Als 'mission1.scx' speichern ...") + res = _data( + await mcp.call_tool( + "sc_save_map", + {"map": base_name, "output_name": "mission1.scx", "overwrite": True}, + ) + ) + assert res.get("ok"), f"save_map fehlgeschlagen: {res}" + out_path = res["path"] + print(f" -> {out_path} ({os.path.getsize(out_path)} Bytes)") + + print("[5] Neue Datei unabhaengig neu laden und verifizieren ...") + from richchk.io.mpq.starcraft_mpq_io_helper import StarCraftMpqIoHelper + from richchk.io.richchk.query.chk_query_util import ChkQueryUtil + from richchk.model.richchk.mrgn.rich_mrgn_section import RichMrgnSection + from richchk.model.richchk.trig.rich_trig_section import RichTrigSection + + chk = StarCraftMpqIoHelper.create_mpq_io().read_chk_from_mpq(out_path) + mrgn = ChkQueryUtil.find_only_rich_section_in_chk(RichMrgnSection, chk) + trig = ChkQueryUtil.find_only_rich_section_in_chk(RichTrigSection, chk) + loc_names = [l._custom_location_name.value for l in mrgn.locations] + assert "Basis" in loc_names, f"Location 'Basis' fehlt nach Reload: {loc_names}" + assert len(trig.triggers) >= 1, "Kein Trigger nach Reload gefunden" + action_types = [type(a).__name__ for a in trig.triggers[-1].actions] + assert "DisplayTextMessageAction" in action_types, action_types + assert "CreateUnitAction" in action_types, action_types + print(" -> Location 'Basis' vorhanden: True") + print(f" -> Trigger-Anzahl: {len(trig.triggers)}") + print(" -> Aktionen im Trigger: DisplayText + CreateUnit bestaetigt") + + shutil.rmtree(workdir, ignore_errors=True) + return True + + +def _find_default_base_map() -> str: + here = os.path.dirname(os.path.abspath(__file__)) + candidate = os.path.join(here, "data", "maps", "base-map.scx") + return candidate + + +def main() -> int: + base_map = sys.argv[1] if len(sys.argv) > 1 else _find_default_base_map() + if not os.path.exists(base_map): + print(f"FEHLER: Basis-Karte nicht gefunden: {base_map}") + return 2 + try: + ok = asyncio.run(run(base_map)) + except AssertionError as exc: + print(f"\nSELBSTTEST FEHLGESCHLAGEN: {exc}") + return 1 + except Exception as exc: # noqa: BLE001 + print(f"\nSELBSTTEST FEHLER: {type(exc).__name__}: {exc}") + return 1 + if ok: + print("\n=== SELBSTTEST BESTANDEN ===") + return 0 + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/starcraft_mcp/__init__.py b/starcraft_mcp/__init__.py new file mode 100644 index 0000000..301821c --- /dev/null +++ b/starcraft_mcp/__init__.py @@ -0,0 +1,21 @@ +"""StarCraft-Kampagnen-MCP-Server ("Missions-Baumeister"). + +Ein MCP-Server, der StarCraft-Brood-War-Karten (.scm/.scx) liest und schreibt, +damit Claude aus einer Kreativ-Vorlage echte, spielbare Missionsdateien baut. +Die Karten-Manipulation laeuft komplett ueber die Bibliothek RichChk. +""" + +import os as _os + +# RichChk loggt sonst sehr ausfuehrlich (INFO/WARNING) nach stderr. Wir setzen das +# Log-Level per RichChk-Config-Datei auf CRITICAL, BEVOR irgendein RichChk-Logger +# erzeugt wird (RichChk liest die Datei beim ersten get_logger). Das haelt die Ausgabe +# sauber; unsere Tools melden Fehler ohnehin als Exceptions. +_os.environ.setdefault( + "io.sethmachine.richchk.config", + _os.path.join(_os.path.dirname(__file__), "richchk_logging.yaml"), +) + +__all__ = ["__version__"] + +__version__ = "0.1.0" diff --git a/starcraft_mcp/enums.py b/starcraft_mcp/enums.py new file mode 100644 index 0000000..344418f --- /dev/null +++ b/starcraft_mcp/enums.py @@ -0,0 +1,111 @@ +"""Flexible Resolver fuer RichChk-Enums. + +Claude (und Menschen) sollen Einheiten, Spieler, Vergleiche usw. tolerant angeben +koennen: per Bezeichner ("TERRAN_MARINE"), per Anzeigename ("Terran Marine") oder per +Zahl ("0"). Diese Helfer uebersetzen solche Eingaben in die echten RichChk-Enum-Member +und werfen bei Fehlern eine *hilfreiche* Fehlermeldung mit Beispielen. +""" + +from __future__ import annotations + +from typing import Type, TypeVar + +from richchk.model.richchk.forc.force_id import ForceId +from richchk.model.richchk.ownr.player_type import PlayerType +from richchk.model.richchk.richchk_enum import RichChkEnum +from richchk.model.richchk.side.player_race import PlayerRace +from richchk.model.richchk.trig.conditions.comparators.numeric_comparator import ( + NumericComparator, +) +from richchk.model.richchk.trig.enums.alliance_status import AllianceStatus +from richchk.model.richchk.trig.enums.amount_modifier import AmountModifier +from richchk.model.richchk.trig.enums.resource_type import ResourceType +from richchk.model.richchk.trig.enums.switch_state import SwitchState +from richchk.model.richchk.trig.player_id import PlayerId +from richchk.model.richchk.unis.unit_id import UnitId + +_E = TypeVar("_E", bound=RichChkEnum) + + +def _identifier(member: RichChkEnum) -> str: + """Der Python-Bezeichner eines Members, z.B. 'TERRAN_MARINE'.""" + return member._name_ # type: ignore[attr-defined] + + +def _build_index(enum_cls: Type[_E]) -> dict[str, _E]: + index: dict[str, _E] = {} + for member in enum_cls: # type: ignore[attr-defined] + index[_identifier(member).upper()] = member + index[member.name.upper()] = member # Anzeigename ("Terran Marine") + index[str(member.id)] = member # numerische ID + return index + + +def _examples(enum_cls: Type[_E], limit: int = 20) -> str: + names = sorted({_identifier(m) for m in enum_cls}) # type: ignore[attr-defined] + shown = ", ".join(names[:limit]) + if len(names) > limit: + shown += f", … ({len(names)} insgesamt)" + return shown + + +def resolve(enum_cls: Type[_E], value: str | int, what: str) -> _E: + """Loese eine tolerante Eingabe in ein RichChk-Enum-Member auf. + + Akzeptiert Bezeichner ('TERRAN_MARINE'), Anzeigename ('Terran Marine') oder ID (0). + """ + index = _build_index(enum_cls) + key = str(value).strip().upper() + if key in index: + return index[key] + raise ValueError( + f"Unbekannter Wert fuer {what}: {value!r}. " + f"Verfuegbar (Bezeichner): {_examples(enum_cls)}" + ) + + +# --- Convenience-Wrapper mit sprechenden Namen -------------------------------- + + +def resolve_unit(value: str | int) -> UnitId: + return resolve(UnitId, value, "Einheit (unit)") + + +def resolve_player(value: str | int) -> PlayerId: + return resolve(PlayerId, value, "Spieler/Gruppe (player)") + + +def resolve_comparator(value: str | int) -> NumericComparator: + return resolve(NumericComparator, value, "Vergleich (comparator)") + + +def resolve_modifier(value: str | int) -> AmountModifier: + return resolve(AmountModifier, value, "Mengen-Modifikator (modifier)") + + +def resolve_resource(value: str | int) -> ResourceType: + return resolve(ResourceType, value, "Ressource (resource)") + + +def resolve_alliance(value: str | int) -> AllianceStatus: + return resolve(AllianceStatus, value, "Allianz-Status (alliance_status)") + + +def resolve_switch_state(value: str | int) -> SwitchState: + return resolve(SwitchState, value, "Schalter-Zustand (switch_state)") + + +def resolve_player_type(value: str | int) -> PlayerType: + return resolve(PlayerType, value, "Spieler-Typ (type)") + + +def resolve_race(value: str | int) -> PlayerRace: + return resolve(PlayerRace, value, "Rasse (race)") + + +def resolve_force(value: str | int) -> ForceId: + return resolve(ForceId, value, "Force/Team (force)") + + +def list_identifiers(enum_cls: Type[_E]) -> list[str]: + return sorted({_identifier(m) for m in enum_cls}) # type: ignore[attr-defined] diff --git a/starcraft_mcp/richchk_logging.yaml b/starcraft_mcp/richchk_logging.yaml new file mode 100644 index 0000000..0e19f0f --- /dev/null +++ b/starcraft_mcp/richchk_logging.yaml @@ -0,0 +1,2 @@ +logging: + level: CRITICAL diff --git a/starcraft_mcp/server.py b/starcraft_mcp/server.py new file mode 100644 index 0000000..0ca0fe9 --- /dev/null +++ b/starcraft_mcp/server.py @@ -0,0 +1,515 @@ +"""FastMCP-Server: StarCraft-Kampagnen-MCP ("Missions-Baumeister"). + +Stellt `sc_`-Tools bereit, mit denen Claude StarCraft-Brood-War-Karten (.scm/.scx) +liest und schreibt. Die Karten-Manipulation laeuft komplett ueber RichChk. + +Start: + - stdio (lokaler Test): python -m starcraft_mcp.server + - Streamable HTTP (Betrieb): SC_TRANSPORT=http python -m starcraft_mcp.server +""" + +from __future__ import annotations + +import os +from typing import Optional + +from mcp.server.fastmcp import FastMCP +from mcp.types import ToolAnnotations +from richchk.editor.richchk.rich_forc_editor import RichForcEditor +from richchk.editor.richchk.rich_ownr_editor import RichOwnrEditor +from richchk.editor.richchk.rich_side_editor import RichSideEditor +from richchk.editor.richchk.rich_trig_editor import RichTrigEditor +from richchk.model.richchk.dim.rich_dim_section import RichDimSection +from richchk.model.richchk.era.rich_era_section import RichEraSection +from richchk.model.richchk.forc.rich_forc_section import RichForcSection +from richchk.model.richchk.ownr.rich_ownr_section import RichOwnrSection +from richchk.model.richchk.side.rich_side_section import RichSideSection +from richchk.model.richchk.trig.actions.preserve_trigger_action import PreserveTrigger +from richchk.model.richchk.trig.rich_trigger import RichTrigger +from pydantic import Field + +from . import enums, triggers, workspace +from .triggers import Action, Condition + +mcp = FastMCP( + "starcraft-campaign-mcp", + instructions=( + "Baue spielbare StarCraft-Brood-War-Missionen aus einer Basis-Karte. " + "Das Gelaende wird NICHT generiert - jede Mission startet von einer " + "vorhandenen Basis-Karte in /data/maps; nur Logik (Trigger), Texte, " + "Locations, Player-Setup und Sounds werden bearbeitet. Typischer Ablauf: " + "sc_list_maps -> sc_describe_map -> sc_create_location -> sc_add_trigger " + "-> sc_embed_wav -> sc_save_map." + ), +) + +_READONLY = ToolAnnotations(readOnlyHint=True, destructiveHint=False) +_EDIT = ToolAnnotations(readOnlyHint=False, destructiveHint=False) +_DESTRUCTIVE = ToolAnnotations(readOnlyHint=False, destructiveHint=True) + + +# --- Lese-Tools --------------------------------------------------------------- + + +@mcp.tool( + annotations=_READONLY, + description="Listet alle Basis-Karten und fertigen Missionen (.scm/.scx) im " + "Karten-Verzeichnis.", +) +def sc_list_maps() -> dict: + maps = workspace.list_maps() + return { + "maps_dir": workspace.MAPS_DIR, + "count": len(maps), + "maps": maps, + } + + +@mcp.tool( + annotations=_READONLY, + description="Liest eine Karte und gibt eine menschenlesbare Uebersicht: Tileset, " + "Groesse, Player-Setup (Typ/Rasse/Force), vorhandene Locations sowie Anzahl und " + "Klartext-Zusammenfassung der Trigger. Spiegelt den aktuellen Bearbeitungsstand, " + "falls die Karte bereits geoeffnet ist.", +) +def sc_describe_map( + map: str = Field(description="Dateiname der Karte in /data/maps."), +) -> dict: + ws = workspace.open_workspace(map) + dim = ws.section(RichDimSection) + era = ws.section(RichEraSection) + ownr = ws.section(RichOwnrSection) + side = ws.section(RichSideSection) + forc = ws.section(RichForcSection) + + players = [] + for i in range(8): + players.append( + { + "player": f"PLAYER_{i + 1}", + "type": ownr.player_types[i].name + if i < len(ownr.player_types) + else "?", + "race": side.player_races[i].name + if i < len(side.player_races) + else "?", + "force": forc.player_force_assignments[i].name + if i < len(forc.player_force_assignments) + else "?", + } + ) + + locations = [ + { + "name": loc._custom_location_name.value or f"(unbenannt #{loc.index})", + "index": loc.index, + "box": [loc._left_x1, loc._top_y1, loc._right_x2, loc._bottom_y2], + } + for loc in ws.mrgn.locations + ] + + trigger_summaries = [] + for i, tr in enumerate(ws.trig.triggers): + trigger_summaries.append(_summarize_trigger(i, tr)) + + return { + "map": ws.base_name, + "tileset": era.tileset.name, + "size": {"width": dim.width, "height": dim.height}, + "players": players, + "forces": [ + {"force": f"FORCE_{i + 1}", "name": f.name.value or f"Force {i + 1}"} + for i, f in enumerate(forc.forces) + ], + "locations": locations, + "trigger_count": len(ws.trig.triggers), + "triggers": trigger_summaries, + "pending_wav_embeds": [os.path.basename(w[0]) for w in ws.pending_wavs], + } + + +@mcp.tool( + annotations=_READONLY, + description="Listet alle Locations (MRGN) der Karte mit Index und Bounding-Box.", +) +def sc_list_locations( + map: str = Field(description="Dateiname der Karte in /data/maps."), +) -> dict: + ws = workspace.open_workspace(map) + return { + "map": ws.base_name, + "locations": [ + { + "name": loc._custom_location_name.value or f"(unbenannt #{loc.index})", + "index": loc.index, + "box": [loc._left_x1, loc._top_y1, loc._right_x2, loc._bottom_y2], + } + for loc in ws.mrgn.locations + ], + } + + +@mcp.tool( + annotations=_READONLY, + description="Listet alle Trigger der geoeffneten Karte mit Index, betroffenen " + "Spielern und einer Klartext-Zusammenfassung der Bedingungen und Aktionen.", +) +def sc_list_triggers( + map: str = Field(description="Dateiname der Karte in /data/maps."), +) -> dict: + ws = workspace.open_workspace(map) + return { + "map": ws.base_name, + "trigger_count": len(ws.trig.triggers), + "triggers": [_summarize_trigger(i, t) for i, t in enumerate(ws.trig.triggers)], + } + + +# --- Locations ---------------------------------------------------------------- + + +@mcp.tool( + annotations=_EDIT, + description="Legt eine neue Location (MRGN) an. Koordinaten in Pixeln (32 px = " + "1 Kachel). Es wird eine Box um den Mittelpunkt gebaut. Locations werden in " + "Triggern ueber ihren Namen referenziert.", +) +def sc_create_location( + map: str = Field(description="Dateiname der Karte in /data/maps."), + name: str = Field(description="Eindeutiger Name der Location, z.B. 'Basis'."), + center_x: int = Field(description="Mittelpunkt X in Pixeln."), + center_y: int = Field(description="Mittelpunkt Y in Pixeln."), + width: int = Field(default=96, description="Breite der Box in Pixeln (Standard 96)."), + height: int = Field( + default=96, description="Hoehe der Box in Pixeln (Standard 96)." + ), +) -> dict: + ws = workspace.open_workspace(map) + loc = ws.add_location(name, center_x, center_y, width, height) + return { + "ok": True, + "name": name, + "index": loc.index, + "box": [loc._left_x1, loc._top_y1, loc._right_x2, loc._bottom_y2], + "hinweis": "Aenderung ist im Speicher. Mit sc_save_map persistieren.", + } + + +@mcp.tool( + annotations=_EDIT, + description="Benennt eine existierende Location um.", +) +def sc_rename_location( + map: str = Field(description="Dateiname der Karte in /data/maps."), + old_name: str = Field(description="Aktueller Name der Location."), + new_name: str = Field(description="Neuer Name der Location."), +) -> dict: + ws = workspace.open_workspace(map) + loc = ws.rename_location(old_name, new_name) + return {"ok": True, "name": new_name, "index": loc.index} + + +# --- Player-Setup ------------------------------------------------------------- + + +@mcp.tool( + annotations=_EDIT, + description=( + "Setzt das Player-Setup: Owner-Typ (OWNR), Rasse (SIDE) und Force-Zuordnung " + "(FORC) je Spieler. Optional Force-Namen.\n" + "type: INACTIVE, COMPUTER_GAME, HUMAN_OCCUPIED, RESCUE_PASSIVE, COMPUTER, " + "HUMAN, NEUTRAL, CLOSED.\n" + "race: ZERG, TERRAN, PROTOSS, INDEPENDENT, NEUTRAL, USER_SELECT, RANDOM, " + "INACTIVE.\n" + "force: FORCE_1..FORCE_4." + ), +) +def sc_set_player_setup( + map: str = Field(description="Dateiname der Karte in /data/maps."), + players: list[dict] = Field( + description="Liste von Eintraegen je Spieler, z.B. " + '[{"player":"PLAYER_1","type":"HUMAN","race":"TERRAN","force":"FORCE_1"}]. ' + "Die Felder type/race/force sind jeweils optional." + ), + force_names: Optional[list[dict]] = Field( + default=None, + description='Optionale Force-Namen, z.B. [{"force":"FORCE_1","name":"Allianz"}].', + ), +) -> dict: + ws = workspace.open_workspace(map) + applied = [] + for entry in players: + player = enums.resolve_player(entry["player"]) + if entry.get("type") is not None: + ownr = ws.section(RichOwnrSection) + ws.replace( + RichOwnrEditor().set_player_type( + player, enums.resolve_player_type(entry["type"]), ownr + ) + ) + if entry.get("race") is not None: + side = ws.section(RichSideSection) + ws.replace( + RichSideEditor().set_player_race( + player, enums.resolve_race(entry["race"]), side + ) + ) + if entry.get("force") is not None: + forc = ws.section(RichForcSection) + ws.replace( + RichForcEditor().add_player_to_force( + player, enums.resolve_force(entry["force"]), forc + ) + ) + applied.append(entry["player"]) + + if force_names: + from richchk.model.richchk.forc.rich_force import RichForce + from richchk.model.richchk.str.rich_string import RichString + + for fn in force_names: + force = enums.resolve_force(fn["force"]) + forc = ws.section(RichForcSection) + existing = forc.forces[force.id] + ws.replace( + RichForcEditor().set_force( + force, + RichForce(_name=RichString(fn["name"]), _flags=existing.flags), + forc, + ) + ) + + return { + "ok": True, + "updated_players": applied, + "updated_forces": [f["force"] for f in force_names] if force_names else [], + "hinweis": "Aenderung ist im Speicher. Mit sc_save_map persistieren.", + } + + +# --- Trigger (Kern-Tool) ------------------------------------------------------ + + +@mcp.tool( + annotations=_EDIT, + description=triggers.__doc__ + + "\n\nFuegt EINEN Trigger hinzu. `players` ist die Liste der Spieler/Gruppen, " + "fuer die der Trigger laeuft (z.B. ['PLAYER_1'] oder ['ALL_PLAYERS']). Mit " + "`preserve=true` (Standard) bleibt der Trigger nach dem Ausloesen bestehen " + "(empfohlen fuer fast alle Kampagnen-Trigger). Locations werden ueber ihren " + "Namen referenziert und muessen vorher existieren.", +) +def sc_add_trigger( + map: str = Field(description="Dateiname der Karte in /data/maps."), + players: list[str] = Field( + description="Spieler/Gruppen, fuer die der Trigger laeuft, z.B. " + "['PLAYER_1'], ['ALL_PLAYERS'], ['FORCE_1']." + ), + conditions: list[Condition] = Field( + description="Liste der Bedingungen (UND-verknuepft). Leere Liste = 'always'." + ), + actions: list[Action] = Field(description="Liste der Aktionen in Reihenfolge."), + preserve: bool = Field( + default=True, + description="Trigger nach Ausloesen erhalten (Preserve Trigger). Standard true.", + ), +) -> dict: + ws = workspace.open_workspace(map) + + if not players: + raise ValueError("Mindestens ein Spieler/eine Gruppe in 'players' angeben.") + player_set = {enums.resolve_player(p) for p in players} + + rich_conditions = [ + triggers.build_condition(c, ws.resolve_location) for c in conditions + ] or [_always()] + rich_actions = [triggers.build_action(a, ws.resolve_location) for a in actions] + if preserve: + rich_actions.append(PreserveTrigger()) + if not rich_actions: + raise ValueError("Ein Trigger braucht mindestens eine Aktion.") + + trigger = RichTrigger( + _conditions=rich_conditions, + _actions=rich_actions, + _players=player_set, + ) + new_trig = RichTrigEditor.add_triggers([trigger], ws.trig) + ws.replace(new_trig) + + return { + "ok": True, + "trigger_index": len(ws.trig.triggers) - 1, + "summary": _summarize_trigger(len(ws.trig.triggers) - 1, trigger), + "hinweis": "Aenderung ist im Speicher. Mit sc_save_map persistieren.", + } + + +@mcp.tool( + annotations=_DESTRUCTIVE, + description="Entfernt EINEN Trigger anhand seines Index (siehe sc_list_triggers).", +) +def sc_remove_trigger( + map: str = Field(description="Dateiname der Karte in /data/maps."), + index: int = Field(description="0-basierter Index des Triggers."), +) -> dict: + from richchk.model.richchk.trig.rich_trig_section import RichTrigSection + + ws = workspace.open_workspace(map) + current = list(ws.trig.triggers) + if not 0 <= index < len(current): + raise ValueError( + f"Trigger-Index {index} ungueltig. Es gibt {len(current)} Trigger (0..{len(current) - 1})." + ) + removed = current.pop(index) + ws.replace(RichTrigSection(_triggers=current)) + return { + "ok": True, + "removed_index": index, + "removed_summary": _summarize_trigger(index, removed), + "remaining": len(current), + } + + +@mcp.tool( + annotations=_DESTRUCTIVE, + description="Entfernt ALLE Trigger der Karte. Vorsicht: nicht umkehrbar (bis zum " + "erneuten Laden der Basis-Karte).", +) +def sc_clear_triggers( + map: str = Field(description="Dateiname der Karte in /data/maps."), +) -> dict: + from richchk.model.richchk.trig.rich_trig_section import RichTrigSection + + ws = workspace.open_workspace(map) + count = len(ws.trig.triggers) + ws.replace(RichTrigSection(_triggers=[])) + return {"ok": True, "removed": count} + + +# --- Sounds ------------------------------------------------------------------- + + +@mcp.tool( + annotations=_EDIT, + description="Bettet eine WAV-Datei (Voiceover/Sound) in die Karte ein und macht sie " + "referenzierbar. Die WAV muss als Datei im Karten-Verzeichnis liegen. Die " + "Einbettung passiert beim Speichern (sc_save_map). Rueckgabe ist der in-MPQ-Pfad, " + "den du in play_wav-Aktionen (sc_add_trigger) als `wav_path` angibst.", +) +def sc_embed_wav( + map: str = Field(description="Dateiname der Karte in /data/maps."), + wav_filename: str = Field( + description="Dateiname der WAV im Karten-Verzeichnis, z.B. 'funk1.wav'." + ), +) -> dict: + ws = workspace.open_workspace(map) + wav_path = workspace.map_path(wav_filename) + if not os.path.exists(wav_path): + available = [ + f + for f in (os.listdir(workspace.MAPS_DIR) if os.path.isdir(workspace.MAPS_DIR) else []) + if f.lower().endswith(".wav") + ] + raise FileNotFoundError( + f"WAV {os.path.basename(wav_filename)!r} nicht gefunden in " + f"{workspace.MAPS_DIR}. Verfuegbar: {', '.join(available) or '(keine)'}." + ) + in_mpq = ws.embed_wav(wav_path) + return { + "ok": True, + "wav_path": in_mpq, + "hinweis": "Nutze diesen wav_path in play_wav-Aktionen. Die Datei wird beim " + "sc_save_map physisch in die Karte eingebettet.", + } + + +# --- Speichern ---------------------------------------------------------------- + + +@mcp.tool( + annotations=_EDIT, + description="Schreibt den aktuellen Bearbeitungsstand als neue Missionsdatei " + "(.scm/.scx) ins Karten-Verzeichnis. Die Basis-Karte dient als Vorlage und bleibt " + "unveraendert. Eingebettete WAVs werden hier hinzugefuegt.", +) +def sc_save_map( + map: str = Field(description="Dateiname der Basis-Karte in /data/maps."), + output_name: str = Field( + description="Dateiname der Ausgabe, z.B. 'mission1.scx'. Ohne Endung wird " + ".scx ergaenzt." + ), + overwrite: bool = Field( + default=False, description="Bestehende Datei ueberschreiben." + ), +) -> dict: + ws = workspace.open_workspace(map) + out_path = ws.save_as(output_name, overwrite) + return { + "ok": True, + "output": os.path.basename(out_path), + "path": out_path, + "embedded_wavs": [os.path.basename(w[0]) for w in ws.pending_wavs], + "triggers": len(ws.trig.triggers), + "locations": len(ws.mrgn.locations), + } + + +@mcp.tool( + annotations=_DESTRUCTIVE, + description="Verwirft alle nicht gespeicherten Aenderungen und laedt die Basis-" + "Karte frisch von der Platte.", +) +def sc_reset_map( + map: str = Field(description="Dateiname der Karte in /data/maps."), +) -> dict: + ws = workspace.open_workspace(map, fresh=True) + return { + "ok": True, + "map": ws.base_name, + "triggers": len(ws.trig.triggers), + "locations": len(ws.mrgn.locations), + } + + +# --- Helfer ------------------------------------------------------------------- + + +def _always(): + from richchk.model.richchk.trig.conditions.always_condition import AlwaysCondition + + return AlwaysCondition() + + +def _summarize_trigger(index: int, tr: RichTrigger) -> dict: + players = sorted(p.name for p in tr.players) + conds = [ + triggers.summarize_condition(c) + for c in tr.conditions + if type(c).__name__ not in ("NoConditionCondition",) + ] + acts = [ + triggers.summarize_action(a) + for a in tr.actions + if type(a).__name__ not in ("NoActionAction",) + ] + return { + "index": index, + "players": players, + "conditions": conds, + "actions": acts, + } + + +def main() -> None: + transport = os.environ.get("SC_TRANSPORT", "stdio").lower() + if transport in ("http", "streamable-http", "streamable_http"): + mcp.settings.host = os.environ.get("SC_HOST", "0.0.0.0") + mcp.settings.port = int(os.environ.get("SC_PORT", "8000")) + mcp.run(transport="streamable-http") + else: + mcp.run() + + +if __name__ == "__main__": + main() diff --git a/starcraft_mcp/triggers.py b/starcraft_mcp/triggers.py new file mode 100644 index 0000000..eaf91dd --- /dev/null +++ b/starcraft_mcp/triggers.py @@ -0,0 +1,452 @@ +"""Bausteine fuer Trigger: Pydantic-Eingaben -> echte RichChk-Objekte. + +Das ist das Herzstueck. Ein Trigger besteht aus *Bedingungen* (conditions) und +*Aktionen* (actions). Hier werden die tolerant getippten Eingaben von Claude in die +echten RichChk-Modelle uebersetzt, mit hilfreichen Fehlermeldungen. + +Unterstuetzte CONDITION-Typen (Feld `type`): + - always : immer wahr (Standard-Bedingung) + - never : nie wahr + - elapsed_time : seit Spielstart sind N Sekunden vergangen + Felder: seconds, comparator (AT_LEAST|AT_MOST|EXACTLY) + - countdown_timer : der Countdown-Timer vergleicht mit N Sekunden + Felder: seconds, comparator + - bring : Spieler hat N Einheiten an einer Location + Felder: player, comparator, amount, unit, location + - command : Spieler kommandiert N Einheiten (ganze Karte) + Felder: player, comparator, amount, unit + - kill : Spieler hat N Einheiten eines Typs getoetet + Felder: player, comparator, amount, unit + - deaths : Spieler hat N Todesfaelle eines Einheitentyps + Felder: player, comparator, amount, unit + - accumulate : Spieler hat N Ressourcen + Felder: player, comparator, amount, resource (ORE|GAS|ORE_AND_GAS) + - opponents : Spieler hat N verbleibende Gegner + Felder: player, comparator, amount + +Unterstuetzte ACTION-Typen (Feld `type`): + - display_text : Text einblenden. Felder: text + - set_mission_objectives : Missionsziele setzen. Felder: text + - play_wav : WAV abspielen. Felder: wav_path (in-MPQ-Pfad), duration_ms? + - create_unit : Einheiten erzeugen. Felder: player, amount, unit, location + - kill_unit_at_location : Einheiten toeten. Felder: player, amount, unit, location + - remove_unit_at_location: Einheiten entfernen. Felder: player, amount, unit, location + - move_unit : Einheiten bewegen. + Felder: player, amount, unit, location (Quelle), destination (Ziel) + - give_units : Einheiten uebergeben. + Felder: player (von), target_player (an), amount, unit, location + - set_resources : Ressourcen setzen. + Felder: player, modifier (SET_TO|ADD|SUBTRACT), amount, resource + - set_deaths : Todeszaehler setzen. + Felder: player, modifier, amount, unit + - set_countdown_timer : Countdown-Timer setzen. Felder: seconds, modifier + - pause_timer / unpause_timer : Countdown-Timer pausieren/fortsetzen + - run_ai_script : AI-Skript ausfuehren. Felder: ai_script (4-Zeichen-Code) + - run_ai_script_at_location : AI-Skript an Location. Felder: ai_script, location + - center_view : Kamera zentrieren. Felder: location + - minimap_ping : Minimap-Ping. Felder: location + - set_alliance_status : Allianz setzen. + Felder: player (Ziel-Gruppe), alliance_status (ENEMY|ALLY|ALLIED_VICTORY) + - victory : Mission gewonnen + - defeat : Mission verloren + +Hinweis "Transmission mit Portrait": RichChk hat (Stand dieser Version) kein +High-Level-Modell fuer die Transmission-/TalkingPortrait-Aktion. Baue einen +Funkspruch stattdessen aus `play_wav` + `display_text` (+ optional `center_view`) +zusammen. Siehe README. +""" + +from __future__ import annotations + +from typing import Callable, Optional + +from pydantic import BaseModel, Field +from richchk.model.richchk.mrgn.rich_location import RichLocation +from richchk.model.richchk.str.rich_string import RichString +from richchk.model.richchk.trig.actions.center_view_action import CenterViewAction +from richchk.model.richchk.trig.actions.create_unit_action import CreateUnitAction +from richchk.model.richchk.trig.actions.defeat_action import DefeatAction +from richchk.model.richchk.trig.actions.display_text_message_action import ( + DisplayTextMessageAction, +) +from richchk.model.richchk.trig.actions.give_unit_action import GiveUnitAction +from richchk.model.richchk.trig.actions.kill_unit_at_location_action import ( + KillUnitAtLocationAction, +) +from richchk.model.richchk.trig.actions.minimap_ping_action import MinimapPingAction +from richchk.model.richchk.trig.actions.move_unit_action import MoveUnitAction +from richchk.model.richchk.trig.actions.pause_countdown_timer import ( + PauseCountdownTimerAction, +) +from richchk.model.richchk.trig.actions.play_wav_action import PlayWavAction +from richchk.model.richchk.trig.actions.remove_unit_at_location_action import ( + RemoveUnitAtLocationAction, +) +from richchk.model.richchk.trig.actions.run_ai_script_action import RunAiScriptAction +from richchk.model.richchk.trig.actions.run_ai_script_at_location_action import ( + RunAiScriptAtLocationAction, +) +from richchk.model.richchk.trig.actions.set_countdown_timer import ( + SetCountdownTimerAction, +) +from richchk.model.richchk.trig.actions.set_alliance_status_action import ( + SetAllianceStatusAction, +) +from richchk.model.richchk.trig.actions.set_deaths_action import SetDeathsAction +from richchk.model.richchk.trig.actions.set_mission_objectives_action import ( + SetMissionObjectivesAction, +) +from richchk.model.richchk.trig.actions.set_resources_action import SetResourcesAction +from richchk.model.richchk.trig.actions.unpause_countdown_timer import ( + UnpauseCountdownTimerAction, +) +from richchk.model.richchk.trig.actions.victory_action import VictoryAction +from richchk.model.richchk.trig.conditions.accumulate_resources_condition import ( + AccumulateResourcesCondition, +) +from richchk.model.richchk.trig.conditions.always_condition import AlwaysCondition +from richchk.model.richchk.trig.conditions.bring_condition import BringCondition +from richchk.model.richchk.trig.conditions.command_condition import CommandCondition +from richchk.model.richchk.trig.conditions.countdown_timer_condition import ( + CountdownTimerCondition, +) +from richchk.model.richchk.trig.conditions.deaths_condition import DeathsCondition +from richchk.model.richchk.trig.conditions.elapsed_time_condition import ( + ElapsedTimeCondition, +) +from richchk.model.richchk.trig.conditions.kill_condition import KillCondition +from richchk.model.richchk.trig.conditions.never_condition import NeverCondition +from richchk.model.richchk.trig.conditions.opponents_remaining_condition import ( + OpponentsRemainingCondition, +) +from richchk.model.richchk.trig.enums.ai_script import AiScript, KnownAiScript + +from . import enums + +# Locations werden ueber einen Resolver (Name -> RichLocation) aufgeloest, den der +# Aufrufer injiziert, weil die verfuegbaren Locations vom Workspace abhaengen. +LocationResolver = Callable[[str], RichLocation] + + +class Condition(BaseModel): + """Eine einzelne Trigger-Bedingung. Pflichtfelder haengen von `type` ab.""" + + type: str = Field( + description="Bedingungstyp: always, never, elapsed_time, countdown_timer, " + "bring, command, kill, deaths, accumulate, opponents." + ) + player: Optional[str] = Field( + default=None, + description="Spieler/Gruppe, z.B. PLAYER_1, ALL_PLAYERS, FOES, CURRENT_PLAYER.", + ) + comparator: str = Field( + default="AT_LEAST", + description="Vergleich: AT_LEAST, AT_MOST oder EXACTLY.", + ) + amount: Optional[int] = Field(default=None, description="Menge/Anzahl/Ressourcen.") + unit: Optional[str] = Field( + default=None, description="Einheit, z.B. TERRAN_MARINE oder 'Terran Marine'." + ) + location: Optional[str] = Field( + default=None, description="Name einer existierenden Location." + ) + seconds: Optional[int] = Field(default=None, description="Sekunden (Zeit-Typen).") + resource: Optional[str] = Field( + default=None, description="Ressource: ORE, GAS oder ORE_AND_GAS." + ) + + +class Action(BaseModel): + """Eine einzelne Trigger-Aktion. Pflichtfelder haengen von `type` ab.""" + + type: str = Field( + description="Aktionstyp: display_text, set_mission_objectives, play_wav, " + "create_unit, kill_unit_at_location, remove_unit_at_location, move_unit, " + "give_units, set_resources, set_deaths, set_countdown_timer, pause_timer, " + "unpause_timer, run_ai_script, run_ai_script_at_location, center_view, " + "minimap_ping, set_alliance_status, victory, defeat." + ) + text: Optional[str] = Field(default=None, description="Anzuzeigender Text.") + wav_path: Optional[str] = Field( + default=None, + description="In-MPQ-Pfad der WAV (Rueckgabe von sc_embed_wav), z.B. " + "'staredit\\\\wav\\\\funk1.wav'.", + ) + duration_ms: Optional[int] = Field( + default=None, description="Dauer der WAV in Millisekunden (optional)." + ) + player: Optional[str] = Field( + default=None, description="Betroffener Spieler/Gruppe (Quelle bei give_units)." + ) + target_player: Optional[str] = Field( + default=None, description="Zielspieler bei give_units." + ) + amount: int = Field( + default=1, description="Anzahl Einheiten / Menge. 0 bedeutet 'Alle'." + ) + unit: Optional[str] = Field(default=None, description="Einheit, z.B. TERRAN_MARINE.") + location: Optional[str] = Field( + default=None, description="Name einer existierenden Location." + ) + destination: Optional[str] = Field( + default=None, description="Ziel-Location (bei move_unit)." + ) + seconds: Optional[int] = Field(default=None, description="Sekunden (Timer).") + modifier: str = Field( + default="SET_TO", description="Mengen-Modifikator: SET_TO, ADD oder SUBTRACT." + ) + resource: Optional[str] = Field( + default=None, description="Ressource: ORE, GAS oder ORE_AND_GAS." + ) + ai_script: Optional[str] = Field( + default=None, + description="AI-Skript: 4-Zeichen-Code (z.B. 'TMCx') oder bekannter Name " + "(z.B. JUNKYARD_DOG).", + ) + alliance_status: Optional[str] = Field( + default=None, description="Allianz: ENEMY, ALLY oder ALLIED_VICTORY." + ) + + +def _require(value, field: str, action_or_cond_type: str): + if value is None: + raise ValueError( + f"Feld '{field}' fehlt fuer Typ '{action_or_cond_type}'. " + f"Bitte '{field}' angeben." + ) + return value + + +def _resolve_ai_script(value: str) -> AiScript: + """Bekannter Name (JUNKYARD_DOG) oder roher 4-Zeichen-Code.""" + key = value.strip() + try: + return KnownAiScript[key.upper()].value + except KeyError: + pass + if len(key) == 4: + return AiScript(key, key) + known = ", ".join(sorted(k.name for k in KnownAiScript)) + raise ValueError( + f"Unbekanntes AI-Skript {value!r}. Gib einen 4-Zeichen-Code an " + f"(z.B. 'TMCx') oder einen bekannten Namen: {known}" + ) + + +def build_condition(cond: Condition, resolve_location: LocationResolver): + """Uebersetze eine Condition-Eingabe in ein RichChk-Conditionobjekt.""" + t = cond.type.strip().lower() + if t == "always": + return AlwaysCondition() + if t == "never": + return NeverCondition() + if t == "elapsed_time": + return ElapsedTimeCondition( + _seconds=_require(cond.seconds, "seconds", t), + _comparator=enums.resolve_comparator(cond.comparator), + ) + if t == "countdown_timer": + return CountdownTimerCondition( + _seconds=_require(cond.seconds, "seconds", t), + _comparator=enums.resolve_comparator(cond.comparator), + ) + if t == "bring": + return BringCondition( + _group=enums.resolve_player(_require(cond.player, "player", t)), + _comparator=enums.resolve_comparator(cond.comparator), + _amount=_require(cond.amount, "amount", t), + _unit=enums.resolve_unit(_require(cond.unit, "unit", t)), + _location=resolve_location(_require(cond.location, "location", t)), + ) + if t == "command": + return CommandCondition( + _group=enums.resolve_player(_require(cond.player, "player", t)), + _comparator=enums.resolve_comparator(cond.comparator), + _amount=_require(cond.amount, "amount", t), + _unit=enums.resolve_unit(_require(cond.unit, "unit", t)), + ) + if t == "kill": + return KillCondition( + _group=enums.resolve_player(_require(cond.player, "player", t)), + _comparator=enums.resolve_comparator(cond.comparator), + _amount=_require(cond.amount, "amount", t), + _unit=enums.resolve_unit(_require(cond.unit, "unit", t)), + ) + if t == "deaths": + return DeathsCondition( + _group=enums.resolve_player(_require(cond.player, "player", t)), + _comparator=enums.resolve_comparator(cond.comparator), + _amount=_require(cond.amount, "amount", t), + _unit=enums.resolve_unit(_require(cond.unit, "unit", t)), + ) + if t == "accumulate": + return AccumulateResourcesCondition( + _group=enums.resolve_player(_require(cond.player, "player", t)), + _comparator=enums.resolve_comparator(cond.comparator), + _amount=_require(cond.amount, "amount", t), + _resource=enums.resolve_resource(_require(cond.resource, "resource", t)), + ) + if t == "opponents": + return OpponentsRemainingCondition( + _group=enums.resolve_player(_require(cond.player, "player", t)), + _amount=_require(cond.amount, "amount", t), + _comparator=enums.resolve_comparator(cond.comparator), + ) + raise ValueError( + f"Unbekannter Bedingungstyp: {cond.type!r}. Erlaubt: always, never, " + "elapsed_time, countdown_timer, bring, command, kill, deaths, accumulate, " + "opponents." + ) + + +def build_action(act: Action, resolve_location: LocationResolver): + """Uebersetze eine Action-Eingabe in ein RichChk-Actionobjekt.""" + t = act.type.strip().lower() + if t == "display_text": + return DisplayTextMessageAction(RichString(_require(act.text, "text", t))) + if t == "set_mission_objectives": + return SetMissionObjectivesAction(RichString(_require(act.text, "text", t))) + if t == "play_wav": + return PlayWavAction( + _path_to_wav_in_mpq=_require(act.wav_path, "wav_path", t), + _duration_ms=act.duration_ms, + ) + if t == "create_unit": + return CreateUnitAction( + enums.resolve_player(_require(act.player, "player", t)), + act.amount, + enums.resolve_unit(_require(act.unit, "unit", t)), + resolve_location(_require(act.location, "location", t)), + ) + if t == "kill_unit_at_location": + return KillUnitAtLocationAction( + enums.resolve_player(_require(act.player, "player", t)), + act.amount, + enums.resolve_unit(_require(act.unit, "unit", t)), + resolve_location(_require(act.location, "location", t)), + ) + if t == "remove_unit_at_location": + return RemoveUnitAtLocationAction( + enums.resolve_player(_require(act.player, "player", t)), + act.amount, + enums.resolve_unit(_require(act.unit, "unit", t)), + resolve_location(_require(act.location, "location", t)), + ) + if t == "move_unit": + return MoveUnitAction( + enums.resolve_unit(_require(act.unit, "unit", t)), + enums.resolve_player(_require(act.player, "player", t)), + act.amount, + resolve_location(_require(act.location, "location", t)), + resolve_location(_require(act.destination, "destination", t)), + ) + if t == "give_units": + return GiveUnitAction( + enums.resolve_player(_require(act.player, "player", t)), + enums.resolve_player(_require(act.target_player, "target_player", t)), + act.amount, + enums.resolve_unit(_require(act.unit, "unit", t)), + resolve_location(_require(act.location, "location", t)), + ) + if t == "set_resources": + return SetResourcesAction( + enums.resolve_player(_require(act.player, "player", t)), + enums.resolve_modifier(act.modifier), + _require(act.amount, "amount", t), + enums.resolve_resource(_require(act.resource, "resource", t)), + ) + if t == "set_deaths": + return SetDeathsAction( + enums.resolve_player(_require(act.player, "player", t)), + enums.resolve_unit(_require(act.unit, "unit", t)), + _require(act.amount, "amount", t), + enums.resolve_modifier(act.modifier), + ) + if t == "set_countdown_timer": + return SetCountdownTimerAction( + _require(act.seconds, "seconds", t), + enums.resolve_modifier(act.modifier), + ) + if t == "pause_timer": + return PauseCountdownTimerAction() + if t == "unpause_timer": + return UnpauseCountdownTimerAction() + if t == "run_ai_script": + return RunAiScriptAction(_resolve_ai_script(_require(act.ai_script, "ai_script", t))) + if t == "run_ai_script_at_location": + return RunAiScriptAtLocationAction( + _resolve_ai_script(_require(act.ai_script, "ai_script", t)), + resolve_location(_require(act.location, "location", t)), + ) + if t == "center_view": + return CenterViewAction(resolve_location(_require(act.location, "location", t))) + if t == "minimap_ping": + return MinimapPingAction(resolve_location(_require(act.location, "location", t))) + if t == "set_alliance_status": + return SetAllianceStatusAction( + enums.resolve_player(_require(act.player, "player", t)), + enums.resolve_alliance(_require(act.alliance_status, "alliance_status", t)), + ) + if t == "victory": + return VictoryAction() + if t == "defeat": + return DefeatAction() + raise ValueError( + f"Unbekannter Aktionstyp: {act.type!r}. Siehe Tool-Beschreibung fuer die Liste." + ) + + +# --- Klartext-Zusammenfassung fuer describe_map / list_triggers ---------------- + + +def summarize_condition(cond) -> str: + name = type(cond).__name__.replace("Condition", "") + parts = [name] + for attr, label in ( + ("group", "player"), + ("comparator", "cmp"), + ("amount", "n"), + ("unit", "unit"), + ("seconds", "s"), + ("resource", "res"), + ("location", "loc"), + ): + if hasattr(cond, attr): + val = getattr(cond, attr) + parts.append(f"{label}={_pretty(val)}") + return " ".join(parts) + + +def summarize_action(act) -> str: + name = type(act).__name__.replace("Action", "") + parts = [name] + for attr, label in ( + ("group", "player"), + ("from_group", "from"), + ("to_group", "to"), + ("amount", "n"), + ("unit", "unit"), + ("location", "loc"), + ("destination_location", "dst"), + ("message", "text"), + ("path_to_wav_in_mpq", "wav"), + ("seconds", "s"), + ("resource", "res"), + ("alliance_status", "ally"), + ): + if hasattr(act, attr): + parts.append(f"{label}={_pretty(getattr(act, attr))}") + return " ".join(parts) + + +def _pretty(val) -> str: + if isinstance(val, RichString): + return repr(val.value) + if isinstance(val, RichLocation): + return repr(val._custom_location_name.value or f"#{val.index}") + if hasattr(val, "name"): + try: + return str(val.name) + except Exception: + pass + return str(val) diff --git a/starcraft_mcp/workspace.py b/starcraft_mcp/workspace.py new file mode 100644 index 0000000..da5bd58 --- /dev/null +++ b/starcraft_mcp/workspace.py @@ -0,0 +1,256 @@ +"""Arbeits-Session pro Basis-Karte. + +MCP-Tool-Aufrufe sind einzeln, aber das Bauen einer Mission besteht aus vielen +Schritten (Location anlegen, mehrere Trigger, WAV einbetten, speichern). Diese Klasse +haelt den bearbeiteten RichChk-Zustand einer Karte im Speicher zusammen mit einer +Namens-Registry fuer Locations und den noch einzubettenden WAV-Dateien. + +Workspaces leben prozessweit (Modul-Singleton-Dict), passend zum Streamable-HTTP- +Server, der ein einzelner Prozess ist. +""" + +from __future__ import annotations + +import os +import threading +from dataclasses import dataclass, field + +from richchk.editor.richchk.rich_chk_editor import RichChkEditor +from richchk.editor.richchk.rich_mrgn_editor import RichMrgnEditor +from richchk.io.mpq.starcraft_mpq_io_helper import StarCraftMpqIoHelper +from richchk.io.richchk.query.chk_query_util import ChkQueryUtil +from richchk.model.richchk.mrgn.rich_location import RichLocation +from richchk.model.richchk.mrgn.rich_mrgn_section import RichMrgnSection +from richchk.model.richchk.rich_chk import RichChk +from richchk.model.richchk.str.rich_string import RichString +from richchk.model.richchk.trig.rich_trig_section import RichTrigSection + +# Verzeichnis mit Basis-Karten, WAVs und fertigen Missionen (gemountetes Volume). +MAPS_DIR = os.environ.get("SC_MAPS_DIR", "/data/maps") + +_MPQ_IO = None +_MPQ_LOCK = threading.Lock() + + +def get_mpq_io(): + """Lazy-Singleton fuer den (relativ teuren) StormLib-Loader.""" + global _MPQ_IO + with _MPQ_LOCK: + if _MPQ_IO is None: + _MPQ_IO = StarCraftMpqIoHelper.create_mpq_io() + return _MPQ_IO + + +def map_path(name: str) -> str: + """Absoluter Pfad einer Karte/WAV im Karten-Verzeichnis (kein Ausbrechen).""" + base = os.path.basename(name) + return os.path.join(MAPS_DIR, base) + + +@dataclass +class Workspace: + """Bearbeitbarer Zustand genau einer Basis-Karte.""" + + base_name: str + base_path: str + chk: RichChk + locations: dict[str, RichLocation] = field(default_factory=dict) + # noch physisch einzubettende WAVs: (Disk-Pfad, In-MPQ-Pfad) + pending_wavs: list[tuple[str, str]] = field(default_factory=list) + + # --- Sektion-Helfer ------------------------------------------------------- + + def section(self, section_type): + return ChkQueryUtil.find_only_rich_section_in_chk(section_type, self.chk) + + def replace(self, new_section) -> None: + self.chk = RichChkEditor().replace_chk_section(new_section, self.chk) + + @property + def mrgn(self) -> RichMrgnSection: + return self.section(RichMrgnSection) + + @property + def trig(self) -> RichTrigSection: + return self.section(RichTrigSection) + + # --- Locations ------------------------------------------------------------ + + def reload_location_registry(self) -> None: + self.locations = {} + for loc in self.mrgn.locations: + name = loc._custom_location_name.value + if name: + self.locations[name] = loc + + def resolve_location(self, name: str) -> RichLocation: + if name in self.locations: + return self.locations[name] + available = ", ".join(sorted(self.locations)) or "(keine)" + raise ValueError( + f"Location {name!r} existiert nicht. Verfuegbar: {available}. " + f"Lege sie zuerst mit sc_create_location an." + ) + + def add_location( + self, + name: str, + center_x: int, + center_y: int, + width: int, + height: int, + ) -> RichLocation: + if name in self.locations: + raise ValueError( + f"Location {name!r} existiert bereits. Nutze sc_rename_location oder " + f"einen anderen Namen." + ) + half_w, half_h = max(1, width // 2), max(1, height // 2) + loc = RichLocation( + _left_x1=max(0, center_x - half_w), + _top_y1=max(0, center_y - half_h), + _right_x2=center_x + half_w, + _bottom_y2=center_y + half_h, + _custom_location_name=RichString(name), + ) + new_mrgn, _ = RichMrgnEditor().add_locations([loc], self.mrgn) + self.replace(new_mrgn) + # Das allokierte Objekt (mit Index) aus der neuen Sektion ziehen. + allocated = next( + l for l in new_mrgn.locations if l._custom_location_name.value == name + ) + self.locations[name] = allocated + return allocated + + def rename_location(self, old_name: str, new_name: str) -> RichLocation: + if old_name not in self.locations: + available = ", ".join(sorted(self.locations)) or "(keine)" + raise ValueError( + f"Location {old_name!r} existiert nicht. Verfuegbar: {available}." + ) + if new_name in self.locations: + raise ValueError(f"Location {new_name!r} existiert bereits.") + old = self.locations[old_name] + renamed = RichLocation( + _left_x1=old._left_x1, + _top_y1=old._top_y1, + _right_x2=old._right_x2, + _bottom_y2=old._bottom_y2, + _custom_location_name=RichString(new_name), + _index=old._index, + _low_elevation=old._low_elevation, + _medium_elevation=old._medium_elevation, + _high_elevation=old._high_elevation, + _low_air=old._low_air, + _medium_air=old._medium_air, + _high_air=old._high_air, + ) + new_locations = [ + renamed if l._index == old._index else l for l in self.mrgn.locations + ] + self.replace(RichMrgnSection(_locations=new_locations)) + del self.locations[old_name] + self.locations[new_name] = renamed + return renamed + + # --- Sounds --------------------------------------------------------------- + + def embed_wav(self, wav_disk_path: str) -> str: + """Registriere eine WAV im WAV-Section (damit der String beim Encode bekannt + ist) und merke die Datei zur physischen Einbettung beim Speichern vor. + + :return: der In-MPQ-Pfad, der in play_wav-Aktionen referenziert wird. + """ + from richchk.editor.richchk.rich_wav_editor import RichWavEditor + from richchk.model.richchk.wav.rich_wav_section import RichWavSection + + in_mpq = "staredit\\wav\\" + os.path.basename(wav_disk_path) + wav_section = self.section(RichWavSection) + self.replace(RichWavEditor().add_wav_files([in_mpq], wav_section)) + if not any(p[1] == in_mpq for p in self.pending_wavs): + self.pending_wavs.append((wav_disk_path, in_mpq)) + return in_mpq + + # --- Speichern ------------------------------------------------------------ + + def save_as(self, output_name: str, overwrite: bool) -> str: + out_path = map_path(output_name) + if not (out_path.lower().endswith(".scx") or out_path.lower().endswith(".scm")): + out_path += ".scx" + if os.path.exists(out_path) and not overwrite: + raise FileExistsError( + f"Datei existiert bereits: {os.path.basename(out_path)}. " + f"Setze overwrite=true zum Ueberschreiben." + ) + mpq_io = get_mpq_io() + # CHK (inkl. aller Edits; die WAV-Eintraege wurden bei sc_embed_wav registriert) + # in die neue MPQ schreiben. + mpq_io.save_chk_to_mpq( + self.chk, self.base_path, out_path, overwrite_existing=True + ) + # Danach die physischen WAV-Dateien in die fertige MPQ kopieren. + if self.pending_wavs: + from richchk.model.mpq.stormlib.stormlib_archive_mode import ( + StormLibArchiveMode, + ) + + sw = mpq_io._stormlib_wrapper + handle = sw.open_archive(out_path, StormLibArchiveMode.STORMLIB_WRITE_ONLY) + try: + for disk_path, in_mpq in self.pending_wavs: + sw.add_file( + handle, + infile=disk_path, + path_to_file_in_archive=in_mpq, + overwrite_existing=True, + ) + sw.compact_archive(handle) + finally: + sw.close_archive(handle) + return out_path + + +# --- Workspace-Registry ------------------------------------------------------- + +_WORKSPACES: dict[str, Workspace] = {} +_WS_LOCK = threading.Lock() + + +def open_workspace(map_name: str, fresh: bool = False) -> Workspace: + """Lade-oder-hole den Workspace einer Basis-Karte. + + :param map_name: Dateiname der Basis-Karte in MAPS_DIR. + :param fresh: True erzwingt ein Neuladen von der Platte (verwirft Edits). + """ + base = os.path.basename(map_name) + with _WS_LOCK: + if not fresh and base in _WORKSPACES: + return _WORKSPACES[base] + path = map_path(base) + if not os.path.exists(path): + available = ", ".join(list_maps()) or "(keine)" + raise FileNotFoundError( + f"Karte {base!r} nicht gefunden in {MAPS_DIR}. Verfuegbar: {available}." + ) + chk = get_mpq_io().read_chk_from_mpq(path) + ws = Workspace(base_name=base, base_path=path, chk=chk) + ws.reload_location_registry() + with _WS_LOCK: + _WORKSPACES[base] = ws + return ws + + +def discard_workspace(map_name: str) -> bool: + base = os.path.basename(map_name) + with _WS_LOCK: + return _WORKSPACES.pop(base, None) is not None + + +def list_maps() -> list[str]: + if not os.path.isdir(MAPS_DIR): + return [] + return sorted( + f + for f in os.listdir(MAPS_DIR) + if f.lower().endswith((".scx", ".scm")) + )