Files
settled-reach/client/scripts/autoloads/session_manager.gd
T
jpmschweitzerandClaude Opus 4.6 e1ea07e746 feat(client): save/load client UI — F5/F6 quicksave/quickload (#554)
Wire SaveGame/LoadGame player actions through the full client stack:
protocol v15 decode, InputMapper F5/F6 bindings, SimBridge wire mapping
with one-shot carry-forward, GameState save_result field, and HUD
notification via monologue display. Quit-to-menu triggers quicksave
before scene change.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:15:05 +01:00

122 lines
4.0 KiB
GDScript

extends Node
## D-085 (#258): Game session lifecycle manager.
## Creates per-game save directories on New Game, resumes existing sessions,
## and handles quit-to-menu flow with save confirmation.
##
## All save dirs live under user://saves/<game-id>/ where game-id is
## <YYYYMMDD>-<HHMMSS>-<hex6> (e.g. "20260225-143022-a7b3f1").
const SAVES_DIR := "user://saves/"
const GAME_SCENE := "res://scenes/main.tscn"
const MENU_SCENE := "res://scenes/main_menu.tscn"
var _quit_dialog: ConfirmationDialog = null
## Generate a new game-id, create its save directory, and activate the session.
## Returns the new game-id string.
func new_game() -> String:
var now := Time.get_datetime_dict_from_system()
var timestamp := "%04d%02d%02d-%02d%02d%02d" % [
now.year, now.month, now.day,
now.hour, now.minute, now.second,
]
var rng := RandomNumberGenerator.new()
var hex_seed := "%06x" % (rng.randi() & 0xFFFFFF)
var game_id := "%s-%s" % [timestamp, hex_seed]
var save_path := SAVES_DIR + game_id + "/"
var err := DirAccess.make_dir_recursive_absolute(save_path)
if err != OK:
push_error("SessionManager: failed to create save dir %s: %s" % [
save_path, error_string(err)])
return ""
GameState.current_game_id = game_id
return game_id
## Resume an existing game session by setting the active game-id.
func resume_game(game_id: String) -> void:
GameState.current_game_id = game_id
## List all game directories under user://saves/ sorted by last-modified (most recent first).
## Returns Array of {game_id: String, modified_time: int, newest_save: String}.
func list_game_dirs() -> Array:
var dir := DirAccess.open(SAVES_DIR)
if dir == null:
return []
var results: Array = []
dir.list_dir_begin()
var entry := dir.get_next()
while entry != "":
if dir.current_is_dir() and not entry.begins_with("."):
var dir_path := SAVES_DIR + entry + "/"
var newest_save := _find_newest_save(dir_path)
var mtime: int = 0
if newest_save != "":
mtime = FileAccess.get_modified_time(dir_path + newest_save)
results.append({
"game_id": entry,
"modified_time": mtime,
"newest_save": newest_save,
})
entry = dir.get_next()
dir.list_dir_end()
results.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a.modified_time > b.modified_time)
return results
## Show "Save before quitting?" confirmation dialog, then return to main menu.
## #554: The actual F5 save will be wired here once server supports SaveCommand.
func quit_to_menu() -> void:
if _quit_dialog != null and is_instance_valid(_quit_dialog):
return # Dialog already open
_quit_dialog = ConfirmationDialog.new()
_quit_dialog.dialog_text = UIStrings.get_text("menu.confirm_quit")
_quit_dialog.ok_button_text = UIStrings.get_text("menu.confirm_yes")
_quit_dialog.cancel_button_text = UIStrings.get_text("menu.confirm_no")
get_tree().root.add_child(_quit_dialog)
_quit_dialog.confirmed.connect(_do_quit_to_menu)
_quit_dialog.canceled.connect(_cleanup_quit_dialog)
_quit_dialog.popup_centered()
func _do_quit_to_menu() -> void:
_cleanup_quit_dialog()
# #554: Trigger quicksave before navigating to menu.
if not GameState.current_game_id.is_empty():
var path := "user://saves/" + GameState.current_game_id + "/quicksave.sav"
SimBridge.send_input({
"action": InputMapper.Action.SAVE_GAME,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {"path": path},
})
GameState.current_game_id = ""
get_tree().change_scene_to_file(MENU_SCENE)
func _cleanup_quit_dialog() -> void:
if _quit_dialog != null and is_instance_valid(_quit_dialog):
_quit_dialog.queue_free()
_quit_dialog = null
func _find_newest_save(dir_path: String) -> String:
var dir := DirAccess.open(dir_path)
if dir == null:
return ""
var best_name := ""
var best_time: int = 0
dir.list_dir_begin()
var entry := dir.get_next()
while entry != "":
if not dir.current_is_dir() and entry.ends_with(".sav"):
var mtime := FileAccess.get_modified_time(dir_path + entry)
if mtime > best_time:
best_time = mtime
best_name = entry
entry = dir.get_next()
dir.list_dir_end()
return best_name