Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 159 additions & 14 deletions core/global/launch_manager.gd
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ signal all_apps_stopped()
signal app_launched(app: RunningApp)
signal app_stopped(app: RunningApp)
signal app_switched(from: RunningApp, to: RunningApp)
signal app_lifecycle_progressed(progress: float, type: AppLifecycleHook.TYPE)
signal app_lifecycle_notified(text: String, type: AppLifecycleHook.TYPE)
signal recent_apps_changed()

const settings_manager := preload("res://core/global/settings_manager.tres")
const notification_manager := preload("res://core/global/notification_manager.tres")
const library_manager := preload("res://core/global/library_manager.tres")

var gamescope := preload("res://core/systems/gamescope/gamescope.tres") as GamescopeInstance
var input_plumber := load("res://core/systems/input/input_plumber.tres") as InputPlumberInstance
Expand Down Expand Up @@ -125,12 +128,28 @@ func _init() -> void:
self.check_running.call_deferred()
# If focusable apps has changed and the currently focused app no longer exists,
# remove the manual focus
var baselayer_app := _xwayland_primary.baselayer_app
to.append(_xwayland_primary.focused_app)
if baselayer_app > 0 and not baselayer_app in to:
_xwayland_primary.remove_baselayer_app()
const keep_app_ids := [GamescopeInstance.EXTRA_UNKNOWN_GAME_ID, GamescopeInstance.OVERLAY_GAME_ID]
var baselayer_apps := _xwayland_primary.baselayer_apps
var new_baselayer_apps := PackedInt64Array()
for app_id in baselayer_apps:
if app_id in keep_app_ids:
new_baselayer_apps.push_back(app_id)
continue
if app_id in to:
new_baselayer_apps.push_back(app_id)
if new_baselayer_apps != baselayer_apps:
_xwayland_primary.baselayer_apps = new_baselayer_apps
_xwayland_primary.focusable_apps_updated.connect(on_focusable_apps_changed)

# Listen for when focusable windows change
var on_focusable_windows_changed := func(_from: PackedInt64Array, to: PackedInt64Array):
# If focusable windows has changed and the currently focused window no longer exists,
# remove the manual focus
var baselayer_window := _xwayland_primary.baselayer_window
if baselayer_window > 0 and not baselayer_window in to:
_xwayland_primary.remove_baselayer_window()
_xwayland_primary.focusable_windows_updated.connect(on_focusable_windows_changed)

# Listen for signals from the secondary Gamescope XWayland
if _xwayland_game:
# Listen for window created/destroyed events
Expand Down Expand Up @@ -196,20 +215,57 @@ func _save_persist_data():
## Launches the given application and switches to the in-game state. Returns a
## [RunningApp] instance of the application.
func launch(app: LibraryLaunchItem) -> RunningApp:
# Create a running app from the launch item
var running_app := _launch(app)

# Add the running app to our list and change to the IN_GAME state
_add_running(running_app)
state_machine.set_state([in_game_state])
_update_recent_apps(app)

# Execute any pre-launch hooks and start the app
await _execute_hooks(app, AppLifecycleHook.TYPE.PRE_LAUNCH)
running_app.start()

# Call any hooks at different points in the app's lifecycle
var on_app_state_changed := func(_from: RunningApp.STATE, to: RunningApp.STATE):
if to == RunningApp.STATE.RUNNING:
_execute_hooks(app, AppLifecycleHook.TYPE.LAUNCH)
elif to == RunningApp.STATE.STOPPED:
_execute_hooks(app, AppLifecycleHook.TYPE.EXIT)
running_app.state_changed.connect(on_app_state_changed)

# Focus any new windows that get created
var switch_to_new_window := func(old_windows: PackedInt64Array, new_windows: PackedInt64Array):
for window in new_windows:
if window in old_windows:
continue
running_app.switch_window(window)
break
running_app.window_ids_changed.connect(switch_to_new_window)

# Remove/restore focus if any windows get removed
var remove_focus := func(old_windows: PackedInt64Array, new_windows: PackedInt64Array):
if not _xwayland_primary:
return
var focused_window := _xwayland_primary.baselayer_window
if not focused_window in old_windows:
return
if new_windows.is_empty():
_xwayland_primary.remove_baselayer_window()
return
var new_window := new_windows[0]
running_app.switch_window(new_window)
running_app.window_ids_changed.connect(remove_focus)

return running_app


## Launches the given app in the background. Returns the [RunningApp] instance.
func launch_in_background(app: LibraryLaunchItem) -> RunningApp:
# Start the application
var running_app := _launch(app)
running_app.start()

