OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / maps / home_reset.gd

91 lines · gd ref:
extends RefCounted
## RESET THIS MAP TO THE RELEASE STARTER — Home only (docs/home-world-design.md §6,
## "Reset to the release starter"; user 2026-10-05: "yes build reset that way").
##
## The flatten pattern, because the designed tombstone reset could not work: the seed
## writes only ABSENT keys (a tombstoned starter record is still present, so the map
## would come back empty), and a flattened map's `base` records refuse tombstones.
##
##   1. refuse unless this room is HOME and you are its admin, and this release ships a
##      starter for the map
##   2. BACKUP every key that will be erased (a failed backup aborts; nothing changed)
##   3. erase_state every key of this map — journal records of every author, plain keys
##      (objects, sky, terrain edits, ...), the starter marker — EXCEPT player positions
##      (per person, not content) and the level-source key (names the .MAP)
##   4. seed the starter at once (GameState.seed_home_starter): every key it ships is
##      now absent, so all of it is written, and the marker with it
##
## No Ctrl+Z past it: the backup file is the way back (as for flatten).

const HomeStarter := preload("res://scenes/maps/home_starter.gd")


## The keys a reset erases, from all of the map's keys.
static func keys_to_reset(map_keys: Array) -> Array[String]:
	var out: Array[String] = []
	for k: Variant in map_keys:
		var s := String(k)
		if s.begins_with("world:players:"):
			continue                                   # where each person stood
		if s.begins_with("world:level:") and s.ends_with(":source"):
			continue                                   # which .MAP: not content
		out.append(s)
	out.sort()
	return out


## Why a reset is not possible here, or "" if it is.
static func refusal(gs: Object, ks: Object, starter: Dictionary) -> String:
	var room := String(ks.get_current_room()) if ks != null and ks.has_method("get_current_room") else ""
	if ks == null or not ks.is_home(room):
		return "only your Home can be reset to the release starter"
	if not gs._has_role("admin"):
		return "only the Home's admin can reset it"
	if starter.is_empty():
		return "this release ships no starter for %s" % String(gs.current_map_id)
	if String(starter.get("map_id", "")) != String(gs.current_map_id):
		return "the starter is for %s, not %s" % [starter.get("map_id", "?"), gs.current_map_id]
	return ""


## {erased, refused, seeded, backup} or {error}.
static func reset(gs: Object, ks: Object, starter: Dictionary, backup_dir: String = "") -> Dictionary:
	var why := refusal(gs, ks, starter)
	if why != "":
		return {"error": why}
	var keys := keys_to_reset(gs.map_scoped_keys())
	var backup := _write_backup(gs, ks, keys, backup_dir)
	if backup == "":
		return {"error": "backup failed — nothing changed"}
	var erased := 0
	var refused := 0
	for k: String in keys:
		gs.erase_state(k)
		if gs.has_state(k):
			refused += 1
		else:
			erased += 1
	var seeded: Dictionary = gs.seed_home_starter(String(ks.get_current_room()), starter)
	print("[HomeReset] %s: erased %d key(s) (%d refused), seeded %s, backup %s" % [
		gs.current_map_id, erased, refused, str(seeded), backup])
	return {"erased": erased, "refused": refused, "seeded": seeded, "backup": backup}


static func _write_backup(gs: Object, ks: Object, keys: Array[String], dir: String) -> String:
	if dir == "":
		dir = "user://reset_backups_ci" if OS.get_environment("CI") == "true" else "user://reset_backups"
	DirAccess.make_dir_recursive_absolute(dir)
	var map_id := String(gs.current_map_id) if String(gs.current_map_id) != "" else "map"
	var path := "%s/%s-%d.json" % [dir, map_id, int(Time.get_unix_time_from_system())]
	var entries := {}
	for k: String in keys:
		entries[k] = var_to_str(gs.get_state(k))
	var f := FileAccess.open(path, FileAccess.WRITE)
	if f == null:
		return ""
	f.store_string(JSON.stringify({"format": 1, "kind": "home_reset", "map_id": map_id,
		"room": String(ks.get_current_room()), "made_at": Time.get_datetime_string_from_system(true),
		"entries": entries}, "  ", true))
	f.close()
	return path