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
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -10,3 +10,8 @@ __pycache__/
|
||||||
data/maps/*
|
data/maps/*
|
||||||
!data/maps/base-map.scx
|
!data/maps/base-map.scx
|
||||||
!data/maps/.gitkeep
|
!data/maps/.gitkeep
|
||||||
|
# Template-Ordner: Struktur + README einchecken, die Template-Maps selbst aber nicht
|
||||||
|
# (koennen gross / lizenzbehaftet sein).
|
||||||
|
!data/maps/templates/
|
||||||
|
data/maps/templates/*
|
||||||
|
!data/maps/templates/README.md
|
||||||
|
|
|
||||||
32
data/maps/templates/README.md
Normal file
32
data/maps/templates/README.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Terrain-Templates
|
||||||
|
|
||||||
|
Hier liegen **vorgefertigte Basis-Karten** (`.scx`/`.scm`), aus denen neue Missionen
|
||||||
|
starten. Eine Vorlage = fertiges Terrain (Größe, Tileset, ISOM-Blending, ggf.
|
||||||
|
Start-Doodads). Der MCP **erzeugt kein Terrain** — er kopiert eine Vorlage und fügt
|
||||||
|
nur die Logik (Trigger/Locations/Units/Sound) hinzu.
|
||||||
|
|
||||||
|
## Woher kommen Templates?
|
||||||
|
|
||||||
|
- **Selbst gebaut** in SCMDraft 2 (`http://www.stormcoast-fortress.net/`) — sauberes,
|
||||||
|
editor-qualitatives Terrain. Empfohlen.
|
||||||
|
- **Fertige Maps** (z. B. Melee-Karten) als Ausgangspunkt. Achte auf Lizenz/Urheberrecht,
|
||||||
|
bevor du etwas weitergibst.
|
||||||
|
|
||||||
|
## Benennung (Empfehlung)
|
||||||
|
|
||||||
|
`<tileset>_<breite>.scx`, z. B. `badlands_128.scx`, `jungle_256.scx`.
|
||||||
|
Tileset/Größe werden ohnehin automatisch aus der Datei gelesen (`sc_list_templates`).
|
||||||
|
|
||||||
|
## Nutzung
|
||||||
|
|
||||||
|
1. `sc_list_templates()` → zeigt alle Vorlagen mit Tileset + Größe.
|
||||||
|
2. `sc_new_from_template(template="badlands_128.scx", output_name="mission01_base.scx")`
|
||||||
|
→ legt eine Arbeits-Basiskarte in `data/maps/` an.
|
||||||
|
3. Weiter wie gewohnt: `sc_describe_map` → `sc_create_location` → `sc_add_trigger` →
|
||||||
|
`sc_save_map`.
|
||||||
|
|
||||||
|
> Konfigurierbar über `SC_TEMPLATES_DIR` (Standard: `<SC_MAPS_DIR>/templates`).
|
||||||
|
|
||||||
|
> **Hinweis:** Die eigentlichen Template-`.scx`/`.scm` werden von Git **ignoriert**
|
||||||
|
> (siehe `.gitignore`) — nur diese README ist eingecheckt. Lege deine Vorlagen lokal
|
||||||
|
> hier ab.
|
||||||
|
|
@ -39,7 +39,8 @@ mcp = FastMCP(
|
||||||
"vorhandenen Basis-Karte in /data/maps; nur Logik (Trigger), Texte, "
|
"vorhandenen Basis-Karte in /data/maps; nur Logik (Trigger), Texte, "
|
||||||
"Locations, Player-Setup und Sounds werden bearbeitet. Typischer Ablauf: "
|
"Locations, Player-Setup und Sounds werden bearbeitet. Typischer Ablauf: "
|
||||||
"sc_list_maps -> sc_describe_map -> sc_create_location -> sc_add_trigger "
|
"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 ----------------------------------------------------------------
|
# --- 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).
|
# Verzeichnis mit Basis-Karten, WAVs und fertigen Missionen (gemountetes Volume).
|
||||||
MAPS_DIR = os.environ.get("SC_MAPS_DIR", "/data/maps")
|
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_IO = None
|
||||||
_MPQ_LOCK = threading.Lock()
|
_MPQ_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
|
@ -254,3 +261,68 @@ def list_maps() -> list[str]:
|
||||||
for f in os.listdir(MAPS_DIR)
|
for f in os.listdir(MAPS_DIR)
|
||||||
if f.lower().endswith((".scx", ".scm"))
|
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
|
||||||
|
|
|
||||||
93
tools/template_tools_test.py
Normal file
93
tools/template_tools_test.py
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
"""Verifikation der Terrain-Template-Tools (sc_list_templates / sc_new_from_template).
|
||||||
|
|
||||||
|
Ablauf:
|
||||||
|
base-map.scx -> als Template ablegen -> sc_list_templates -> sc_new_from_template
|
||||||
|
-> die erzeugte Basiskarte oeffnen und Tileset/Groesse bestaetigen.
|
||||||
|
Raeumt seine Test-Artefakte am Ende wieder weg.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
HERE = os.path.dirname(__file__)
|
||||||
|
MAPS = os.path.abspath(os.path.join(HERE, "..", "data", "maps"))
|
||||||
|
TPLS = os.path.join(MAPS, "templates")
|
||||||
|
|
||||||
|
# WICHTIG: vor dem Import setzen (Module lesen die Env beim Laden).
|
||||||
|
os.environ["SC_MAPS_DIR"] = MAPS
|
||||||
|
os.environ["SC_TEMPLATES_DIR"] = TPLS
|
||||||
|
|
||||||
|
from starcraft_mcp import server, workspace # noqa: E402
|
||||||
|
|
||||||
|
TEST_TEMPLATE = "_test_jungle_256.scx"
|
||||||
|
TEST_BASE = "_test_tpl_base.scx"
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup():
|
||||||
|
for p in (os.path.join(TPLS, TEST_TEMPLATE), os.path.join(MAPS, TEST_BASE)):
|
||||||
|
if os.path.exists(p):
|
||||||
|
os.remove(p)
|
||||||
|
workspace.discard_workspace(TEST_BASE)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
cleanup()
|
||||||
|
ok = True
|
||||||
|
|
||||||
|
# 1) Template bereitstellen (base-map als Vorlage kopieren)
|
||||||
|
shutil.copyfile(os.path.join(MAPS, "base-map.scx"), os.path.join(TPLS, TEST_TEMPLATE))
|
||||||
|
print(f"[setup] template gelegt: {TEST_TEMPLATE}")
|
||||||
|
|
||||||
|
# 2) sc_list_templates
|
||||||
|
listed = server.sc_list_templates()
|
||||||
|
names = [t["name"] for t in listed["templates"]]
|
||||||
|
print(f"[1] sc_list_templates -> count={listed['count']} names={names}")
|
||||||
|
match = next((t for t in listed["templates"] if t["name"] == TEST_TEMPLATE), None)
|
||||||
|
if not match:
|
||||||
|
print(" FAIL: Test-Template nicht gelistet")
|
||||||
|
ok = False
|
||||||
|
else:
|
||||||
|
print(f" meta: tileset={match.get('tileset')} "
|
||||||
|
f"size={match.get('width')}x{match.get('height')}")
|
||||||
|
if str(match.get("tileset")).upper() != "JUNGLE" or match.get("width") != 256:
|
||||||
|
print(" WARN: unerwartete Metadaten (base-map = 256x256 Jungle erwartet)")
|
||||||
|
|
||||||
|
# 3) sc_new_from_template (overwrite explizit -> kein Field-Default-Problem)
|
||||||
|
created = server.sc_new_from_template(
|
||||||
|
template=TEST_TEMPLATE, output_name=TEST_BASE, overwrite=True
|
||||||
|
)
|
||||||
|
print(f"[2] sc_new_from_template -> {created}")
|
||||||
|
new_path = os.path.join(MAPS, TEST_BASE)
|
||||||
|
if not os.path.exists(new_path):
|
||||||
|
print(" FAIL: neue Basiskarte wurde nicht erstellt")
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
# 4) erzeugte Karte oeffnen + beschreiben
|
||||||
|
desc = server.sc_describe_map(map=TEST_BASE)
|
||||||
|
print(f"[3] sc_describe_map -> tileset={desc['tileset']} size={desc['size']} "
|
||||||
|
f"triggers={desc['trigger_count']} locations={len(desc['locations'])}")
|
||||||
|
if str(desc["tileset"]).upper() != "JUNGLE" or desc["size"]["width"] != 256:
|
||||||
|
print(" FAIL: erzeugte Karte hat unerwartetes Terrain")
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
# 5) Fehlerpfad: unbekanntes Template -> FileNotFoundError
|
||||||
|
try:
|
||||||
|
server.sc_new_from_template(
|
||||||
|
template="gibtsnicht.scx", output_name="_x.scx", overwrite=True
|
||||||
|
)
|
||||||
|
print("[4] FAIL: unbekanntes Template haette werfen muessen")
|
||||||
|
ok = False
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
print(f"[4] unbekanntes Template wirft korrekt: {type(e).__name__}")
|
||||||
|
|
||||||
|
cleanup()
|
||||||
|
print()
|
||||||
|
print("RESULT:", "PASS" if ok else "FAIL")
|
||||||
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
121
tools/terrain_roundtrip_test.py
Normal file
121
tools/terrain_roundtrip_test.py
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
"""Round-Trip-Gate (Terrain-Automatisierung).
|
||||||
|
|
||||||
|
Belegt den load-bearing Befund aus docs/research/TERRAIN-AUTOMATION.md:
|
||||||
|
Reicht RichChk eine .scx byte-/section-stabil durch (Terrain-Sections + Trigger/Locations),
|
||||||
|
wenn man sie liest und UNVERAENDERT wieder speichert?
|
||||||
|
|
||||||
|
Ablauf:
|
||||||
|
read_chk_from_mpq(base) -> save_chk_to_mpq(chk, base, out) -> read_chk_from_mpq(out)
|
||||||
|
dann: jede CHK-Section des Originals mit der des Re-Reads vergleichen.
|
||||||
|
|
||||||
|
Den finalen "oeffnet in SCMDraft + StarCraft"-Check macht der Mensch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
MAPS_DIR = os.path.join(os.path.dirname(__file__), "..", "data", "maps")
|
||||||
|
MAPS_DIR = os.path.abspath(MAPS_DIR)
|
||||||
|
BASE = "base-map.scx"
|
||||||
|
OUT = "_roundtrip_out.scx"
|
||||||
|
|
||||||
|
from richchk.io.mpq.starcraft_mpq_io_helper import StarCraftMpqIoHelper
|
||||||
|
|
||||||
|
|
||||||
|
def section_name(sec) -> str:
|
||||||
|
return type(sec).__name__
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
base_path = os.path.join(MAPS_DIR, BASE)
|
||||||
|
out_path = os.path.join(MAPS_DIR, OUT)
|
||||||
|
if os.path.exists(out_path):
|
||||||
|
os.remove(out_path)
|
||||||
|
|
||||||
|
print(f"[i] MAPS_DIR = {MAPS_DIR}")
|
||||||
|
print(f"[i] base = {base_path} ({os.path.getsize(base_path)} bytes)")
|
||||||
|
|
||||||
|
mpq_io = StarCraftMpqIoHelper.create_mpq_io()
|
||||||
|
|
||||||
|
print("[1] read original ...")
|
||||||
|
orig = mpq_io.read_chk_from_mpq(base_path)
|
||||||
|
orig_secs = list(orig.chk_sections)
|
||||||
|
print(f" {len(orig_secs)} sections: {[section_name(s) for s in orig_secs]}")
|
||||||
|
|
||||||
|
print("[2] save unchanged ...")
|
||||||
|
mpq_io.save_chk_to_mpq(orig, base_path, out_path, overwrite_existing=True)
|
||||||
|
print(f" wrote {out_path} ({os.path.getsize(out_path)} bytes)")
|
||||||
|
|
||||||
|
print("[3] re-read output ...")
|
||||||
|
rt = mpq_io.read_chk_from_mpq(out_path)
|
||||||
|
rt_secs = list(rt.chk_sections)
|
||||||
|
print(f" {len(rt_secs)} sections: {[section_name(s) for s in rt_secs]}")
|
||||||
|
|
||||||
|
print("[4] compare sections (by type) ...")
|
||||||
|
orig_by_type: dict[str, object] = {section_name(s): s for s in orig_secs}
|
||||||
|
rt_by_type: dict[str, object] = {section_name(s): s for s in rt_secs}
|
||||||
|
|
||||||
|
all_types = sorted(set(orig_by_type) | set(rt_by_type))
|
||||||
|
terrain = {
|
||||||
|
"RichDimSection", "RichEraSection", "RichMtxmSection",
|
||||||
|
"RichTileSection", "RichIsomSection", "RichMaskSection",
|
||||||
|
}
|
||||||
|
ok = True
|
||||||
|
for t in all_types:
|
||||||
|
a = orig_by_type.get(t)
|
||||||
|
b = rt_by_type.get(t)
|
||||||
|
tag = "TERRAIN" if t in terrain else " "
|
||||||
|
if a is None:
|
||||||
|
print(f" [+only-out] {tag} {t}")
|
||||||
|
continue
|
||||||
|
if b is None:
|
||||||
|
print(f" [-only-in ] {tag} {t} <-- LOST on round-trip")
|
||||||
|
if t in terrain:
|
||||||
|
ok = False
|
||||||
|
continue
|
||||||
|
equal = a == b
|
||||||
|
mark = "OK " if equal else "DIFF"
|
||||||
|
if not equal and t in terrain:
|
||||||
|
ok = False
|
||||||
|
print(f" [{mark}] {tag} {t}")
|
||||||
|
|
||||||
|
# Trigger / Location quick counts
|
||||||
|
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
|
||||||
|
|
||||||
|
def counts(chk):
|
||||||
|
mrgn = ChkQueryUtil.find_only_rich_section_in_chk(RichMrgnSection, chk)
|
||||||
|
trig = ChkQueryUtil.find_only_rich_section_in_chk(RichTrigSection, chk)
|
||||||
|
return len(mrgn.locations), len(trig.triggers)
|
||||||
|
|
||||||
|
o_loc, o_trig = counts(orig)
|
||||||
|
r_loc, r_trig = counts(rt)
|
||||||
|
print(f"[5] locations: {o_loc} -> {r_loc} | triggers: {o_trig} -> {r_trig}")
|
||||||
|
if (o_loc, o_trig) != (r_loc, r_trig):
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
print("[6] positional integrity (every section in order, incl. VCOD) ...")
|
||||||
|
if len(orig_secs) != len(rt_secs):
|
||||||
|
print(f" section COUNT differs: {len(orig_secs)} -> {len(rt_secs)}")
|
||||||
|
ok = False
|
||||||
|
pos_diffs = 0
|
||||||
|
for i, (x, y) in enumerate(zip(orig_secs, rt_secs)):
|
||||||
|
if x != y:
|
||||||
|
pos_diffs += 1
|
||||||
|
print(f" DIFF @ {i}: {section_name(x)}")
|
||||||
|
print(f" positional diffs: {pos_diffs} (0 = byte-identical CHK content)")
|
||||||
|
if pos_diffs:
|
||||||
|
ok = False
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("RESULT:", "PASS (terrain + logic section-stable)" if ok
|
||||||
|
else "FAIL (see DIFF/LOST above)")
|
||||||
|
print("NOTE: final 'opens in SCMDraft + StarCraft' check is manual.")
|
||||||
|
return 0 if ok else 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Loading…
Add table
Add a link
Reference in a new issue