118 lines
6.1 KiB
GDScript
118 lines
6.1 KiB
GDScript
extends RefCounted
|
|
|
|
const DATA := preload("res://scripts/game/save_data.gd")
|
|
const PATH := "user://last_game.res"
|
|
const BACKUP := "user://last_game_backup.res"
|
|
const TEMP := "user://last_game_pending.res"
|
|
const DIRECTORY := "user://saves"
|
|
const STATE_FIELDS := ["selected_ship_path", "system_level", "system_seed", "active_distant_map", "distant_home", "highest_system", "system_seeds", "cleared_systems", "explored_pois", "asteroid_retry_seeds", "exploration_wreck_bonuses", "hud_panel_state"]
|
|
|
|
static func read_save(path: String = PATH) -> Resource:
|
|
if not FileAccess.file_exists(path): return null
|
|
var file := FileAccess.open(path, FileAccess.READ)
|
|
if file == null: return null
|
|
var header := file.get_buffer(4).get_string_from_ascii()
|
|
file.close()
|
|
if header not in ["RSRC", "RSCC"]: return null
|
|
var data := ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE_DEEP)
|
|
if data == null or data.get_script() != DATA or data.version != 1: return null
|
|
if not data.captain is CaptainProgress or data.state.get("system_seed", 0) == 0: return null
|
|
for key in ["cargo", "weapon", "module", "extension"]:
|
|
if not data.inventory.get(key) is Array: return null
|
|
return data
|
|
|
|
static func available() -> bool:
|
|
return not list_saves().is_empty()
|
|
|
|
static func valid_slot(id: String) -> bool:
|
|
return RegEx.create_from_string("^slot_[a-zA-Z0-9_-]{1,72}$").search(id) != null
|
|
|
|
static func slot_path(id: String) -> String:
|
|
return DIRECTORY.path_join(id+".res") if valid_slot(id) else ""
|
|
|
|
static func list_saves() -> Array[Dictionary]:
|
|
var entries: Array[Dictionary] = []
|
|
var ids: Array[String] = ["auto"]
|
|
if DirAccess.dir_exists_absolute(DIRECTORY):
|
|
for file in DirAccess.get_files_at(DIRECTORY):
|
|
if not file.ends_with(".res") or file.ends_with(".pending.res"): continue
|
|
var id := file.trim_suffix(".backup.res") if file.ends_with(".backup.res") else file.trim_suffix(".res")
|
|
if valid_slot(id) and id not in ids: ids.append(id)
|
|
for id in ids:
|
|
var path := PATH if id == "auto" else slot_path(id)
|
|
var backup := BACKUP if id == "auto" else path.trim_suffix(".res")+".backup.res"
|
|
var data := read_save(path)
|
|
var recovered := false
|
|
if data == null:
|
|
data = read_save(backup)
|
|
recovered = data != null
|
|
if data == null: continue
|
|
var timestamp: float = data.saved_unix if data.saved_unix > 0 else float(FileAccess.get_modified_time(backup if recovered else path))
|
|
entries.append({"id":id,"path":backup if recovered else path,"name":data.save_name if not data.save_name.is_empty() else ("Automatyczny zapis" if id == "auto" else "Zapis rozgrywki"),"saved_at":data.saved_at,"timestamp":timestamp,"level":int(data.state.system_level),"captain_level":data.captain.level,"credits":data.captain.credits,"ship_path":str(data.state.get("selected_ship_path","")),"recovered":recovered})
|
|
entries.sort_custom(func(a: Dictionary,b: Dictionary) -> bool: return float(a.timestamp) > float(b.timestamp))
|
|
return entries
|
|
|
|
static func write(state: Node, game: Node, save_name: String = "", id: String = "") -> Error:
|
|
var named := not save_name.is_empty()
|
|
if named and (save_name.strip_edges().is_empty() or save_name.length() > 48): return ERR_INVALID_PARAMETER
|
|
if not id.is_empty() and (not named or not valid_slot(id)): return ERR_INVALID_PARAMETER
|
|
if game == null or not game.has_node("PlayerShip") or bool(game.get("_transitioning")): return ERR_UNAVAILABLE
|
|
var ship := game.get_node("PlayerShip")
|
|
if bool(ship.get("_is_destroyed")): return ERR_UNAVAILABLE
|
|
var data := DATA.new()
|
|
data.saved_at = Time.get_datetime_string_from_system()
|
|
data.saved_unix = Time.get_unix_time_from_system()
|
|
data.save_name = save_name.strip_edges() if named else "Automatyczny zapis"
|
|
data.slot_id = id if not id.is_empty() else ("slot_%d_%08x" % [Time.get_ticks_usec(),randi()] if named else "auto")
|
|
for key in STATE_FIELDS: data.state[key] = state.get(key)
|
|
data.captain = state.captain.duplicate(true)
|
|
data.captain.skill_offers.assign(state.captain.skill_offers)
|
|
# A saved surface expedition returns safely to orbit with its current cargo.
|
|
if state.captain.expedition.active:
|
|
for key in ["active", "cargo", "cargo_modules", "cargo_records", "cargo_credits", "cargo_parts", "cargo_missiles", "active_profile", "visit_seed"]:
|
|
var value: Variant = state.captain.expedition.get(key)
|
|
data.captain.expedition.set(key, value.duplicate(true) if value is Dictionary or value is Array else value)
|
|
data.captain.expedition.finish_visit(data.captain, true)
|
|
data.skill_offers.assign(data.captain.skill_offers)
|
|
var inventory := ship.get_node("ShipInventory")
|
|
var previous: Dictionary = state.restart_inventory
|
|
inventory.preserve_for_restart()
|
|
data.inventory = state.restart_inventory
|
|
state.restart_inventory = previous
|
|
if named:
|
|
var directory_error := DirAccess.make_dir_recursive_absolute(DIRECTORY)
|
|
if directory_error != OK: return directory_error
|
|
return _commit(data, slot_path(data.slot_id), slot_path(data.slot_id).trim_suffix(".res")+".backup.res", slot_path(data.slot_id).trim_suffix(".res")+".pending.res")
|
|
return _commit(data, PATH, BACKUP, TEMP)
|
|
|
|
static func _commit(data: Resource, path: String, backup: String, temporary: String) -> Error:
|
|
var error := ResourceSaver.save(data, temporary)
|
|
if error != OK: return error
|
|
if read_save(temporary) == null: return ERR_FILE_CORRUPT
|
|
if read_save(path) != null:
|
|
error = DirAccess.copy_absolute(path, backup)
|
|
if error != OK: return error
|
|
return DirAccess.rename_absolute(temporary, path)
|
|
|
|
static func restore_state(state: Node, id: String = "") -> bool:
|
|
var entries := list_saves()
|
|
var data: Resource
|
|
for entry in entries:
|
|
if id.is_empty() or entry.id == id:
|
|
data = read_save(entry.path)
|
|
break
|
|
if data == null: return false
|
|
for key in STATE_FIELDS:
|
|
if data.state.has(key): state.set(key, data.state[key])
|
|
state.captain = data.captain
|
|
state.captain.skill_offers.assign(data.skill_offers)
|
|
state.captain.reset_combat_state()
|
|
state.restart_inventory = data.inventory
|
|
state.return_to_freighter = false
|
|
state.distant_warp_arrival = false
|
|
state.hull_bastion_used = false
|
|
# Loading always starts the current sector again, with fresh encounters.
|
|
if state.active_distant_map.is_empty(): state.cleared_systems.erase(state.system_level)
|
|
state.reset_current_system_exploration()
|
|
return true
|