feat: Terrain-Template-Auswahl (sc_list_templates / sc_new_from_template)
Aus vorgefertigten Basis-Karten (data/maps/templates/) eine Arbeits-Basiskarte erzeugen, statt jedes Terrain manuell zu bauen. workspace.py: TEMPLATES_DIR + list_templates/read_terrain_meta/new_from_template. tools/terrain_roundtrip_test.py belegt section-stabilen RichChk-Durchlauf; tools/template_tools_test.py testet die neuen Tools end-to-end. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4075cf4ebd
commit
aba9759400
6 changed files with 390 additions and 1 deletions
|
|
@ -39,7 +39,8 @@ mcp = FastMCP(
|
|||
"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."
|
||||
"-> sc_embed_wav -> sc_save_map. Alternativ als Start: sc_list_templates -> "
|
||||
"sc_new_from_template, um aus einer vorgefertigten Karte eine Basis zu erzeugen."
|
||||
),
|
||||
)
|
||||
|
||||
|
|
@ -165,6 +166,71 @@ def sc_list_triggers(
|
|||
}
|
||||
|
||||
|
||||
# --- Terrain-Templates --------------------------------------------------------
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
annotations=_READONLY,
|
||||
description="Listet verfuegbare Terrain-Templates (vorgefertigte Basis-Karten) im "
|
||||
"Template-Verzeichnis, jeweils mit Tileset und Groesse. Templates sind fertige "
|
||||
".scx/.scm-Karten (in SCMDraft gebaut oder fertige Maps), aus denen neue Missionen "
|
||||
"starten - so entfaellt das manuelle Bauen einer Basis-Karte pro Mission. Lege "
|
||||
"Templates in SC_TEMPLATES_DIR ab (Standard: <MAPS_DIR>/templates).",
|
||||
)
|
||||
def sc_list_templates() -> dict:
|
||||
names = workspace.list_templates()
|
||||
templates = []
|
||||
for n in names:
|
||||
try:
|
||||
meta = workspace.read_terrain_meta(workspace.template_path(n))
|
||||
except Exception as e: # defektes/fremdes File darf den Call nicht kippen
|
||||
meta = {"error": str(e)}
|
||||
templates.append({"name": n, **meta})
|
||||
return {
|
||||
"templates_dir": workspace.TEMPLATES_DIR,
|
||||
"count": len(templates),
|
||||
"templates": templates,
|
||||
"hinweis": (
|
||||
"Mit sc_new_from_template eine Arbeitskopie als neue Basis-Karte anlegen."
|
||||
if templates
|
||||
else "Keine Templates gefunden. Lege fertige .scx/.scm-Karten in "
|
||||
f"{workspace.TEMPLATES_DIR} ab, z.B. badlands_128.scx."
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool(
|
||||
annotations=_EDIT,
|
||||
description="Erstellt aus einem Terrain-Template eine neue Arbeits-Basiskarte im "
|
||||
"Karten-Verzeichnis (reine Datei-Kopie - das Terrain inkl. ISOM bleibt unveraendert). "
|
||||
"Danach wie gewohnt mit sc_describe_map / sc_create_location / sc_add_trigger "
|
||||
"bearbeiten und mit sc_save_map als Mission speichern.",
|
||||
)
|
||||
def sc_new_from_template(
|
||||
template: str = Field(
|
||||
description="Dateiname des Templates (siehe sc_list_templates)."
|
||||
),
|
||||
output_name: str = Field(
|
||||
description="Name der neuen Basis-Karte in /data/maps, z.B. "
|
||||
"'mission01_base.scx'. Ohne Endung wird die des Templates uebernommen."
|
||||
),
|
||||
overwrite: bool = Field(
|
||||
default=False, description="Bestehende Datei ueberschreiben."
|
||||
),
|
||||
) -> dict:
|
||||
out_path = workspace.new_from_template(template, output_name, overwrite)
|
||||
meta = workspace.read_terrain_meta(out_path)
|
||||
return {
|
||||
"ok": True,
|
||||
"map": os.path.basename(out_path),
|
||||
"from_template": os.path.basename(template),
|
||||
"tileset": meta.get("tileset"),
|
||||
"size": {"width": meta.get("width"), "height": meta.get("height")},
|
||||
"hinweis": "Neue Basis-Karte angelegt. Jetzt mit sc_describe_map oeffnen, "
|
||||
"Logik hinzufuegen und mit sc_save_map als Mission speichern.",
|
||||
}
|
||||
|
||||
|
||||
# --- Locations ----------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ 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")
|
||||
|
||||
# Verzeichnis mit Terrain-Templates (vorgefertigte Basis-Karten). Aus einer Vorlage
|
||||
# wird per new_from_template eine neue Arbeits-Basiskarte erzeugt. Standard: ein
|
||||
# Unterordner des Karten-Verzeichnisses (liegt damit im selben gemounteten Volume).
|
||||
TEMPLATES_DIR = os.environ.get(
|
||||
"SC_TEMPLATES_DIR", os.path.join(MAPS_DIR, "templates")
|
||||
)
|
||||
|
||||
_MPQ_IO = None
|
||||
_MPQ_LOCK = threading.Lock()
|
||||
|
||||
|
|
@ -254,3 +261,68 @@ def list_maps() -> list[str]:
|
|||
for f in os.listdir(MAPS_DIR)
|
||||
if f.lower().endswith((".scx", ".scm"))
|
||||
)
|
||||
|
||||
|
||||
# --- Terrain-Templates --------------------------------------------------------
|
||||
|
||||
|
||||
def template_path(name: str) -> str:
|
||||
"""Absoluter Pfad eines Templates (kein Ausbrechen aus TEMPLATES_DIR)."""
|
||||
return os.path.join(TEMPLATES_DIR, os.path.basename(name))
|
||||
|
||||
|
||||
def list_templates() -> list[str]:
|
||||
if not os.path.isdir(TEMPLATES_DIR):
|
||||
return []
|
||||
return sorted(
|
||||
f
|
||||
for f in os.listdir(TEMPLATES_DIR)
|
||||
if f.lower().endswith((".scx", ".scm"))
|
||||
)
|
||||
|
||||
|
||||
def read_terrain_meta(path: str) -> dict:
|
||||
"""Liest Tileset + Groesse einer Karte (leichtgewichtig, ohne Workspace-Cache)."""
|
||||
from richchk.model.richchk.dim.rich_dim_section import RichDimSection
|
||||
from richchk.model.richchk.era.rich_era_section import RichEraSection
|
||||
|
||||
chk = get_mpq_io().read_chk_from_mpq(path)
|
||||
dim = ChkQueryUtil.find_only_rich_section_in_chk(RichDimSection, chk)
|
||||
era = ChkQueryUtil.find_only_rich_section_in_chk(RichEraSection, chk)
|
||||
return {"tileset": era.tileset.name, "width": dim.width, "height": dim.height}
|
||||
|
||||
|
||||
def new_from_template(template_name: str, output_name: str, overwrite: bool) -> str:
|
||||
"""Kopiert ein Template als neue Arbeits-Basiskarte in MAPS_DIR.
|
||||
|
||||
Reine Datei-Kopie: das Terrain (inkl. ISOM) bleibt unangetastet. Die Kopie wird
|
||||
danach wie jede Basis-Karte mit den sc_-Tools bearbeitet und per sc_save_map als
|
||||
Mission gespeichert.
|
||||
|
||||
:return: absoluter Pfad der neuen Basis-Karte.
|
||||
"""
|
||||
import shutil
|
||||
|
||||
src = template_path(template_name)
|
||||
if not os.path.exists(src):
|
||||
available = ", ".join(list_templates()) or "(keine)"
|
||||
raise FileNotFoundError(
|
||||
f"Template {os.path.basename(template_name)!r} nicht gefunden in "
|
||||
f"{TEMPLATES_DIR}. Verfuegbar: {available}."
|
||||
)
|
||||
out_path = map_path(output_name)
|
||||
if not (out_path.lower().endswith(".scx") or out_path.lower().endswith(".scm")):
|
||||
# Endung des Templates uebernehmen, statt blind .scx anzuhaengen.
|
||||
out_path += os.path.splitext(src)[1]
|
||||
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."
|
||||
)
|
||||
if os.path.abspath(src) == os.path.abspath(out_path):
|
||||
raise ValueError("Template und Ziel sind dieselbe Datei.")
|
||||
shutil.copyfile(src, out_path)
|
||||
# Falls eine gleichnamige Karte bereits im Speicher gecacht war: verwerfen,
|
||||
# damit der naechste Zugriff die frische Kopie laedt.
|
||||
discard_workspace(os.path.basename(out_path))
|
||||
return out_path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue