extends SceneTree
## THE TWO JOURNALS — map-authored landscape survives a player-journal clear.
##
## "$HYPER_TPS_GODOT" --headless --script scenes/test/landscape_edit_gate.gd
##
## This gate exists because its absence cost a week. hyper-games #30 spent four
## hours and six wrong causes on "terrain is inside START.MAP's rooms", and the
## answer was not in either repo's diff: the map stamped its level-shell subtract
## during import, and TerrainEditManager.reenter_map() — which fires on
## GameState.map_changing, AFTER that deferred import — called clear_edits() on
## the terrain it had just found and wiped it. The octree then rebuilt from a
## field with no subtract in it.
##
## The field was never wrong. debug_sdf_at at those same points still reported
## air, because the probe walks edit_list_ and the wipe had already happened.
##
## So there are TWO journals over one edit list and only one owner was considered:
##
## PLAYER world:terrain:edit:*, replayed per room. Dropping it on a map
## change is correct — that is what reenter_map is for.
## LANDSCAPE authored by the MAP. The level shell is one 563k-triangle MESH
## edit; the moat fallback is a BOX. Nothing outside the map may
## drop either.
##
## Both shapes are asserted, and the moat is the one that matters: it is the
## DEGRADED path, taken when the shell will not seal — which is exactly what you
## land in after deleting a brush from the level's outer boundary. Shipping the
## mesh persistent and leaving the box non-persistent put the original bug back
## through the fallback door, silently, on the path that only fires once you have
## started editing.
##
## Runs with no display and no map: DCTerrain outside the tree still keeps an edit
## list, and rebuild() returns early on !is_inside_tree(), so clear_edits() is
## exercised without meshing anything.
var _failures: Array[String] = []
func _check(label: String, ok: bool, detail: String) -> void:
if not ok:
_failures.append("%s (%s)" % [label, detail])
print(" %-46s %s %s" % [label, "OK " if ok else "FAIL", detail])
func _init() -> void:
print("\n=== two journals over one edit list ===")
if not ClassDB.class_exists("DCTerrain"):
print("RESULT: SKIP (no DCTerrain — libdc not built)")
quit(0)
return
var t: Node = ClassDB.instantiate("DCTerrain")
if not t.has_method("clear_landscape_edits"):
print("RESULT: SKIP (libdc predates clear_landscape_edits)")
t.free()
quit(0)
return
# --- MESH landscape: the level shell ---------------------------------------
var verts := PackedVector3Array([
Vector3(0, 0, 0), Vector3(4, 0, 0), Vector3(0, 4, 0), Vector3(0, 0, 4)])
var idx := PackedInt32Array([0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3])
t.stamp_mesh_arrays(verts, idx, Vector3(1, 1, 1), 1.0, -1.0, false, 0, true)
_check("mesh landscape stamped", t.get_edit_count() == 1,
"%d edit(s)" % t.get_edit_count())
# --- BOX landscape: the moat fallback --------------------------------------
# The path _subtract_shell() takes when the shell will not seal.
t.edit_terrain_box(Vector3(8, 0, 0), Vector3(2, 2, 2), -1.0, false, 0, true)
_check("box landscape (moat) stamped", t.get_edit_count() == 2,
"%d edit(s)" % t.get_edit_count())
# --- PLAYER journal: an ordinary carve --------------------------------------
t.edit_terrain_box(Vector3(-8, 0, 0), Vector3(1, 1, 1), -1.0, true, 0)
t.edit_terrain(Vector3(-4, 0, 0), 2.0, -1.0, 0, true, 0)
_check("player edits appended", t.get_edit_count() == 4,
"%d edit(s)" % t.get_edit_count())
# --- THE ASSERTION THIS GATE EXISTS FOR -------------------------------------
# What TerrainEditManager.reenter_map() calls on every map change.
t.clear_edits()
_check("clear_edits keeps BOTH landscape edits", t.get_edit_count() == 2,
"%d kept, want 2 (mesh shell + box moat)" % t.get_edit_count())
# --- and the inverse, which drives the `;` re-shell --------------------------
t.clear_landscape_edits()
_check("clear_landscape_edits drops them", t.get_edit_count() == 0,
"%d left" % t.get_edit_count())
# --- non-persistent edits are NOT sticky ------------------------------------
# The other direction: it would be just as wrong for an ordinary carve to
# survive a room change, which is what reenter_map is there to prevent.
t.edit_terrain_box(Vector3(0, 0, 0), Vector3(1, 1, 1), -1.0, true, 0)
t.clear_edits()
_check("a player edit does NOT survive", t.get_edit_count() == 0,
"%d left" % t.get_edit_count())
t.free()
print("")
if _failures.is_empty():
print("RESULT: PASS")
quit(0)
else:
for f in _failures:
print(" FAILED: %s" % f)
print("RESULT: FAIL")
quit(1)