# Listen for app state changes
var on_app_state_changed := func(from: RunningApp.STATE, to: RunningApp.STATE):
Expand All @@ -224,6 +280,37 @@ func launch_in_background(app: LibraryLaunchItem) -> RunningApp:
return running_app


## Executes application lifecycle hooks for the given app
func _execute_hooks(app: LibraryLaunchItem, type: AppLifecycleHook.TYPE) -> void:
var library := library_manager.get_library_by_id(app._provider_id)
if not library:
logger.warn("Unable to find library for app:", app)
return
var hooks := library.get_app_lifecycle_hooks()

# Filter based on hook type
var hooks_to_run: Array[AppLifecycleHook] = []
for item in hooks:
if item.get_type() != type:
continue
hooks_to_run.push_back(item)

# Emit signals if the hook has progress
var on_hook_progress := func(progress: float):
self.app_lifecycle_progressed.emit(progress, type)
var on_hook_notify := func(text: String):
self.app_lifecycle_notified.emit(text, type)

# Run each hook and emit signals on hook progress
for hook in hooks_to_run:
logger.info("Executing lifecycle hook:", hook)
hook.progressed.connect(on_hook_progress)
hook.notified.connect(on_hook_notify)
await hook.execute(app)
hook.notified.disconnect(on_hook_notify)
hook.progressed.disconnect(on_hook_progress)


## Launches the given app
func _launch(app: LibraryLaunchItem) -> RunningApp:
var cmd: String = app.command
Expand Down Expand Up @@ -283,9 +370,8 @@ func _launch(app: LibraryLaunchItem) -> RunningApp:
command.append_array(args)
logger.info("Launching game with command: {0} {1}".format([exec, str(command)]))

# Launch the application process
var running_app := RunningApp.spawn(app, env, exec, command)
logger.info("Launched with PID: {0}".format([running_app.pid]))
# Create the running app instance, but do not start it yet.
var running_app := RunningApp.create(app, env, exec, command)

return running_app

