OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_terrain_material_setting.gd

154 lines · gd ref:
extends Node
## TERRAIN MATERIAL SETTING GATE (hyper-games #91): Settings "terrain_material"
## (Full / Vertex) drives map_mountain's mesh-time bake, live, and a FORCED mode
## (mountain_terrain_material, what the bench and the pixel gate use) ignores it.
##
##   "$HYPER_TPS_GODOT" --path . --rendering-driver vulkan \
##       res://scenes/test/gate_terrain_material_setting.tscn
##
## Needs the republished mountain (its material carries meta bb_mesh_bake) and a
## VISIBLE window -- the terrain meshes on the GPU thread. Changes the setting in
## memory only (never save_settings) and puts the player's value back at the end.
##
## Checks, in order:
##   1. setting Full at load      -> no kernel, the FULL material
##   2. setting -> Vertex (live)  -> kernel set, the SLIM material
##   3. setting -> Full (live)    -> kernel cleared, the FULL material again
##   4. forced Vertex, setting -> Full -> stays Vertex (the force wins)
##   Each step waits for the remesh and reads the chunks' CUSTOM0: Vertex must have
##   most vertices BAKED (c0.x >= 1.5), Full none -- "kernel set" alone cannot see a
##   kernel that failed to compile (it uploads zeros).

const MAP := "res://scenes/maps/map_mountain.tscn"
const KEY := "terrain_material"
const LOAD_CAP_S := 240.0
const SETTLE_CAP_S := 90.0

var _map: Node = null
var _fails := 0


func _ready() -> void:
	_run.call_deferred()


func _terrain() -> Node:
	return _map.get_node_or_null(^"MountainTerrain")


## "full" / "slim" / "none": which material the terrain draws with. The slim one
## is the only one whose code reads the bb_mk0 varying.
func _drawn() -> String:
	var m: Variant = _terrain().get("custom_material")
	if not (m is ShaderMaterial) or (m as ShaderMaterial).shader == null:
		return "none"
	return "slim" if (m as ShaderMaterial).shader.code.contains("bb_mk0") else "full"


func _kernel_set() -> bool:
	return not String(_terrain().call("get_material_kernel")).is_empty()


## The fraction of loaded chunk vertices the kernel actually BAKED: c0.x >= 1.5
## (mask 0 + 2.0). A kernel that is set but failed to compile -- e.g. a bundle
## whose shaders/ lacks material_kernel.glsl -- uploads zeros, so "kernel set"
## alone proves nothing; this is the check that sees it. -1 = no chunk meshes.
func _baked_fraction() -> float:
	var total := 0
	var baked := 0
	for d in _terrain().call("debug_chunk_meshes"):
		var m: ArrayMesh = d["mesh"]
		if m == null or m.get_surface_count() == 0:
			continue
		var c0: Variant = m.surface_get_arrays(0)[Mesh.ARRAY_CUSTOM0]
		var n := (m.surface_get_arrays(0)[Mesh.ARRAY_VERTEX] as PackedVector3Array).size()
		total += n
		if c0 is PackedFloat32Array and (c0 as PackedFloat32Array).size() == n * 4:
			for i in n:
				if (c0 as PackedFloat32Array)[i * 4] >= 1.5:
					baked += 1
	return -1.0 if total == 0 else float(baked) / float(total)


## A switch rebuilds the terrain: wait until it has remeshed and settled.
func _settle() -> void:
	var t0 := Time.get_ticks_msec()
	var calm := 0
	while calm < 30 and (Time.get_ticks_msec() - t0) / 1000.0 < SETTLE_CAP_S:
		await get_tree().process_frame
		# Chunk MESHES, not get_chunk_count: right after a rebuild the count and
		# "settled" can still describe the old tree while no mesh exists yet
		# (measured: baked=-1 on the first Vertex step).
		calm = calm + 1 if (bool(_terrain().call("is_lod_settled"))
				and (_terrain().call("debug_chunk_meshes") as Array).size() > 0) else 0


func _expect(step: String, want_kernel: bool, want_mat: String) -> void:
	await _settle()
	var k := _kernel_set()
	var d := _drawn()
	var f := _baked_fraction()
	# Vertex: most vertices baked (CPU-meshed chunks and seams are not). Full: none.
	var baked_ok := (f > 0.5) if want_kernel else (f == 0.0)
	var ok := k == want_kernel and d == want_mat and baked_ok
	if not ok:
		_fails += 1
	print("[GATE-TM] %s %s: kernel=%s material=%s baked=%.3f (want kernel=%s material=%s baked %s)"
			% ["PASS" if ok else "FAIL", step, k, d, f, want_kernel, want_mat,
			   "> 0.5" if want_kernel else "== 0"])


func _run() -> void:
	var had := Settings.is_overridden(KEY)
	var prev: Variant = Settings.get_value(KEY)
	Settings.set_value(KEY, 0)

	_map = (load(MAP) as PackedScene).instantiate()
	add_child(_map)
	var t0 := Time.get_ticks_msec()
	while (_terrain() == null or int(_terrain().call("get_chunk_count")) == 0) \
			and (Time.get_ticks_msec() - t0) / 1000.0 < LOAD_CAP_S:
		await get_tree().process_frame
	var tr := _terrain()
	if tr == null or not tr.has_method("get_material_kernel"):
		print("[GATE-TM] FAIL: no MountainTerrain, or a dc-terrain without get_material_kernel")
		get_tree().quit(2)
		return
	var full: Variant = tr.get("custom_material")
	if not (full is ShaderMaterial):
		print("[GATE-TM] FAIL: the terrain has no ShaderMaterial")
		get_tree().quit(2)
		return
	# The precondition every check below depends on: without the meta, Vertex can
	# never happen and step 2 would fail for the wrong reason.
	var src: Variant = _map.get("_full_mat")
	if not (src is ShaderMaterial and (src as ShaderMaterial).has_meta("bb_mesh_bake")):
		print("[GATE-TM] FAIL: the loaded material carries no bb_mesh_bake -- republish the mountain")
		get_tree().quit(2)
		return

	await _expect("1 load with Full", false, "full")

	Settings.set_value(KEY, 1)
	await get_tree().process_frame
	await _expect("2 live -> Vertex", true, "slim")

	Settings.set_value(KEY, 0)
	await get_tree().process_frame
	await _expect("3 live -> Full", false, "full")

	_map.set("mountain_terrain_material", 2)   # what the bench does, here after load
	Settings.set_value(KEY, 1)
	await get_tree().process_frame
	_map.call("_on_mesh_bake_toggled", true)   # forced Vertex, as _ready would have resolved it
	Settings.set_value(KEY, 0)
	await get_tree().process_frame
	await _expect("4 forced Vertex ignores the setting", true, "slim")

	if had:
		Settings.set_value(KEY, prev)
	else:
		Settings.clear_override(KEY)
	print("[GATE-TM] %s (%d failed)" % ["PASS" if _fails == 0 else "FAIL", _fails])
	get_tree().quit(0 if _fails == 0 else 1)