OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / game / flatten_room.gd

89 lines · gd ref:
extends Node
## FLATTEN THE WHOLE ROOM (2026-10-05: "per map but with an optional full room
## flatten"). Flattening needs the map LOADED — its level and objects are what the
## minimum records are read from (LevelPersistence.flatten_map) — so this visits every
## map that has level history, flattens it, and comes back to where you were.
##
## It lives under the scene ROOT, not under a map: a map change frees the map and
## everything below it (the History panel included), and this has to outlive several.
##
## Usage: FlattenRoom.start(game) -> the node; await node.done -> [{map, before, after,
## backup} | {map, error}].

signal done(report: Array)

const MAP_DIR := "res://scenes/maps/%s.tscn"
const READY_TIMEOUT_S := 20.0

var report: Array = []


## Make one under the root and run it. `maps` defaults to every map with level history
## in the room's state (KeetService.game_state.get_all()).
static func start(game: Node, maps: Array = []) -> Node:
	var fr: Node = load("res://scenes/game/flatten_room.gd").new()
	fr.name = "FlattenRoom"
	game.get_tree().root.add_child(fr)
	if maps.is_empty():
		var ks := game.get_node_or_null(^"/root/KeetService")
		var all: Dictionary = ks.game_state.get_all() if ks != null and ks.game_state != null else {}
		maps = maps_with_history(all)
	fr.call_deferred("_run", game, maps)
	return fr


## Map ids that have level journal records (not just a level-source key), sorted.
static func maps_with_history(entries: Dictionary) -> Array:
	var ids := {}
	for k: String in entries:
		if not k.begins_with("map:") or k.ends_with(":source"):
			continue
		var rest := k.substr(4)
		var cut := rest.find(":")
		if cut <= 0:
			continue
		if rest.substr(cut + 1).begins_with("world:level:"):
			ids[rest.substr(0, cut)] = true
	var out: Array = ids.keys()
	out.sort()
	return out


func _run(game: Node, maps: Array) -> void:
	var start_path := String(game.get("current_map_path"))
	for id: Variant in maps:
		var path := MAP_DIR % String(id)
		if not ResourceLoader.exists(path):
			report.append({"map": String(id), "error": "no scene %s" % path})
			continue
		if String(game.get("current_map_path")) != path:
			await game.change_map(path, Vector3.ZERO)
		var persist: Node = await _wait_persist(game)
		if persist == null:
			report.append({"map": String(id), "error": "the map's journal was not ready in %ds" % int(READY_TIMEOUT_S)})
			continue
		var r: Dictionary = persist.flatten_map()
		r["map"] = String(id)
		report.append(r)
	if start_path != "" and String(game.get("current_map_path")) != start_path:
		await game.change_map(start_path, Vector3.ZERO)
	print("[FlattenRoom] done: %s" % str(report))
	done.emit(report)
	queue_free()


## The loaded map's LevelPersistence once its room state has arrived, or null.
func _wait_persist(game: Node) -> Node:
	var gs := get_node_or_null(^"/root/GameState")
	var t0 := Time.get_ticks_msec()
	while Time.get_ticks_msec() - t0 < int(READY_TIMEOUT_S * 1000.0):
		await get_tree().process_frame
		var m: Node = game.get("current_map")
		var p: Node = m.get("_persist") if m != null and is_instance_valid(m) else null
		if p != null and p.is_inside_tree() and (gs == null or bool(gs.get("room_state_loaded"))):
			for i in 2:
				await get_tree().process_frame
			p.refold()
			return p
	return null