Expand Down Expand Up @@ -466,6 +552,7 @@ func _on_app_state_changed(from: RunningApp.STATE, to: RunningApp.STATE, app: Ru
logger.debug("Currently running apps:", _running)
if state_machine.has_state(in_game_state) and _running.size() == 0:
logger.info("No more apps are running. Removing in-game state.")
_current_app = null
_xwayland_primary.remove_baselayer_window()
state_machine.remove_state(in_game_state)
state_machine.remove_state(in_game_menu_state)
Expand All @@ -475,6 +562,8 @@ func _on_app_state_changed(from: RunningApp.STATE, to: RunningApp.STATE, app: Ru
# Removes the given PID from our list of running apps
func _remove_running(app: RunningApp):
logger.info("Removing app", app, "from running apps.")
if app == _current_app:
_current_app = null
_running.erase(app)
_apps_by_name.erase(app.launch_item.name)
_apps_by_pid.erase(app.pid)
Expand All @@ -485,28 +574,83 @@ func _remove_running(app: RunningApp):
func check_running() -> void:
# Find the root window
if not _xwayland_game:
logger.warn("No XWayland instance exists to check for running apps")
return
var root_id := _xwayland_game.root_window_id
if root_id < 0:
return

# Get all windows and their geometry
var all_windows := _xwayland_game.get_all_windows(root_id)
var all_window_sizes := _xwayland_game.get_window_sizes(all_windows)
logger.trace("Found windows:", all_windows)
logger.trace("Found window sizes:", all_window_sizes)

# Only consider valid windows of a certain size
var all_valid_windows := PackedInt64Array()
var i := 0
for window in all_windows:
var size := all_window_sizes[i]
if size.x > 20 and size.y > 20:
all_valid_windows.push_back(window)
i += 1
logger.trace("Found valid windows:", all_valid_windows)

# Get a list of all running processes
var all_pids := Reaper.get_pids()

# All OGUI processes should have an OGUI_ID environment variable set. Look
# at every running process and sort them by OGUI_ID.
var ogui_id_to_pids: Dictionary[String, PackedInt64Array] = {}
for pid in all_pids:
var env := Reaper.get_pid_environment(pid)
if not env.is_empty():
logger.trace("Found environment for pid", pid, ":", env)
if not "OGUI_ID" in env:
continue
var id := env["OGUI_ID"] as String
if not id in ogui_id_to_pids:
ogui_id_to_pids[id] = PackedInt64Array()
ogui_id_to_pids[id].push_back(pid)
if !ogui_id_to_pids.is_empty():
logger.debug("Running processes:", ogui_id_to_pids)

# Update our view of running processes and what windows they have
_update_pids(root_id)
_update_pids(all_valid_windows)

# Update the state of all running apps
for app in _running:
app.update()
var app_pids := PackedInt64Array()
if app.ogui_id in ogui_id_to_pids:
app_pids = ogui_id_to_pids[app.ogui_id]
app.update(all_valid_windows, app_pids)
for app in _running_background:
app.update()
var app_pids := PackedInt64Array()
if app.ogui_id in ogui_id_to_pids:
app_pids = ogui_id_to_pids[app.ogui_id]
app.update(all_valid_windows, app_pids)

# Look for orphan windows
var windows_with_app := PackedInt64Array()
var orphan_windows := PackedInt64Array()
for app in _running:
windows_with_app.append_array(app.window_ids.duplicate())
for app in _running_background:
windows_with_app.append_array(app.window_ids.duplicate())
for window in all_valid_windows:
if window in windows_with_app:
continue
orphan_windows.push_back(window)
if not orphan_windows.is_empty():
logger.warn("Found orphan windows:", orphan_windows)


# Updates our mapping of PIDs to Windows. This gives us a good view of what
# processes are running, and what windows they have.
func _update_pids(root_id: int):
func _update_pids(all_windows: PackedInt64Array):
if not _xwayland_game:
return
var pids := {}
var all_windows := _xwayland_game.get_all_windows(root_id)
for window in all_windows:
var window_pids := _xwayland_game.get_pids_for_window(window)
for window_pid in window_pids:
Expand Down Expand Up @@ -661,7 +805,7 @@ func _make_running_app_from_process(name: String, pid: int, window_id: int, app_
# Creates a new RunningApp instance from a given LibraryLaunchItem, PID, and
# xwayland instance.
func _make_running_app(launch_item: LibraryLaunchItem, pid: int, display: String) -> RunningApp:
var running_app: RunningApp = RunningApp.new(launch_item, pid, display)
var running_app: RunningApp = RunningApp.new(launch_item, display)
running_app.launch_item = launch_item
running_app.pid = pid
running_app.display = display
Expand All @@ -670,8 +814,9 @@ func _make_running_app(launch_item: LibraryLaunchItem, pid: int, display: String

# Returns the parent app if the focused app is a child of a currently running app.
func _get_app_from_running_pid_groups(pid: int) -> RunningApp:
var pids_with_ogui_id := PackedInt64Array()
for app in _running:
if pid in app.get_child_pids():
if pid in app.get_child_pids(pids_with_ogui_id):
return app
return null

Expand Down
2 changes: 1 addition & 1 deletion core/global/plugin_loader.gd
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class_name PluginLoader
## pack.

const PLUGIN_STORE_URL = "https://raw.githubusercontent.com/ShadowBlip/OpenGamepadUI-plugins/main/plugins.json"
const PLUGIN_API_VERSION = "1.1.0"
const PLUGIN_API_VERSION = "1.2.0"
const PLUGINS_DIR = "user://plugins"
const LOADED_PLUGINS_DIR = "res://plugins"
const REQUIRED_META = ["plugin.name", "plugin.version", "plugin.min-api-version", "entrypoint"]
Expand Down
26 changes: 26 additions & 0 deletions core/systems/launcher/app_lifecycle_hook.gd
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ class_name AppLifecycleHook
## the ability to execute actions when apps are about to start, have started,
## or have exited.

## Emit this signal if you want to indicate progression of the hook.
signal progressed(percent: float)
## Emit this signal whenever you want custom text to be displayed
signal notified(text: String)

## The type of hook determines where in the application's lifecycle this hook
## should be executed.
enum TYPE {
Expand All @@ -26,6 +31,11 @@ func _init(hook_type: TYPE) -> void:
_hook_type = hook_type


## Name of the lifecycle hook
func get_name() -> String:
return ""


## Executes whenever an app from this library reaches the stage in its lifecycle
## designated by the hook type. E.g. a `PRE_LAUNCH` hook will have this method
## called whenever an app is about to launch.
Expand All @@ -37,3 +47,19 @@ func execute(item: LibraryLaunchItem) -> void:
## the hook should be executed.
func get_type() -> TYPE:
return _hook_type


func _to_string() -> String:
var kind: String
match self.get_type():
TYPE.PRE_LAUNCH:
kind = "PreLaunch"
TYPE.LAUNCH:
kind = "Launch"
TYPE.EXIT:
kind = "Exit"
var name := self.get_name()
if name.is_empty():
name = "Anonymous"

return "<AppLifecycleHook.{0}-{1}>".format([kind, name])
Loading