Worker hatte die Arbeit fertig, aber NICHT committet/gepusht (3. Fall heute) — aus dem Karten-Arbeitsordner gerettet und hier gepusht. - GlbMeshStore.quellen(): Reihenfolge EINE Stelle — Setting -> Sibling-Repos -> mitgelieferter Ordner models/ direkt neben der App (dedupliziert, typisiert) - Klartext-Meldung FEHLT_TEXT statt stillem Platzhalter; Notfall-Modus: Terrain und Bauteile laufen ohne GLBs - neu: tools/sync_models.sh (fuellt models/, --check), utils/modelle_status.gd (Autoload ModelleStatus), tools/probe_gen_map.gd - neu: README_INSTALL.md (85 Zeilen, Schritt fuer Schritt + §5 Lizenzhinweis: ITHappy-Rohdaten NICHT weitergeben, App fuer Slys eigene Rechner) - export_presets.cfg (eigenstaendiger Linux-/Windows-Export), .gitignore: models/ + export/ bleiben draussen (Lizenz + Groesse) - Tests: test_model_sources (26/27; die rote ist umgebungsabhaengig: 'lager_ist_leer' erwartet fehlende Sibling-Repos) Belegt: Import 0 Fehler, gui_smoke 92, ui_layout 71, playtest_spawn 56.
229 lines
10 KiB
GDScript
229 lines
10 KiB
GDScript
extends SceneTree
|
|
## Editor-Tests (headless): Karte t_12cf94fe — Modell-Quellen-Reihenfolge,
|
|
## mitgelieferter models/-Ordner, Klartext-Banner, Notfall-Modus.
|
|
## Ausführen: godot --headless --path . --script res://tests/test_model_sources.gd
|
|
## Exit 0 = alles grün. Prüft:
|
|
## 1. quellen(): Reihenfolge Einstellungs-Pfad → Sibling-Defaults → models/,
|
|
## Duplikate gefiltert, ungültiger Settings-Wert fällt auf die Vorgabe.
|
|
## 2. probe_source_fuer(oid): erster EXISTIERENDER Kandidat gewinnt, ohne
|
|
## Parse (billiger FileAccess-Check), leerer String ohne Fund.
|
|
## 3. fehl_text(): Klartext-Meldung enthält models/-Pfad + sync_models.sh.
|
|
## 4. lager_ist_leer(): true ohne GLB-Quellen, false sobald eine Quelle eine
|
|
## GLB hat (auch rekursiv verschachtelt).
|
|
## 5. Notfall-Modus: fehlende OID → fehlt()=true, mesh_fuer()=null (Platzhalter
|
|
## ist Sache des view_builders — hier nur der Store-Vertrag).
|
|
## 6. models_pfad(): endet auf /models, neben der App (res://-Wurzel).
|
|
##
|
|
## Die Tests arbeiten auf TEMP-Ordnern und ProjectSettings im Speicher —
|
|
## sie berühren KEINE echten Dateien im Repo.
|
|
|
|
var failures := 0
|
|
var checks := 0
|
|
|
|
|
|
func _init() -> void:
|
|
# Vorgabe-Zustand: keine Einstellungs-Quellen (Sly-PCs ohne Siblings sind
|
|
# der Normalfall dieser Karte).
|
|
if ProjectSettings.has_setting(GlbMeshStore.QUELLEN_SETTING):
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
|
|
GlbMeshStore.cache_leeren()
|
|
_test_quellen_reihenfolge()
|
|
_test_probe_source()
|
|
_test_fehl_text()
|
|
_test_lager_ist_leer()
|
|
_test_notfall_modus()
|
|
_test_models_pfad()
|
|
if failures == 0:
|
|
print("ALL OK: %d checks" % checks)
|
|
quit(0)
|
|
else:
|
|
print("FAILED: %d/%d checks" % [failures, checks])
|
|
quit(1)
|
|
|
|
|
|
func _ok(bedingung: bool, name: String, detail := "") -> void:
|
|
checks += 1
|
|
if bedingung:
|
|
print(" ok: %s" % name)
|
|
else:
|
|
failures += 1
|
|
print(" FEHLER: %s%s" % [name, " — " + detail if detail != "" else ""])
|
|
|
|
|
|
func _temp_ordner(na: String) -> String:
|
|
var p := OS.get_environment("TMPDIR")
|
|
if p.is_empty():
|
|
p = "/tmp"
|
|
var out := p.path_join("t12cf94fe_" + na + "_" + str(randi() % 100000))
|
|
DirAccess.make_dir_recursive_absolute(out)
|
|
return out
|
|
|
|
|
|
## 1. Reihenfolge: (1) Settings-Pfad, (2) Defaults, (3) models/ — Duplikate raus.
|
|
func _test_quellen_reihenfolge() -> void:
|
|
# Ohne Setting: Defaults + models/ am Ende, keine Duplikate.
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
|
|
var q: Array = GlbMeshStore.quellen()
|
|
_ok(q.size() == 3, "quellen: ohne Setting = 2 Defaults + models/", str(q))
|
|
_ok(q[0] == GlbMeshStore.DEFAULT_QUELLEN[0] and q[1] == GlbMeshStore.DEFAULT_QUELLEN[1],
|
|
"quellen: Defaults zuerst (bisheriges Verhalten)", str(q))
|
|
_ok(q[2] == GlbMeshStore.MODELS_DIR, "quellen: models/ zuletzt", str(q))
|
|
# Mit Setting: der gesetzte Pfad gewinnt (erster Treffer).
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING,
|
|
PackedStringArray(["/eigener/pfad", q[0] as String]))
|
|
var q2: Array = GlbMeshStore.quellen()
|
|
_ok(q2[0] == "/eigener/pfad", "quellen: gesetzter Pfad gewinnt", str(q2))
|
|
_ok(q2.find(GlbMeshStore.MODELS_DIR) == q2.size() - 1,
|
|
"quellen: models/ bleibt am Ende", str(q2))
|
|
_ok(q2.size() == 4, "quellen: Duplikat Default[0] gefiltert", str(q2))
|
|
# Leere/whitespace Einträge fliegen raus.
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING,
|
|
PackedStringArray(["", " ", "/x"]))
|
|
var q3: Array = GlbMeshStore.quellen()
|
|
_ok(q3[0] == "/x", "quellen: leere Einträge gefiltert", str(q3))
|
|
# Ungültiger Settings-Wert (kein Array) → fällt auf Vorgabe-Verhalten.
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, "kein-array")
|
|
var q4: Array = GlbMeshStore.quellen()
|
|
_ok(q4.size() >= 3 and q4[0] == GlbMeshStore.DEFAULT_QUELLEN[0],
|
|
"quellen: ungültiger Wert → Vorgabe", str(q4))
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
|
|
|
|
|
|
## 2. probe_source_fuer: erster EXISTIERENDER Pfad, Reihenfolge respektiert,
|
|
## verschachtelter Registry-Pfad VOR flacher Alt-Ablage, kein Parse.
|
|
## Der verschachtelte Test nutzt eine echte Registry-OID (sonst erzeugt
|
|
## _kandidaten keinen verschachtelten Kandidaten-Pfad).
|
|
func _test_probe_source() -> void:
|
|
var basis := _temp_ordner("probe")
|
|
var a := basis.path_join("a_objekte")
|
|
var b := basis.path_join("b_objekte")
|
|
var models := basis.path_join("models")
|
|
# Echte OID mit verschachteltem glb-Pfad aus der Registry holen.
|
|
var manifest: Dictionary = {}
|
|
var parsed: Variant = JSON.parse_string(
|
|
FileAccess.get_file_as_string(GlbMeshStore.MANIFEST_PATH))
|
|
if parsed is Dictionary:
|
|
manifest = (parsed as Dictionary).get("objects", {})
|
|
var oid := ""
|
|
var sub := ""
|
|
for key: String in manifest:
|
|
var eintrag: Variant = manifest[key]
|
|
if eintrag is Dictionary:
|
|
var glb := str((eintrag as Dictionary).get("glb", ""))
|
|
if glb.begins_with("res://"):
|
|
oid = key
|
|
sub = glb.substr(6)
|
|
break
|
|
if oid.is_empty():
|
|
print(" SKIP: kein OID mit res://-glb in der Registry — verschachtelter Pfad nicht testbar")
|
|
return
|
|
DirAccess.make_dir_recursive_absolute(a.path_join(sub.get_base_dir()))
|
|
DirAccess.make_dir_recursive_absolute(b)
|
|
DirAccess.make_dir_recursive_absolute(models)
|
|
# (1) verschachtelt in a (Registry-Pfad), (2) flach in b, (3) models/.
|
|
var glb_a := a.path_join(sub)
|
|
var glb_b := b.path_join(oid + ".glb")
|
|
var glb_m := models.path_join(oid + ".glb")
|
|
for p: String in [glb_a, glb_b, glb_m]:
|
|
var f := FileAccess.open(p, FileAccess.WRITE)
|
|
f.store_8(0) # Inhalt egal — probe nur file_exists, kein Parse
|
|
f.close()
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray(
|
|
[_relativ(basis, a), _relativ(basis, b), _relativ(basis, models)]))
|
|
GlbMeshStore.cache_leeren()
|
|
_ok(GlbMeshStore.probe_source_fuer(oid).simplify_path() == glb_a.simplify_path(),
|
|
"probe: verschachtelter Registry-Pfad in Quelle 1 gewinnt",
|
|
GlbMeshStore.probe_source_fuer(oid))
|
|
_ok(GlbMeshStore.probe_source_fuer(oid).contains("/a_objekte/"),
|
|
"probe: Pfad enthält Quelle 1", GlbMeshStore.probe_source_fuer(oid))
|
|
# Nur models/ hat die Datei → Pfad aus models/.
|
|
var oid2 := "nur_models_oid"
|
|
var glb_m2 := models.path_join(oid2 + ".glb")
|
|
var f2 := FileAccess.open(glb_m2, FileAccess.WRITE)
|
|
f2.store_8(0)
|
|
f2.close()
|
|
_ok(GlbMeshStore.probe_source_fuer(oid2).simplify_path() == glb_m2.simplify_path(),
|
|
"probe: models/-Pfad gefunden, wenn nur dort vorhanden",
|
|
GlbMeshStore.probe_source_fuer(oid2))
|
|
_ok(GlbMeshStore.probe_source_fuer(oid2).contains("/models/"),
|
|
"probe: Pfad enthält /models/", GlbMeshStore.probe_source_fuer(oid2))
|
|
_ok(GlbMeshStore.probe_source_fuer("gaenzlich_fehlt") == "",
|
|
"probe: leerer String ohne Fund")
|
|
_ok(GlbMeshStore.probe_source_fuer("") == "", "probe: leere OID → leerer String")
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
|
|
GlbMeshStore.cache_leeren()
|
|
|
|
|
|
## 3. Klartext-Meldung: models/-Pfad + sync_models.sh-Verweis, mehrzeilig.
|
|
func _test_fehl_text() -> void:
|
|
var text := GlbMeshStore.fehl_text()
|
|
_ok(text.contains(GlbMeshStore.models_pfad()), "fehl_text: models/-Pfad drin")
|
|
_ok(text.contains("sync_models.sh"), "fehl_text: sync_models.sh-Verweis drin")
|
|
_ok(text.contains("Notfall-Modus"), "fehl_text: Notfall-Modus erwähnt")
|
|
_ok(text.contains("\n"), "fehl_text: mehrzeilig (klar lesbar)")
|
|
_ok(not text.contains("%s"), "fehl_text: kein unfüllter Platzhalter")
|
|
|
|
|
|
## 4. lager_ist_leer: ohne Quellen true; false, sobald eine Quelle (auch
|
|
## verschachtelt) eine GLB trägt. models/ zählt als Quelle.
|
|
func _test_lager_ist_leer() -> void:
|
|
var basis := _temp_ordner("leer")
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
|
|
GlbMeshStore.cache_leeren()
|
|
_ok(GlbMeshStore.lager_ist_leer(),
|
|
"lager_ist_leer: true ohne GLB-Quellen (Testumgebung hat keine Siblings)")
|
|
var tief := basis.path_join("tief/verschachtelt")
|
|
DirAccess.make_dir_recursive_absolute(tief)
|
|
var f := FileAccess.open(tief.path_join("eins.glb"), FileAccess.WRITE)
|
|
f.store_8(0)
|
|
f.close()
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING,
|
|
PackedStringArray([_relativ(basis, basis)]))
|
|
_ok(not GlbMeshStore.lager_ist_leer(),
|
|
"lager_ist_leer: false mit verschachtelter GLB in Quelle")
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
|
|
GlbMeshStore.cache_leeren()
|
|
|
|
|
|
## 5. Notfall-Modus: fehlende OID → fehlt()=true, mesh_fuer()=null. Ohne Modelle
|
|
## darf der Store nicht crashen (Platzhalter übernimmt der view_builder).
|
|
func _test_notfall_modus() -> void:
|
|
ProjectSettings.set_setting(GlbMeshStore.QUELLEN_SETTING, PackedStringArray())
|
|
GlbMeshStore.cache_leeren()
|
|
_ok(GlbMeshStore.fehlt("notfall_test_oid"), "notfall: fehlt()=true ohne Quellen")
|
|
_ok(GlbMeshStore.mesh_fuer("notfall_test_oid") == null,
|
|
"notfall: mesh_fuer()=null ohne Quellen (view_builder zeigt Platzhalter)")
|
|
_ok(GlbMeshStore.probe_source_fuer("notfall_test_oid") == "",
|
|
"notfall: probe_source_fuer() leer ohne Quellen")
|
|
|
|
|
|
## 6. models_pfad: direkt neben der App (res://-Wurzel), endet auf /models.
|
|
func _test_models_pfad() -> void:
|
|
var p := GlbMeshStore.models_pfad()
|
|
_ok(p.ends_with("/models"), "models_pfad: endet auf /models", p)
|
|
_ok(p.begins_with(ProjectSettings.globalize_path("res://")),
|
|
"models_pfad: neben der App (res://-Wurzel)", p)
|
|
_ok(p == ProjectSettings.globalize_path("res://").path_join("models"),
|
|
"models_pfad: exakt res://-Wurzel/models", p)
|
|
|
|
|
|
## Relativer Pfad (vom res://-Wurzel aus) — Quellen sind basis-relativ.
|
|
## (_basis wird ignoriert: die Funktion rechnet IMMER von der res://-Wurzel,
|
|
## wie _relativ_zu in test_glb_view.gd — sonst zeigt der Pfad in den falschen
|
|
## Ordner, wenn _kandidaten ihn wieder an die Wurzel hängt.)
|
|
static func _relativ(_basis: String, pfad: String) -> String:
|
|
var von := ProjectSettings.globalize_path("res://").simplify_path()
|
|
var n := pfad.simplify_path()
|
|
var v: PackedStringArray = von.split("/", false)
|
|
var teile_n: PackedStringArray = n.split("/", false)
|
|
var i := 0
|
|
while i < v.size() and i < teile_n.size() and v[i] == teile_n[i]:
|
|
i += 1
|
|
var out := PackedStringArray()
|
|
for _j: int in range(i, v.size()):
|
|
out.append("..")
|
|
for k: int in range(i, teile_n.size()):
|
|
out.append(teile_n[k])
|
|
if out.is_empty():
|
|
return "."
|
|
return "/".join(out)
|