feat(client): game session management (#258, D-085)
Per-game save directories under user://saves/<timestamp>-<seed>/. SessionManager autoload handles new_game(), resume_game(), quit flow. Main menu scene with New Game / Continue / Quit buttons. Game-id passed to server subprocess via --game-id flag. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,11 @@ extends Node
|
||||
# Entities use Protocol decoded format: {entity_id, x, y, z, kind: {variant, data}}.
|
||||
# Tiles use format: [{x, y, z, type}].
|
||||
var current_snapshot: Dictionary = {}
|
||||
|
||||
# D-085 (#258): Active game session identifier. Format: <YYYYMMDD>-<HHMMSS>-<hex6>
|
||||
# Set by SessionManager.new_game() or SessionManager.resume_game().
|
||||
# Empty string when no session is active (main menu state).
|
||||
var current_game_id: String = ""
|
||||
var current_tick: int = 0
|
||||
var player_position: Vector2 = Vector2.ZERO
|
||||
var visible_entities: Array = []
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
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()
|
||||
rng.randomize()
|
||||
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)])
|
||||
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.
|
||||
## node: the calling scene node (used as dialog parent).
|
||||
## #554: The actual F5 save will be wired here once server supports SaveCommand.
|
||||
func quit_to_menu(node: Node) -> void:
|
||||
if _quit_dialog != null and is_instance_valid(_quit_dialog):
|
||||
return # Dialog already open
|
||||
_quit_dialog = ConfirmationDialog.new()
|
||||
_quit_dialog.title = ""
|
||||
_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")
|
||||
node.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: F5 quicksave will be triggered here before scene change when server
|
||||
# supports SaveCommand. For now: navigate to menu without saving.
|
||||
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
|
||||
@@ -66,8 +66,13 @@ func connect_to_sim() -> void:
|
||||
# Spawn server subprocess
|
||||
if not server_path.is_empty():
|
||||
_server = ServerProcess.new()
|
||||
# Server reads first positional arg as bind address (e.g. "127.0.0.1:9876")
|
||||
var pid := _server.start(server_path, ["127.0.0.1:" + str(server_port)])
|
||||
# Server reads first positional arg as bind address (e.g. "127.0.0.1:9876").
|
||||
# D-085 (#258): pass --game-id <id> so server logs use the same session identifier.
|
||||
var args := ["127.0.0.1:" + str(server_port)]
|
||||
var game_id: String = GameState.current_game_id
|
||||
if not game_id.is_empty():
|
||||
args.append_array(["--game-id", game_id])
|
||||
var pid := _server.start(server_path, args)
|
||||
if pid <= 0:
|
||||
push_error("SimBridge: failed to start server")
|
||||
_set_state(ConnectionState.ERROR)
|
||||
|
||||
Reference in New Issue
Block a user