extends Node ## Autoload "Saves": world slots on disk. ## ## Each world is a folder under `root_dir` (user://worlds//) holding: ## meta.json name, seed, created, last played, play time (read by the title screen) ## world.json the game state, written by world.gd's collect_save() ## ## The world itself is regenerated from its seed, so a save only stores what ## the player changed: their own state, placed pieces, chest contents, harvested ## resources and collected pickups. That keeps saves small, and it's the same ## data a dedicated server will persist in M2. ## ## Writes are atomic (temp file + rename) and the previous world.json is kept ## as world.json.bak, so a crash mid-save can't destroy a world. const VERSION := 1 var root_dir := "user://worlds" var current_id := "" ## world being played; "" = unsaved session (title screen, tests) func has_world() -> bool: return not current_id.is_empty() func world_seed() -> int: return int(read_meta(current_id).get("seed", 1337)) func _dir(id: String) -> String: return root_dir.path_join(id) func list_worlds() -> Array: var out: Array = [] var d := DirAccess.open(root_dir) if d == null: return out for id in d.get_directories(): var m := read_meta(id) if not m.is_empty(): m["id"] = id out.append(m) out.sort_custom(func(a, b): return float(a.get("last_played", 0)) > float(b.get("last_played", 0))) return out func create_world(world_name: String, seed: int) -> String: DirAccess.make_dir_recursive_absolute(root_dir) var base := world_name.to_lower().replace(" ", "_").validate_filename() if base.is_empty(): base = "world" var id := base var n := 2 while DirAccess.dir_exists_absolute(_dir(id)): id = "%s_%d" % [base, n] n += 1 DirAccess.make_dir_recursive_absolute(_dir(id)) var now := Time.get_unix_time_from_system() _write_json(_dir(id).path_join("meta.json"), { "name": world_name, "seed": seed, "created": now, "last_played": now, "play_time": 0.0, "version": VERSION, }) return id func delete_world(id: String) -> void: var dir := _dir(id) var d := DirAccess.open(dir) if d == null: return for f in d.get_files(): d.remove(f) DirAccess.remove_absolute(dir) if current_id == id: current_id = "" func read_meta(id: String) -> Dictionary: return _read_json(_dir(id).path_join("meta.json")) func load_state(id: String) -> Dictionary: var s := _read_json(_dir(id).path_join("world.json")) if s.is_empty() and FileAccess.file_exists(_dir(id).path_join("world.json.bak")): push_warning("Saves: world.json unreadable, using backup") s = _read_json(_dir(id).path_join("world.json.bak")) return s ## Writes the state for `id` and updates its meta. Returns true on success. func save_state(id: String, state: Dictionary, session_seconds: float) -> bool: var path := _dir(id).path_join("world.json") if FileAccess.file_exists(path): DirAccess.copy_absolute(path, path + ".bak") state["version"] = VERSION if not _write_json(path, state): return false var m := read_meta(id) m["last_played"] = Time.get_unix_time_from_system() m["play_time"] = float(m.get("play_time", 0.0)) + session_seconds _write_json(_dir(id).path_join("meta.json"), m) return true func _write_json(path: String, data: Dictionary) -> bool: var tmp := path + ".tmp" var f := FileAccess.open(tmp, FileAccess.WRITE) if f == null: push_error("Saves: can't write %s (%s)" % [tmp, error_string(FileAccess.get_open_error())]) return false f.store_string(JSON.stringify(data, "\t")) f.close() return DirAccess.rename_absolute(tmp, path) == OK func _read_json(path: String) -> Dictionary: if not FileAccess.file_exists(path): return {} var data = JSON.parse_string(FileAccess.get_file_as_string(path)) return data if typeof(data) == TYPE_DICTIONARY else {}