OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / landscape_edit_gate.gd

111 lines · gd ref:
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)