From 0eae5f56adb9b7fc87116ff7f62a0021f7ab028e Mon Sep 17 00:00:00 2001 From: hermes-godot-vr Date: Thu, 15 Oct 2026 15:36:06 +0000 Subject: [PATCH] =?UTF-8?q?M2-Karte=20t=5Fb84af40f:=20'=E2=96=B6=20Karte?= =?UTF-8?q?=20testen'=20(Editor-Seite).=20Gro=C3=9Fer=20deutsch=20beschrif?= =?UTF-8?q?teter=20Button=20oben=20+=20Men=C3=BC-Eintrag:=20exportiert=20d?= =?UTF-8?q?ie=20AKTUELLE=20Map=20(auch=20ungespeichert)=20nach=20user://te?= =?UTF-8?q?strun/.svmap=20=C3=BCber=20dieselbe=20SvmapIO-Validierung?= =?UTF-8?q?=20wie=20das=20Speichern,=20sucht=20den=20scopedvr-client=20(Kl?= =?UTF-8?q?on=20neben=20dem=20Editor=20ODER=20client=5Fdir=20in=20user://s?= =?UTF-8?q?copedvr=5Feditor.cfg,=20dokumentiert=20im=20README-Abschnitt=20?= =?UTF-8?q?'Karte=20testen')=20und=20startet=20ihn=20als=20EIGENEN=20Proze?= =?UTF-8?q?ss=20(godot=20--path=20=20--=20--testmap=20=20--mo?= =?UTF-8?q?ck-master)=20=E2=80=94=20Editor=20bleibt=20offen,=20Spiel=20bee?= =?UTF-8?q?ndet=20sich=20selbst=20sauber=20per=20ESC.=20Ung=C3=BCltige=20M?= =?UTF-8?q?ap=20=E2=86=92=20Klartext-Block=20statt=20Start.=20TestRun-Klas?= =?UTF-8?q?se=20komplett=20static=20+=20headless=20testbar.=20Tests:=20tes?= =?UTF-8?q?ts/test=5Ftestrun.gd=20(14=20Checks:=20Temp-Export-Roundtrip,?= =?UTF-8?q?=20ung=C3=BCltige=20Map=20blockiert,=20Client-Suche=20inkl.=20K?= =?UTF-8?q?onfig-Vorrang,=20Godot-Findung,=20exakte=20Argumentliste);=20ru?= =?UTF-8?q?n=5Ftests.sh=20+=20parse=5Fcheck.sh=20um=20test=5Ftestrun/test?= =?UTF-8?q?=5Frun.gd=20erweitert=20=E2=80=94=20volle=20Suite=20220=20Check?= =?UTF-8?q?s=20gr=C3=BCn.=20GODOT-Pfad-Default=20in=20tests/run=5Ftestmap?= =?UTF-8?q?=5Fe2e.sh=20des=20Clients=20jetzt=20auto-erkennend=20(Sly-PC-Pf?= =?UTF-8?q?ad=20=E2=86=92=20~/bin/godot4=20=E2=86=92=20PATH).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 31 ++++++++++ src/editor/test_run.gd | 91 +++++++++++++++++++++++++++++ src/editor/test_run.gd.uid | 1 + src/ui/main.gd | 61 +++++++++++++++++++ tests/test_testrun.gd | 116 +++++++++++++++++++++++++++++++++++++ tests/test_testrun.gd.uid | 1 + tools/parse_check.sh | 2 +- tools/run_tests.sh | 2 +- 8 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 src/editor/test_run.gd create mode 100644 src/editor/test_run.gd.uid create mode 100644 tests/test_testrun.gd create mode 100644 tests/test_testrun.gd.uid diff --git a/README.md b/README.md index 898f7c2..725cae4 100644 --- a/README.md +++ b/README.md @@ -259,3 +259,34 @@ Mehrfachauswahl + Gruppieren + Gruppen-Verschieben, auf dem UX-Fundament - **Desktop-Smoke:** `godot … -- --groups-demo [ordner] [--groups-demo-quit]` fährt Mehrfachauswahl → Gruppieren → gemeinsames Ziehen/Drehen → Undo programmatisch durch und speichert Screenshots. + +## Karte testen (M2, Karte t_b84af40f — FERTIG) + +Button „▶ Karte testen“ oben (auch im Datei-Menü) startet die AKTUELLE Map — +auch ungespeicherte! — sofort als Spiel in einem EIGENEN Fenster. Der Editor +bleibt offen. Im Spiel: WASD laufen, Maus umsehen, Shift sprinten, Leertaste +springen, ESC beendet das Test-Fenster sauber (kein Prozess-Kill nötig). + +- **Temp-Export (`src/editor/test_run.gd`):** Die Map wird nach + `user://testrun/.svmap` exportiert (deterministischer Name aus dem + Map-Namen, erneuter Testlauf überschreibt) — über dieselbe + `SvmapIO.save_to_file`-Validierung wie das echte Speichern. Ist die Map + noch nicht lauffähig (z. B. Spawn-Zonen fehlen), kommt der Klartext-Block + mit den fehlenden Punkten, GESTARTET wird nichts. +- **Client-Suche (erste Treffer-Zeile gewinnt):** + 1. Konfigurationsdatei `user://scopedvr_editor.cfg`: + ```ini + [test] + client_dir="/pfad/zu/scopedvr-client" + godot_path="/pfad/zu/godot" ; nur nötig, wenn der Editor NICHT über Godot läuft + ``` + 2. Klon neben dem Editor: `/../scopedvr-client` + (erkennbar an `project.godot`). +- **Start:** Der Client läuft als eigener Godot-Prozess + (`godot --path -- --testmap --mock-master`) — kein + Master, kein Match-Server, alles lokal. Godot-Binary = das, unter dem der + Editor selbst läuft. +- **Tests:** `tests/test_testrun.gd` (14 Checks: Temp-Export mit Roundtrip, + ungültige Map blockiert, Client-Suche inkl. Konfig-Vorrang, Godot-Findung, + exakte Argumentliste). Parse-Check umfasst `test_run.gd`. + diff --git a/src/editor/test_run.gd b/src/editor/test_run.gd new file mode 100644 index 0000000..df7d7de --- /dev/null +++ b/src/editor/test_run.gd @@ -0,0 +1,91 @@ +class_name TestRun +extends RefCounted +## „▶ Karte testen" (Karte t_b84af40f): Export der AKTUELLEN Map nach Temp +## + Start des scopedvr-client als EIGENES Fenster im Testlauf-Modus +## (--testmap ). Der Editor bleibt offen — der Client ist ein eigener +## Prozess; das Spiel beendet sich selbst sauber (ESC = „Zurück zum Editor"). +## +## Client-Suche (erste Treffer-Zeile gewinnt): +## 1. Konfigurationsdatei user://scopedvr_editor.cfg, Abschnitt [test]: +## client_dir="/pfad/zu/scopedvr-client" +## 2. Klon neben dem Editor: /../scopedvr-client +## (erkennbar an project.godot im Ordner). +## +## Godot-Binary für den Start: das Binary, unter dem der Editor selbst läuft +## (OS.get_executable_path — Sly startet den Editor über den Godot-Editor, +## also kann DAS auch den Client starten). Läuft der Editor irgendwann als +## exportierte App, muss [test] godot_path gesetzt werden. +## +## Alles static + testbar ohne UI (tests/test_testrun.gd). + +const CFG_PATH := "user://scopedvr_editor.cfg" +const TESTRUN_DIR := "user://testrun" + + +static func load_cfg() -> ConfigFile: + var cfg := ConfigFile.new() + if FileAccess.file_exists(CFG_PATH): + cfg.load(CFG_PATH) # Fehler → Default-Logik greift weiter unten. + return cfg + + +## Liefert den Client-Projektordner (mit project.godot) oder "". +static func find_client(project_dir: String, cfg: ConfigFile = null) -> String: + var candidates: Array = [] + if cfg != null: + var configured := str(cfg.get_value("test", "client_dir", "")) + # Explizit gesetzter Pfad gewinnt, sobald der ORDNER existiert + # (bewusst ohne project.godot-Check — wer ihn konfiguriert, weiß es besser). + if not configured.is_empty() and DirAccess.dir_exists_absolute(configured): + return configured + var parent := project_dir.simplify_path().get_base_dir() + candidates.append(parent.path_join("scopedvr-client")) + for candidate in candidates: + var dir := str(candidate).simplify_path() + if FileAccess.file_exists(dir.path_join("project.godot")): + return dir + return "" + + +## Godot-Binary zum Starten des Clients (Editor-Binary oder Konfiguration). +static func find_godot(cfg: ConfigFile = null) -> String: + var exe := OS.get_executable_path() + if exe.get_file().to_lower().contains("godot"): + return exe + if cfg != null: + return str(cfg.get_value("test", "godot_path", "")) + return "" + + +## Deterministischer Temp-Pfad aus dem Map-Namen (erneuter Testlauf +## überschreibt die alte Export-Datei — kein Müll-Berg). +static func temp_map_path(doc: Dictionary) -> String: + var raw := str(doc.get("info", {}).get("name", "")).strip_edges() + if raw.is_empty(): + raw = "karte" + var safe := "" + for ch in raw: + var c := ch.to_lower() + if (c >= "a" and c <= "z") or (c >= "0" and c <= "9") or ch == "_" or ch == "-" or ch == " ": + safe += ch + else: + safe += "_" + safe = safe.strip_edges().replace(" ", "_").rstrip("_") + if safe.is_empty(): + safe = "karte" + return TESTRUN_DIR.path_join(safe + ".svmap") + + +## Exportiert das AKTUELLE Dokument (auch ungespeicherte Maps!) nach Temp. +## Rückgabe: {ok, errors, path} — path ist globalisiert (für den Client-Prozess). +static func export_temp_map(doc: Dictionary) -> Dictionary: + var rel := temp_map_path(doc) + DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(TESTRUN_DIR)) + var result := SvmapIO.save_to_file(rel, doc) + result["path"] = ProjectSettings.globalize_path(rel) + return result + + +## Argumentliste für den Client-Start (getestet: exakt diese Form). +static func launch_args(client_dir: String, map_path: String) -> PackedStringArray: + return PackedStringArray(["--path", client_dir, "--", "--testmap", map_path, "--mock-master"]) diff --git a/src/editor/test_run.gd.uid b/src/editor/test_run.gd.uid new file mode 100644 index 0000000..4ba94ac --- /dev/null +++ b/src/editor/test_run.gd.uid @@ -0,0 +1 @@ +uid://cd3gmuvpf5ldv diff --git a/src/ui/main.gd b/src/ui/main.gd index 795b055..6e38216 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -94,6 +94,9 @@ var check_btn: Button var check_dialog: AcceptDialog var check_text: Label +# Karte testen (Karte t_b84af40f) +var test_btn: Button + # Dialoge var new_dialog: AcceptDialog var open_dialog: FileDialog @@ -150,6 +153,8 @@ func _build_ui() -> void: popup.add_item("Speichern (Strg+S)", 2) popup.add_item("Speichern unter…", 3) popup.add_separator() + popup.add_item("▶ Karte testen", 5) + popup.add_separator() popup.add_item("Beenden", 4) popup.id_pressed.connect(_on_menu_action) top_h.add_child(file_menu) @@ -176,6 +181,14 @@ func _build_ui() -> void: check_btn.pressed.connect(_on_check_map) top_h.add_child(check_btn) + # Karte testen (Karte t_b84af40f): Map SOFORT als Spiel zum Durchlaufen. + test_btn = Button.new() + test_btn.text = "▶ Karte testen" + test_btn.custom_minimum_size = Vector2(190, 40) + test_btn.tooltip_text = "Startet die Map SOFORT als Spiel zum Durchlaufen — eigenes Fenster, der Editor bleibt offen. ESC im Spiel = zurück zum Editor." + test_btn.pressed.connect(_on_test_run) + top_h.add_child(test_btn) + var title_label := Label.new() title_label.text = " %s" % APP_TITLE title_label.add_theme_font_size_override("font_size", 20) @@ -756,6 +769,54 @@ func _on_menu_action(id: int) -> void: save_dialog.popup_centered() 4: # Beenden get_tree().quit() + 5: # Karte testen + _on_test_run() + + +## „▶ Karte testen" (Karte t_b84af40f): aktuelle Map (auch ungespeichert!) +## nach Temp exportieren und den scopedvr-client als EIGENES Fenster starten. +## Der Editor bleibt offen; das Spiel beendet sich selbst sauber (ESC). +func _on_test_run() -> void: + if doc.is_empty(): + _set_status("Erst eine Map anlegen (Datei → Neue Map), dann testen.", true) + return + var cfg := TestRun.load_cfg() + var client_dir := TestRun.find_client(ProjectSettings.globalize_path("res://"), cfg) + if client_dir.is_empty(): + _show_errors("Karte testen — das Spiel wurde nicht gefunden:", [ + "Der ScopedVR-Client (Ordner „scopedvr-client“) wurde nicht neben dem Editor gefunden.", + "So geht es: Den Client-Ordner neben den Editor-Ordner legen (gleicher Überordner),", + "ODER in der Datei scopedvr_editor.cfg im Ordner „ScopedVR Map Editor“ (Godot-App-Daten) eintragen:", + " [test]", + " client_dir=\"/pfad/zu/scopedvr-client\"", + " godot_path=\"/pfad/zu/godot\" (nur nötig, wenn der Editor NICHT über Godot gestartet wird)", + ]) + _set_status("Karte testen: Spiel nicht gefunden — siehe Fenster.", true) + return + var godot := TestRun.find_godot(cfg) + if godot.is_empty(): + _show_errors("Karte testen — Godot-Programm nicht gefunden:", [ + "Der Editor läuft gerade nicht über ein Godot-Programm, deshalb weiß er nicht,", + "wie er das Spiel-Fenster starten soll. Trage in scopedvr_editor.cfg ein:", + " [test]", + " godot_path=\"/pfad/zu/godot\"", + ]) + _set_status("Karte testen: Godot-Programm nicht gefunden — siehe Fenster.", true) + return + var exported := TestRun.export_temp_map(doc) + if not bool(exported["ok"]): + _show_errors("Karte testen — die Map ist noch nicht lauffähig:", exported["errors"]) + _set_status("Testlauf nicht möglich — siehe Fenster für die fehlenden Punkte (z. B. Spawn-Zonen).", true) + return + var map_name := str(doc.get("info", {}).get("name", "?")) + var args := TestRun.launch_args(client_dir, str(exported["path"])) + var pid := OS.create_process(godot, args) + if pid <= 0: + _show_errors("Karte testen — Start fehlgeschlagen:", [ + "Das Spiel-Fenster konnte nicht gestartet werden (Programm: %s)." % godot]) + _set_status("Karte testen: Start fehlgeschlagen.", true) + return + _set_status("▶ Karte „%s“ läuft im Test-Fenster — WASD laufen, ESC zurück zum Editor. Der Editor bleibt offen." % map_name) func _on_new_map_confirmed() -> void: diff --git a/tests/test_testrun.gd b/tests/test_testrun.gd new file mode 100644 index 0000000..86ecd26 --- /dev/null +++ b/tests/test_testrun.gd @@ -0,0 +1,116 @@ +extends SceneTree +## Editor-Tests (headless): Karte t_b84af40f — „▶ Karte testen" Verkabelung. +## Ausführen: godot --headless --path . --script res://tests/test_testrun.gd +## Exit 0 = alles grün. Läuft OHNE UI (TestRun/SvmapIO/SvmapDocument): +## Temp-Export (auch ungespeicherte Maps), Client-Suche, Godot-Binär-Findung, +## Argument-Aufbau — der eigentliche Prozess-Start ist Desktop-Sache +## (run_testmap_e2e.sh im CLIENT deckt die other-Seite ab). + +var failures := 0 +var checks := 0 + +const TEST_DIR := "user://test_testrun" + + +func _init() -> void: + _cleanup() + _run_tests() + _cleanup() + if failures == 0: + print("ALL OK: %d checks" % checks) + quit(0) + else: + print("FAILED: %d/%d checks" % [failures, checks]) + quit(1) + + +func _check(cond: bool, label: String) -> void: + checks += 1 + if cond: + print(" ok: %s" % label) + else: + failures += 1 + print(" FAIL: %s" % label) + + +func _cleanup() -> void: + _cleanup_dir(ProjectSettings.globalize_path(TEST_DIR)) + var rel := TestRun.temp_map_path({"info": {"name": "Testmap"}}) + var p := ProjectSettings.globalize_path(rel) + if FileAccess.file_exists(p): + DirAccess.remove_absolute(p) + + +func _cleanup_dir(base: String) -> void: + if not DirAccess.dir_exists_absolute(base): + return + var d := DirAccess.open(base) + if d == null: + return + for file in d.get_files(): + DirAccess.remove_absolute(base.path_join(file)) + for sub in d.get_directories(): + _cleanup_dir(base.path_join(sub)) + DirAccess.remove_absolute(base) + + +func _run_tests() -> void: + print("== Temp-Map-Pfad ==") + var path := TestRun.temp_map_path({"info": {"name": "Offenes Gelände 2!"}}) + _check(str(path).begins_with("user://testrun/"), "Temp-Pfad liegt unter user://testrun/") + _check(str(path).ends_with("Offenes_Gel_nde_2.svmap"), "Map-Name bereinigt (Sonderzeichen → _, kein trailing _)") + path = TestRun.temp_map_path({}) + _check(str(path).ends_with("karte.svmap"), "fehlender Name → Fallback „karte.svmap“") + + print("== Temp-Export (ungespeicherte Map) ==") + var doc := SvmapDocument.create_new("Testkarte", 256.0, "mittag") + var exported := TestRun.export_temp_map(doc) + _check(bool(exported["ok"]), "neue Map exportiert (ohne je gespeichert zu haben)") + var global_path := str(exported["path"]) + _check(FileAccess.file_exists(global_path), "Temp-Datei existiert auf Disk") + var back := SvmapIO.load_from_file(global_path) + _check(bool(back["ok"]), "Temp-Datei lädt wieder (Prüfsumme gültig — voller Roundtrip)") + _check(str(back["data"]["info"]["name"]) == "Testkarte", "Inhalt identisch") + + print("== Temp-Export (ungültige Map → Klartext-Block) ==") + var broken: Dictionary = doc.duplicate(true) + broken["spawn_zones"] = {"0": {"points": []}, "1": {"points": []}} + var failed := TestRun.export_temp_map(broken) + _check(not bool(failed["ok"]), "ungültige Map wird NICHT exportiert") + _check((failed["errors"] as Array).size() > 0, "Fehlerliste fürs Dialog-Fenster vorhanden") + + print("== Client-Suche ==") + var base := ProjectSettings.globalize_path(TEST_DIR) + DirAccess.make_dir_recursive_absolute(base.path_join("mein-editor-projekt")) + DirAccess.make_dir_recursive_absolute(base.path_join("scopedvr-client")) + var f := FileAccess.open(base.path_join("scopedvr-client/project.godot"), FileAccess.WRITE) + f.store_string("[application]\n") + # 2) Nachbar-Klon wird gefunden: + var found := TestRun.find_client(base.path_join("mein-editor-projekt"), ConfigFile.new()) + _check(found == base.path_join("scopedvr-client"), + "Nachbar-Klon ../scopedvr-client gefunden") + # 1) Konfiguration gewinnt (Ordner existiert, ohne project.godot-Check): + var cfg := ConfigFile.new() + cfg.set_value("test", "client_dir", base.path_join("irgendwo")) + DirAccess.make_dir_recursive_absolute(base.path_join("irgendwo")) + var found2 := TestRun.find_client(base.path_join("mein-editor-projekt"), cfg) + _check(found2 == base.path_join("irgendwo"), "Konfig-Pfad hat Vorrang (Ordner existiert)") + # 3) Gar nichts da: + DirAccess.remove_absolute(base.path_join("scopedvr-client/project.godot")) + var found3 := TestRun.find_client(base.path_join("mein-editor-projekt"), ConfigFile.new()) + _check(found3 == "", "kein Client → leerer String (Fehler-Dialog greift)") + + print("== Godot-Binär-Findung ==") + var exe := TestRun.find_godot(ConfigFile.new()) + _check(not exe.is_empty() and exe.get_file().to_lower().contains("godot"), + "Editor läuft über Godot → dasselbe Binary zum Starten") + + print("== Argument-Aufbau ==") + var args := TestRun.launch_args("/pfad/client", "/tmp/x.svmap") + var want := ["--path", "/pfad/client", "--", "--testmap", "/tmp/x.svmap", "--mock-master"] + var ok := args.size() == want.size() + if ok: + for i in want.size(): + if str(args[i]) != want[i]: + ok = false + _check(ok, "launch_args exakt: --path … -- --testmap … --mock-master") diff --git a/tests/test_testrun.gd.uid b/tests/test_testrun.gd.uid new file mode 100644 index 0000000..bcabc36 --- /dev/null +++ b/tests/test_testrun.gd.uid @@ -0,0 +1 @@ +uid://dhycopnavsglm diff --git a/tools/parse_check.sh b/tools/parse_check.sh index 13edd0e..84f574e 100755 --- a/tools/parse_check.sh +++ b/tools/parse_check.sh @@ -3,7 +3,7 @@ cd "$(dirname "$0")/.." GODOT=~/bin/godot4 fail=0 -for f in src/ui/main.gd src/editor/library_browser.gd src/editor/object_ops.gd \ +for f in src/ui/main.gd src/editor/test_run.gd src/editor/library_browser.gd src/editor/object_ops.gd \ src/editor/interaction.gd tools/gen_thumbs.gd tests/test_library.gd; do out=$("$GODOT" --headless --path . --check-only --script "res://$f" 2>&1 | grep -iv "godot engine\|^$" || true) if [ -n "$out" ]; then diff --git a/tools/run_tests.sh b/tools/run_tests.sh index b8c3926..3cb8e39 100755 --- a/tools/run_tests.sh +++ b/tools/run_tests.sh @@ -3,7 +3,7 @@ cd "$(dirname "$0")/.." GODOT=~/bin/godot4 rc=0 -for t in test_ux test_editor_svmap test_terrain test_library test_spawns; do +for t in test_ux test_editor_svmap test_terrain test_library test_spawns test_testrun; do "$GODOT" --headless --path . --script "res://tests/$t.gd" > "/tmp/$t.log" 2>&1 code=$? line=$(grep -E "ALL OK|FAILED" "/tmp/$t.log" | tail -1)