diff --git a/core/global/gamescope.gd b/core/global/gamescope.gd index 022fa7e0..0a64e98a 100644 --- a/core/global/gamescope.gd +++ b/core/global/gamescope.gd @@ -16,6 +16,13 @@ class_name Gamescope ## Most of the core functionality of this class is provided by the [Xlib] ## module, which is a GDExtension that exposes Xlib methods to Godot. +signal blur_mode_updated(from: int, to: int) +signal display_is_external_updated(from: int, to: int) +signal focused_window_updated(from: int, to: int) +signal focusable_windows_updated(from: PackedInt32Array, to: PackedInt32Array) +signal focused_app_updated(from: int, to: int) +signal focusable_apps_updated(from: PackedInt32Array, to: PackedInt32Array) + ## Gamescope Blur modes enum BLUR_MODE { OFF = 0, ## Turns off blur of running games @@ -30,13 +37,66 @@ enum XWAYLAND { GAME, ## Xwayland instance where games run } +## Gamescope is hard-coded to look for STEAM_GAME=769 to determine if it is the +## overlay app. +const OVERLAY_GAME_ID := 769 + @export var log_level := Log.LEVEL.INFO +## The primary xwayland is the primary Gamescope xwayland session that contains +## Gamescope properties on the root window. var xwayland_primary: Xlib +## The OGUI xwayland is the xwayland instance that OGUI is running under. var xwayland_ogui: Xlib +## The Game xwayland is the xwayland instance that games are launched under. var xwayland_game: Xlib +## Array of all discovered xwayland instances var xwaylands: Array[Xlib] = [] var logger := Log.get_logger("Gamescope", log_level) +# Gamescope properties +## Blur mode (read-only) +var blur_mode: int: + set(v): + var prev_value := blur_mode + blur_mode = v + if prev_value != v: + blur_mode_updated.emit(prev_value, v) +var baselayer_window: int +var input_counter: int +var display_is_external: int +var vrr_enabled: int +var vrr_feedback: int +var vrr_capable: int +var keyboard_focus_display: PackedInt32Array +var mouse_focus_display: PackedInt32Array +var focus_display: PackedInt32Array +var focused_window: int: + set(v): + var prev_value := focused_window + focused_window = v + if prev_value != v: + focused_window_updated.emit(prev_value, v) +var focused_app_gfx: int +var focused_app: int: + set(v): + var prev_value := focused_app + focused_app = v + if prev_value != v: + focused_app_updated.emit(prev_value, v) +var focusable_windows: PackedInt32Array: + set(v): + var prev_value := focusable_windows + focusable_windows = v + if prev_value != v: + focusable_windows_updated.emit(prev_value, v) +var focusable_apps: PackedInt32Array: + set(v): + var prev_value := focusable_apps + focusable_apps = v + if prev_value != v: + focusable_apps_updated.emit(prev_value, v) +var cursor_visible_feedback: int + # Connects to all gamescope xwayland instances func _init() -> void: @@ -118,6 +178,20 @@ func discover_gamescope_displays() -> PackedStringArray: return gamescope_displays +## Updates the Gamescope state. Should be called in a loop to keep the Gamescope +## state up-to-date. +func update() -> void: + blur_mode = get_blur_mode() + focused_window = get_focused_window() + focused_app = get_focused_app() + focusable_windows = get_focusable_windows() + focusable_apps = get_focusable_apps() + baselayer_window = get_baselayer_window() + if not baselayer_window in get_focusable_windows(): + baselayer_window = -1 + remove_baselayer_window() + + ## Returns the name of the given xwayland display (e.g. ":1") func get_display_name(display: XWAYLAND) -> String: var xwayland := get_xwayland(display) @@ -285,13 +359,25 @@ func get_focused_window(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: return results[0] +## Return the currently focused app id. +func get_focused_app(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: + var xwayland := get_xwayland(display) + if not xwayland: + return -1 + var root_id := xwayland.get_root_window_id() + var results := _get_xprop_array(xwayland, root_id, "GAMESCOPE_FOCUSED_APP") + if results.size() == 0: + return 0 + return results[0] + + ## Sets the given window as the main launcher app. ## Gamescope is hard-coded to look for appId 769 func set_main_app(window_id: int, display: XWAYLAND = XWAYLAND.OGUI) -> int: var xwayland := get_xwayland(display) if not xwayland: return -1 - return _set_xprop(xwayland, window_id, "STEAM_GAME", 769) + return _set_xprop(xwayland, window_id, "STEAM_GAME", OVERLAY_GAME_ID) ## Set the given window as the primary overlay input focus @@ -302,6 +388,11 @@ func set_input_focus(window_id: int, value: int, display: XWAYLAND = XWAYLAND.OG return _set_xprop(xwayland, window_id, "STEAM_INPUT_FOCUS", value) +## Returns whether or not the overlay window is currently focused +func is_overlay_focused(display: XWAYLAND = XWAYLAND.OGUI) -> bool: + return get_focused_app(display) == OVERLAY_GAME_ID + + ## Get the overlay status for the given window func get_overlay(window_id: int, display: XWAYLAND = XWAYLAND.OGUI) -> int: var xwayland := get_xwayland(display) @@ -382,7 +473,9 @@ func set_blur_mode(mode: BLUR_MODE = BLUR_MODE.OFF, display: XWAYLAND = XWAYLAND if not xwayland: return -1 var root_id := xwayland.get_root_window_id() - return _set_xprop(xwayland, root_id, "GAMESCOPE_BLUR_MODE", mode) + var err := _set_xprop(xwayland, root_id, "GAMESCOPE_BLUR_MODE", mode) + blur_mode = mode + return err ## Sets the Gamescope blur radius when blur is active @@ -408,6 +501,15 @@ func set_allow_tearing(allow: bool, display: XWAYLAND = XWAYLAND.PRIMARY) -> int return _set_xprop(xwayland, root_id, "GAMESCOPE_ALLOW_TEARING", value) +## Returns the currently set manual focus +func get_baselayer_window(display: XWAYLAND = XWAYLAND.PRIMARY) -> int: + var xwayland := get_xwayland(display) + if not xwayland: + return -1 + var root_id := xwayland.get_root_window_id() + return _get_xprop(xwayland, root_id, "GAMESCOPECTRL_BASELAYER_WINDOW") + + ## Focuses the given window func set_baselayer_window(window_id: int, display: XWAYLAND = XWAYLAND.PRIMARY) -> int: var xwayland := get_xwayland(display) diff --git a/core/global/launch_manager.gd b/core/global/launch_manager.gd index 2c5681c5..9ba3a118 100644 --- a/core/global/launch_manager.gd +++ b/core/global/launch_manager.gd @@ -37,7 +37,7 @@ signal recent_apps_changed() const SettingsManager := preload("res://core/global/settings_manager.tres") const InputManager := preload("res://core/global/input_manager.tres") const NotificationManager := preload("res://core/global/notification_manager.tres") -const Gamescope := preload("res://core/global/gamescope.tres") +var Gamescope := preload("res://core/global/gamescope.tres") as Gamescope var state_machine := preload("res://assets/state/state_machines/global_state_machine.tres") as StateMachine var in_game_state := preload("res://assets/state/states/in_game.tres") as State @@ -47,6 +47,7 @@ var _sandbox := Sandbox.get_sandbox() var _current_app: RunningApp var _pid_to_windows := {} var _running: Array[RunningApp] = [] +var _stopping: Array[RunningApp] = [] var _apps_by_pid: Dictionary = {} var _apps_by_name: Dictionary = {} var _data_dir: String = ProjectSettings.get_setting("OpenGamepadUI/data/directory") @@ -55,6 +56,25 @@ var _persist_data: Dictionary = {"version": 1} var logger := Log.get_logger("LaunchManager", Log.LEVEL.DEBUG) +# Connect to Gamescope signals +func _init() -> void: + # When window focus changes, update the current app and gamepad profile + var on_focus_changed := func(from: int, to: int): + logger.info("Window focus changed from " + str(from) + " to: " + str(to)) + var last_app := _current_app + _current_app = get_running_from_window_id(to) + app_switched.emit(last_app, _current_app) + # If the app has a gamepad profile, set it + set_app_gamepad_profile(_current_app) + Gamescope.focused_window_updated.connect(on_focus_changed) + + # Remove the in-game state when no apps are running and only one focusable + # app exists. + var on_focusable_apps_changed := func(from: PackedInt32Array, to: PackedInt32Array): + logger.debug("Apps changed from " + str(from) + " to " + str(to)) + Gamescope.focusable_apps_updated.connect(on_focusable_apps_changed) + + # Loads persistent data like recent games launched, etc. func _load_persist_data(): # Create the data directory if it doesn't exist @@ -157,31 +177,12 @@ func launch(app: LibraryLaunchItem) -> RunningApp: return running_app -## Sets the gamepad profile for the running app with the given profile -func set_gamepad_profile(path: String) -> void: - # If no profile was specified, unset the gamepad profiles - if path == "": - for gamepad in InputManager.get_managed_gamepads(): - InputManager.set_gamepad_profile(gamepad, null) - return - - # Try to load the profile and set it - var profile := load(path) - - # TODO: Save profiles for individual controllers? - for gamepad in InputManager.get_managed_gamepads(): - InputManager.set_gamepad_profile(gamepad, profile) - if not profile: - logger.warn("Gamepad profile not found: " + path) - return - var notify := Notification.new("Using gamepad profile: " + profile.name) - NotificationManager.show(notify) - - ## Stops the game and all its children with the given PID func stop(app: RunningApp) -> void: + if app.state == app.STATE.STOPPING: + app.kill(Reaper.SIG.KILL) + return app.kill() - _remove_running(app) ## Returns a list of apps that have been launched recently @@ -199,9 +200,34 @@ func get_recent_apps() -> Array: return recent +# Updates our list of recently launched apps +func _update_recent_apps(app: LibraryLaunchItem) -> void: + if not "recent" in _persist_data: + _persist_data["recent"] = [] + var recent: Array = _persist_data["recent"] + recent.erase(app.name) + recent.push_front(app.name) + + if recent.size() > 30: + recent.pop_back() + _persist_data["recent"] = recent + _save_persist_data() + recent_apps_changed.emit() + + ## Returns a list of currently running apps func get_running() -> Array[RunningApp]: - return _running + return _running.duplicate() + + +## Returns the running app from the given window id +func get_running_from_window_id(window_id: int) -> RunningApp: + for app in _running: + if window_id == app.window_id: + return app + if window_id in app.window_ids: + return app + return null ## Returns the currently running app @@ -209,20 +235,11 @@ func get_current_app() -> RunningApp: return _current_app -## Sets the given running app as the current app -func set_current_app(app: RunningApp, switch_baselayer: bool = true) -> void: - if switch_baselayer: - if not can_switch_app(app): - return - Gamescope.set_baselayer_window(app.window_id) - var old := _current_app - _current_app = app - app_switched.emit(old, app) - - # Return if we are switching to null - if not app: - return - +## Sets the gamepad profile for the running app with the given profile +func set_app_gamepad_profile(app: RunningApp) -> void: + # If no app was specified, unset the current gamepad profile + if app == null or app.launch_item == null: + set_gamepad_profile("") # Check to see if this game has any gamepad profiles. If so, set our # gamepads to use them. var section := ".".join(["game", app.launch_item.name.to_lower()]) @@ -230,123 +247,94 @@ func set_current_app(app: RunningApp, switch_baselayer: bool = true) -> void: set_gamepad_profile(profile_path) +## Sets the gamepad profile for the running app with the given profile +func set_gamepad_profile(path: String) -> void: + # If no profile was specified, unset the gamepad profiles + if path == "": + for gamepad in InputManager.get_managed_gamepads(): + InputManager.set_gamepad_profile(gamepad, null) + return + + # Try to load the profile and set it + var profile := load(path) + + # TODO: Save profiles for individual controllers? + for gamepad in InputManager.get_managed_gamepads(): + InputManager.set_gamepad_profile(gamepad, profile) + if not profile: + logger.warn("Gamepad profile not found: " + path) + return + var notify := Notification.new("Using gamepad profile: " + profile.name) + NotificationManager.show(notify) + + +## Sets the given running app as the current app +func set_current_app(app: RunningApp, switch_baselayer: bool = true) -> void: + if app == null: + return + app.grab_focus() + + ## Returns true if the given app can be switched to via Gamescope func can_switch_app(app: RunningApp) -> bool: if app == null: logger.warn("Unable to switch to null app") return false - if not app.window_id > 0: - logger.warn("No Window ID was found for given app") - return false - return true + return app.can_focus() ## Returns whether the given app is running func is_running(app_name: String) -> bool: - if app_name in _apps_by_name: - return true - return false - - -# Updates our list of recently launched apps -func _update_recent_apps(app: LibraryLaunchItem) -> void: - if not "recent" in _persist_data: - _persist_data["recent"] = [] - var recent: Array = _persist_data["recent"] - recent.erase(app.name) - recent.push_front(app.name) - - if len(recent) > 30: - recent.pop_back() - _persist_data["recent"] = recent - _save_persist_data() - recent_apps_changed.emit() + return app_name in _apps_by_name # Adds the given PID to our list of running apps func _add_running(app: RunningApp): _apps_by_pid[app.pid] = app _apps_by_name[app.launch_item.name] = app + app.state_changed.connect(_on_app_state_changed.bind(app)) _running.append(app) - set_current_app(app, false) app_launched.emit(app) +## Called when a running app's state changes +func _on_app_state_changed(_from: RunningApp.STATE, to: RunningApp.STATE, app: RunningApp) -> void: + if to != RunningApp.STATE.STOPPED: + return + _remove_running(app) + if state_machine.has_state(in_game_state) and _running.size() == 0: + logger.info("No more apps are running. Removing in-game state.") + Gamescope.remove_baselayer_window() + state_machine.remove_state(in_game_state) + state_machine.remove_state(in_game_menu_state) + + # Removes the given PID from our list of running apps func _remove_running(app: RunningApp): logger.info("Cleaning up pid {0}".format([app.pid])) _running.erase(app) _apps_by_name.erase(app.launch_item.name) _apps_by_pid.erase(app.pid) - - if app == _current_app: - if _running.size() > 0: - set_current_app(_running[-1]) - else: - set_current_app(null, false) - - # If no more apps are running, clear the in-game state - if len(_running) == 0: - Gamescope.remove_baselayer_window() - state_machine.remove_state(in_game_state) - state_machine.remove_state(in_game_menu_state) app_stopped.emit(app) - app.app_killed.emit() # Checks for running apps and updates our state accordingly -func _check_running(): +func _check_running() -> void: # Find the root window var root_id := Gamescope.get_root_window_id(Gamescope.XWAYLAND.GAME) if root_id < 0: return + # Update the Gamescope state + Gamescope.update() + # Update our view of running processes and what windows they have _update_pids(root_id) - # If nothing should is running, skip our window checks - if len(_running) == 0: - return - - # TODO: Maybe start a timer for any apps that haven't produced a window - # in a certain timeframe? If the timer ends, kill everything. - - # Check all running apps - var to_remove := [] + # Update the state of all running apps for app in _running: - var app_name := app.launch_item.name - # Ensure that all windows related to the app have an app ID set - app.ensure_app_id() - - # Ensure that the running app has a corresponding window ID - if app.needs_window_id(): - var window_id := app.discover_window_id() - if window_id > 0: - logger.debug("Setting window ID " + str(window_id) + " for " + app_name) - app.window_id = window_id - continue #? - - # If this was launched by Steam, try and detect if the game closed - # so we can kill Steam graefully - #if app.is_steam_app() and app.num_created_windows > 0: - # logger.debug("Running app is a Steam game and has no valid window ID, but has been detected " + str(app.num_created_windows) + " times.") - # var steam_pid := app.find_steam() - # if steam_pid > 0: - # logger.info("Trying to stop steam with pid: " + str(steam_pid)) - # OS.execute("kill", ["-15", str(steam_pid)]) - - # If our app is still running, great! - if app.is_running(): - continue - - # If it's not running, make sure we remove it from our list - if app.not_running_count > 3: - to_remove.push_back(app) - - # Remove any non-running apps - for app in to_remove: - _remove_running(app) + app.update() # Updates our mapping of PIDs to Windows. This gives us a good view of what diff --git a/core/systems/launcher/running_app.gd b/core/systems/launcher/running_app.gd index b5d6902e..e9a22df4 100644 --- a/core/systems/launcher/running_app.gd +++ b/core/systems/launcher/running_app.gd @@ -7,12 +7,31 @@ class_name RunningApp const Gamescope := preload("res://core/global/gamescope.tres") -## Emitted when the given app has been killed +## Emitted when all child processes of the app are no longer running signal app_killed +## Emitted when the given app is gracefully stopped +signal app_stopped ## Emitted when the window id of the given app has changed signal window_id_changed +## Emitted whenever the windows change for the app +signal window_ids_changed(from: PackedInt32Array, to: PackedInt32Array) ## Emitted when the app id of the given app has changed signal app_id_changed +## Emitted when the app's state has changed +signal state_changed(from: STATE, to: STATE) +## Emitted when the app is focused +signal focus_entered +## Emitted when the app is unfocused +signal focus_exited + +## Possible states the running app can be in +enum STATE { + STARTED, ## App was just started + RUNNING, ## App is running and has an app_id and window_id + MISSING_WINDOW, ## App was running, but now its window cannot be discovered + STOPPING, ## App is being killed gracefully + STOPPED, ## App is no longer running +} ## The LibraryLaunchItem associated with the running application var launch_item: LibraryLaunchItem @@ -24,22 +43,52 @@ var display: String var command: PackedStringArray ## Environment variables that were set with the launched application var environment: Dictionary +## The state of the running app +var state: STATE = STATE.STARTED: + set(v): + var old_state := state + state = v + if old_state != state: + state_changed.emit(old_state, state) +## Time in milliseconds when the app started +var start_time := Time.get_ticks_msec() ## The currently detected window ID of the application var window_id: int: set(v): window_id = v window_id_changed.emit() +## A list of all detected window IDs related to the application +var window_ids: PackedInt32Array = PackedInt32Array(): + set(v): + var old_windows := window_ids + window_ids = v + if old_windows != window_ids: + window_ids_changed.emit(old_windows, window_ids) ## The current app ID of the application var app_id: int: set(v): app_id = v app_id_changed.emit() +## Whether or not the app is currently focused +var focused: bool = false: + set(v): + var old_focus := focused + focused = v + if old_focus == focused: + return + if focused: + focus_entered.emit() + return + focus_exited.emit() ## Whether or not the running app has created at least one valid window var created_window := false -## The number of windows that have been disovered from this app +## The number of windows that have been discovered from this app var num_created_windows := 0 ## Number of times this app has failed its "is_running" check var not_running_count := 0 +## When a steam-launched app has no window, count a few tries before trying +## to close Steam +var steam_close_tries := 0 var logger := Log.get_logger("RunningApp", Log.LEVEL.DEBUG) @@ -49,6 +98,68 @@ func _init(item: LibraryLaunchItem, process_id: int, dsp: String) -> void: display = dsp +## Updates the running app and fires signals +func update() -> void: + # Update all windows related to the app's PID + window_ids = get_all_window_ids() + + # Ensure that all windows related to the app have an app ID set + _ensure_app_id() + + # Ensure that the running app has a corresponding window ID + var has_valid_window := false + if needs_window_id(): + logger.debug("App needs a valid window id") + var id := _discover_window_id() + if id > 0 and window_id != id: + logger.debug("Setting window ID " + str(id) + " for " + launch_item.name) + window_id = id + else: + has_valid_window = true + + # Update the focus state of the app + focused = is_focused() + + # Check if the app, or any of its children, are still running + var running := is_running() + if not running: + not_running_count += 1 + + # Update the running app's state + if not_running_count > 3: + state = STATE.STOPPED + app_killed.emit() + elif state == STATE.STARTED and has_valid_window: + state = STATE.RUNNING + elif state == STATE.RUNNING and not has_valid_window: + state = STATE.MISSING_WINDOW + + var state_str := { + STATE.STARTED: "started", + STATE.RUNNING: "running", + STATE.MISSING_WINDOW: "no window", + STATE.STOPPING: "stopping", + STATE.STOPPED: "stopped" + } + logger.info("Current state: " + state_str[state]) + + # TODO: Check all windows for STEAM_GAME prop + # If this was launched by Steam, try and detect if the game closed + # so we can kill Steam gracefully + if is_steam_app() and state == STATE.MISSING_WINDOW: + logger.debug("Running app is a Steam game and has no valid window ID. The game may have closed.") + # Don't try closing Steam immediately. Wait a few more ticks before attempting + # to close Steam. + if steam_close_tries < 4: + steam_close_tries += 1 + return + var steam_pid := find_steam() + if steam_pid > 0: + logger.info("Trying to stop steam with pid: " + str(steam_pid)) + OS.execute("kill", ["-15", str(steam_pid)]) + + + ## Attempt to discover the window ID from the PID of the given application func get_window_id_from_pid() -> int: var display_type := Gamescope.get_display_type(display) @@ -73,7 +184,7 @@ func get_all_window_ids() -> PackedInt32Array: if window in window_ids: continue window_ids.append(window) - logger.debug(app_name + " found window IDs: " + str(window_ids)) + logger.debug(app_name + " found related window IDs: " + str(window_ids)) return window_ids @@ -93,7 +204,6 @@ func is_running() -> bool: logger.debug("{0} is not running, but lives on in {1}".format([pid, ",".join(pids)])) return true - not_running_count += 1 return false @@ -134,29 +244,44 @@ func get_child_pids() -> PackedInt32Array: return pids +## Returns whether or not the app can be switched to/focused +func can_focus() -> bool: + return window_id > 0 + + ## Return true if the currently running app is focused func is_focused() -> bool: + if not can_focus(): + return false var focused_window := Gamescope.get_focused_window() - if window_id == focused_window: - return true - return false + return window_id == focused_window or focused_window in window_ids + + +## Focuses to the app's window +func grab_focus() -> void: + if not can_focus(): + return + Gamescope.set_baselayer_window(window_id) + focused = true ## Kill the running app func kill(sig: Reaper.SIG = Reaper.SIG.TERM) -> void: + state = STATE.STOPPING + app_stopped.emit() Reaper.reap(pid, sig) ## Iterates through all windows related to the app and sets the app ID property ## so they will appear as focusable windows to Gamescope -func ensure_app_id() -> void: +func _ensure_app_id() -> void: # If this is a Steam app, there's no need to set the app ID; Steam will do # it for us. if is_steam_app(): return # Get all windows associated with the running app - var possible_windows := get_all_window_ids() + var possible_windows := window_ids.duplicate() # Try setting the app ID on each possible Window. If they are valid windows, # gamescope will make these windows available as focusable windows. @@ -184,6 +309,18 @@ func needs_window_id() -> bool: if not window_id in all_windows: logger.debug(str(window_id) + " is not in the list of all windows") return true + + # If this is a Steam app, the only acceptable window will have its STEAM_GAME + # property set. + if is_steam_app(): + var display_type := Gamescope.get_display_type(display) + var steam_app_id := get_meta("steam_app_id") as int + if not Gamescope.has_app_id(window_id, display_type): + logger.debug(str(window_id) + " does not have an app ID already set by Steam") + return true + if Gamescope.get_app_id(window_id) != steam_app_id: + logger.debug(str(window_id) + " has an app ID but it does not match " + str(steam_app_id)) + return true # Track that a window has been successfully detected at least once. if not created_window: @@ -194,7 +331,7 @@ func needs_window_id() -> bool: ## Tries to discover the window ID of the running app -func discover_window_id() -> int: +func _discover_window_id() -> int: # If there's a window directly associated with the PID, return that var win_id := get_window_id_from_pid() if win_id > 0: @@ -202,7 +339,7 @@ func discover_window_id() -> int: return win_id # Get all windows associated with the running app - var possible_windows := get_all_window_ids() + var possible_windows := window_ids.duplicate() # Look for the app window in the list of focusable windows var focusable := Gamescope.get_focusable_windows() @@ -221,6 +358,9 @@ func is_steam_app() -> bool: for arg in args: if arg.contains("steam://rungameid/"): set_meta("is_steam_app", true) + var steam_app_id := arg.split("/")[-1] + if steam_app_id.is_valid_int(): + set_meta("steam_app_id", steam_app_id.to_int()) return true set_meta("is_steam_app", false) return false diff --git a/core/ui/card_ui/navigation/running_game_card.gd b/core/ui/card_ui/navigation/running_game_card.gd index 240c3463..c6445b17 100644 --- a/core/ui/card_ui/navigation/running_game_card.gd +++ b/core/ui/card_ui/navigation/running_game_card.gd @@ -55,7 +55,8 @@ func _ready() -> void: resume_button.pressed.connect(on_resume_game) var on_exit_game := func(): # TODO: Handle this better - launch_manager.stop(launch_manager.get_current_app()) + launch_manager.stop(running_app) + state_machine.pop_state() exit_button.pressed.connect(on_exit_game) diff --git a/core/ui/card_ui/navigation/running_game_card.tscn b/core/ui/card_ui/navigation/running_game_card.tscn index 1588af9d..38c4a191 100644 --- a/core/ui/card_ui/navigation/running_game_card.tscn +++ b/core/ui/card_ui/navigation/running_game_card.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=3] +[gd_scene load_steps=16 format=3 uid="uid://dlouq0b0bnm41"] [ext_resource type="Texture2D" uid="uid://d2ipfga47yjju" path="res://assets/images/empty-grid-logo.png" id="1_4m4go"] [ext_resource type="Script" path="res://core/ui/card_ui/navigation/running_game_card.gd" id="1_vgpef"]