OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_objects_without_level.gd

116 lines · gd ref:
extends SceneTree
## Gate: a map with objects but no DCLevel still folds its object records.
##
## THE BUG THIS EXISTS FOR. refold() opened with
##
##     if level == null or ... or not level.has_method("apply_journal"):
##         return {}
##
## and returned before touching `objects`. That conflates "this map has no brush
## level" with "there is nothing to fold". It never showed because every map with
## an object system also had a DCLevel — map_dc_level is the only map that
## instantiates SceneObjects at all (map_dc_level.gd:1169).
##
## It shows the moment the object system is brought to map_flat, which is a bare
## ground plane with no DCLevel: every object.create record would be journalled,
## replicated, and then silently never built, on every peer.
##
## Brushes still need a level and still skip without one. The two halves are
## already partitioned inside refold for a stated reason — handing object records
## to DCLevel makes it report each as a missing target — so the fix is to move the
## guard onto the brush half rather than to keep it in front of both.

var ok := true
var _ran := false


func _p(c: bool, m: String) -> void:
	print(("ok   " if c else "FAIL: ") + m)
	if not c:
		ok = false


func _process(_delta: float) -> bool:
	if _ran:
		return true
	_ran = true
	_run()
	print("GATE objects_without_level: %s" % ("PASS" if ok else "FAIL"))
	if not ok:
		quit(1)
	return true


func _run() -> void:
	# ⛔ load() AT RUNTIME, NOT class_name. Naming LevelPersistence / SceneObjects /
	# PrefabCatalogue directly resolves them at COMPILE time, which happens before
	# the autoload registry exists -- level_persistence.gd references GameState, so
	# the whole gate failed with "Identifier not found: GameState" and, because the
	# abort happened before any _p(), printed PASS. Same trap the repo already
	# records for --check-only.
	var SceneObjectsScript: GDScript = load("res://scenes/game/scene_objects.gd")
	var PersistScript: GDScript = load("res://scenes/maps/level_persistence.gd")
	var Catalogue: GDScript = load("res://scenes/game/prefab_catalogue.gd")
	_p(SceneObjectsScript != null and PersistScript != null and Catalogue != null,
		"scene_objects.gd, level_persistence.gd and prefab_catalogue.gd all load")
	if SceneObjectsScript == null or PersistScript == null or Catalogue == null:
		return

	var objects: Node = SceneObjectsScript.new()
	objects.name = "SceneObjects"
	root.add_child(objects)

	var persist: Node = PersistScript.new()
	persist.name = "LevelPersistenceNoLevel"
	persist.level = null                    # THE CASE: a map with no DCLevel
	persist.objects = objects
	root.add_child(persist)

	_p(persist.level == null, "the fixture really has no level")
	_p(persist.objects == objects, "and it does have an objects manager")

	# A prefab id that exists, so a skip here means the FOLD did not run rather
	# than that the catalogue refused it.
	var type_id := "marker"
	_p(Catalogue.has(type_id), "the '%s' prefab exists to create" % type_id)

	var dot: String = persist.reserve_dot()
	_p(dot != "", "the journal reserved a dot without a level (%s)" % dot)
	if dot == "":
		return

	var xf := Transform3D(Basis(), Vector3(4.0, 0.0, -2.0))
	var wrote: bool = persist.record_at(dot, "object.create",
		{"type": type_id, "xform": SceneObjectsScript.xform_to(xf)})
	_p(wrote, "and recorded an object.create")
	if not wrote:
		return

	var stats: Dictionary = persist.refold()
	_p(not stats.is_empty(),
		"refold returns a result with no level -- it used to return {} and do nothing")
	var os: Dictionary = stats.get("objects", {}) as Dictionary
	_p(int(os.get("applied", 0)) == 1,
		"the object record was APPLIED (applied=%d)" % int(os.get("applied", 0)))
	_p(objects.count() == 1, "and a node exists for it (count=%d)" % objects.count())

	var n: Node = objects.node_for(dot)
	_p(n != null, "the node resolves from its dot")
	if n is Node3D:
		_p((n as Node3D).transform.origin.is_equal_approx(xf.origin),
			"and landed where the record said (%s)" % str((n as Node3D).transform.origin))

	# SELF-CHECK: the fold must be doing the work, not the recording. A second
	# record that is NOT folded must not produce a node.
	var dot2: String = persist.reserve_dot()
	persist.record_at(dot2, "object.create",
		{"type": type_id, "xform": SceneObjectsScript.xform_to(Transform3D())})
	_p(objects.count() == 1,
		"self-check: recording alone builds nothing (still %d)" % objects.count())
	persist.refold()
	_p(objects.count() == 2,
		"self-check: and the fold is what builds it (now %d)" % objects.count())

	persist.free()
	objects.free()