feat: Implement command-line scene loading and PLAY-on-launch functionality

This commit is contained in:
2026-09-15 15:43:15 -04:00
parent a7c58705e1
commit e1b4554ca9
9 changed files with 580 additions and 7 deletions
+47
View File
@@ -0,0 +1,47 @@
class_name CliSceneLauncher
extends RefCounted
## Pure command-line argument parser for the Sandbox Stage scene loader.
## No scene-tree or OS access, so it is directly unit-testable. Parses
## OS.get_cmdline_user_args() output into a structured {scene_path, play, error}
## result; the caller (SandboxStage) is responsible for the error dialog and
## autosave policy.
static func parse(args: Array) -> Dictionary:
var scene_path := ""
var play := false
var used_scene_flag := false
var used_positional := false
var error := ""
var i := 0
while i < args.size():
var token := String(args[i])
if token == "--play":
play = true
elif token == "--scene":
used_scene_flag = true
if i + 1 >= args.size():
return {"scene_path": "", "play": play, "error": "--scene requires a path."}
var value := String(args[i + 1])
if value.is_empty():
return {"scene_path": "", "play": play, "error": "--scene requires a path."}
scene_path = value
i += 1
elif token.begins_with("--scene="):
used_scene_flag = true
var value := token.substr("--scene=".length())
if value.is_empty():
return {"scene_path": "", "play": play, "error": "--scene requires a path."}
scene_path = value
elif token.begins_with("--"):
return {"scene_path": "", "play": play, "error": "Unknown option: %s" % token}
else:
if used_positional:
return {"scene_path": "", "play": play, "error": "Specify the scene with either --scene or a single positional path."}
used_positional = true
scene_path = token
i += 1
if used_scene_flag and used_positional:
error = "Specify the scene with either --scene or a single positional path."
elif play and scene_path.is_empty():
error = "--play requires --scene <path>."
return {"scene_path": scene_path, "play": play, "error": error}
+1
View File
@@ -0,0 +1 @@
uid://dmoyg7nq46v07
+47 -2
View File
@@ -198,6 +198,8 @@ var _unsaved_changes_dialog: ConfirmationDialog = null
var _rename_scene_dialog: ConfirmationDialog = null
var _scene_name_edit: LineEdit = null
var _missing_assets_dialog: AcceptDialog = null
var _cli_error_dialog: AcceptDialog = null
var _cli_startup: bool = false
var _panning: bool = false
var _pan_last: Vector2 = Vector2.ZERO
@@ -422,7 +424,7 @@ func _ready() -> void:
_apply_cursor()
_refresh_status()
_update_scene_chrome()
_check_autosave_recovery()
_handle_cli_startup(OS.get_cmdline_user_args())
func _process(delta: float) -> void:
@@ -607,7 +609,7 @@ func _viewport_to_world(viewport_pos: Vector2) -> Vector2:
func set_mode(mode: StageMode) -> void:
if mode == current_mode:
return
if mode == StageMode.PLAY:
if mode == StageMode.PLAY and not _cli_startup:
if not _save_scene_to_path(AUTOSAVE_PATH, true):
push_warning("SandboxStage: autosave before Play failed.")
current_mode = mode
@@ -1454,6 +1456,44 @@ func _check_autosave_recovery() -> void:
get_node(NodePath("UI")).add_child(dialog); dialog.popup_centered()
func _handle_cli_startup(args: Array) -> void:
var result: Dictionary = CliSceneLauncher.parse(args)
if not String(result.get("error", "")).is_empty():
_show_cli_error_dialog(String(result["error"]))
_check_autosave_recovery()
return
var scene_path := String(result.get("scene_path", ""))
if scene_path.is_empty():
_check_autosave_recovery()
return
if FileAccess.file_exists(AUTOSAVE_PATH):
DirAccess.remove_absolute(AUTOSAVE_PATH)
var load: Dictionary = _load_cli_scene(scene_path)
if not bool(load.get("ok", false)):
_show_cli_error_dialog(String(load.get("error", "Could not load scene.")))
return
if bool(result.get("play", false)):
_cli_startup = true
set_mode(StageMode.PLAY)
_cli_startup = false
func _load_cli_scene(path: String) -> Dictionary:
var read: Dictionary = STAGE_SCENE_IO.read_file(path)
if not bool(read.get("ok", false)):
return {"ok": false, "error": String(read.get("error", "Could not load scene."))}
if not _apply_scene_data(read["data"], path, false):
return {"ok": false, "error": "Could not apply scene data."}
return {"ok": true, "error": ""}
func _show_cli_error_dialog(message: String) -> void:
if _cli_error_dialog == null:
return
_cli_error_dialog.dialog_text = message
_cli_error_dialog.popup_centered()
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST:
_request_pending_file_action(PendingFileAction.QUIT)
@@ -1906,6 +1946,11 @@ func _build_ui() -> void:
_missing_assets_dialog.title = "Missing Scene Assets"
_missing_assets_dialog.min_size = Vector2i(560, 0)
ui.add_child(_missing_assets_dialog)
_cli_error_dialog = AcceptDialog.new()
_cli_error_dialog.name = "CliErrorDialog"
_cli_error_dialog.title = "Could Not Load Scene"
_cli_error_dialog.min_size = Vector2i(560, 0)
ui.add_child(_cli_error_dialog)
_unsaved_changes_dialog = ConfirmationDialog.new()
_unsaved_changes_dialog.name = "UnsavedChangesDialog"; _unsaved_changes_dialog.title = "Unsaved Changes"; _unsaved_changes_dialog.dialog_text = "Save changes before continuing?"; _unsaved_changes_dialog.ok_button_text = "Save"
_unsaved_changes_dialog.add_button("Discard", false, "discard")