Merge remote-tracking branch 'origin/feat/desktop-steam' into feat/desktop-steam-dolphin

This commit is contained in:
The412Banner committed 2026-09-24 22:24:00 -04:00
commit a28e2ffe72
7 files changed
+347 -21

No files matched your search

@@ -219,7 +219,20 @@ object Library {
"armsx2" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath)
// DuckStation shares PCSX2's flags (src/duckstation-qt/qthost.cpp): the same launch and exit.
"duckstation" -> listOf("-batch", "-bigpicture", "-fullscreen", "--", guestPath)
"dolphin" -> listOf("-e", guestPath)
// Dolphin: full screen, drawn inside its own main window, without its "stop the emulation?"
// question; no warning boxes either, which wait for a click a controller cannot give
// (they still go to Dolphin's log). The guide button is Dolphin's Toggle Fullscreen hotkey
// (bannerlator-pad-defaults), which shows Dolphin's window and its settings - so no -b, which
// hides that window; closing Dolphin ends the session. Under gamescope Dolphin does not
// always see its window as focused, and by default both its hotkeys and the game's
// controller then stop: HotkeysRequireFocus off, BackgroundInput on. -C sets a Dolphin.ini
// value for this run only, so Dolphin started from the desktop keeps its own settings.
"dolphin" -> listOf(
"-C", "Dolphin.Display.Fullscreen=True", "-C", "Dolphin.Display.RenderToMain=True",
"-C", "Dolphin.Interface.ConfirmStop=False", "-C", "Dolphin.Interface.UsePanicHandlers=False",
"-C", "Dolphin.General.HotkeysRequireFocus=False", "-C", "Dolphin.Input.BackgroundInput=True",
"-e", guestPath,
)
"cemu" -> listOf("-g", guestPath)
else -> listOf(guestPath)
}
@@ -16,6 +16,10 @@ import kotlin.math.abs
* - two fingers up / down ............ scroll wheel
* - hold still, then drag ............ left-button drag
* - double-tap, hold, drag ........... left-button drag as well
* - one finger held, another slides .. left-button drag, as on a laptop's pad: the held finger is
* the button, the other moves the pointer (a window by its
* title bar or edge, a desktop icon). The sliding finger may
* lift and slide again; lifting the held one drops.
*
* Positions handed to the listener are the pointer's, in the view's pixels; the caller keeps them
* inside the picture.
@@ -45,6 +49,16 @@ class TouchpadGestures(
private var twoFingers = false
private var scrollAnchorY = 0f
private var scrolled = false
// Hold-and-slide: the first finger stays as the button, the second moves the pointer.
private var anchorId = -1
private var anchorStartX = 0f
private var anchorStartY = 0f
private var moverId = -1
private var moverStartX = 0f
private var moverStartY = 0f
private var moverLastX = 0f
private var moverLastY = 0f
private var holdDrag = false
private val longPress = Runnable {
held = true
dragging = true
@@ -63,6 +77,7 @@ class TouchpadGestures(
lastX = event.x; lastY = event.y
downAt = event.eventTime
travelled = 0f; held = false; twoFingers = false; scrolled = false; secondWasTap = false
anchorId = event.getPointerId(0); moverId = -1; holdDrag = false
// A tap shortly before this touch: holding now drags (double-tap-and-hold).
if (event.eventTime - lastTapAt < DOUBLE_TAP_MS) {
dragging = true
@@ -75,6 +90,17 @@ class TouchpadGestures(
}
MotionEvent.ACTION_POINTER_DOWN -> {
handler.removeCallbacks(longPress)
val i = event.actionIndex
if (moverId == -1 && event.getPointerId(i) != anchorId) {
moverId = event.getPointerId(i)
moverStartX = event.getX(i); moverStartY = event.getY(i)
moverLastX = moverStartX; moverLastY = moverStartY
event.findPointerIndex(anchorId).takeIf { it >= 0 }?.let {
anchorStartX = event.getX(it); anchorStartY = event.getY(it)
}
}
// Mid-drag, a finger coming back only picks the sliding up again.
if (holdDrag) return true
twoFingers = true
secondDownAt = event.eventTime
secondWasTap = true
@@ -82,6 +108,38 @@ class TouchpadGestures(
return true
}
MotionEvent.ACTION_MOVE -> {
val ai = event.findPointerIndex(anchorId)
val mi = if (moverId == -1) -1 else event.findPointerIndex(moverId)
if (ai >= 0 && mi >= 0) {
val mx = event.getX(mi); val my = event.getY(mi)
// One finger still, the other on the move: that is a drag, not a scroll (which
// moves both). Decided once, before anything has scrolled.
if (!holdDrag && !scrolled) {
val anchorMoved = abs(event.getX(ai) - anchorStartX) + abs(event.getY(ai) - anchorStartY)
val moverMoved = abs(mx - moverStartX) + abs(my - moverStartY)
if (moverMoved > slopPx && anchorMoved < slopPx) {
holdDrag = true
secondWasTap = false
if (!dragging) {
dragging = true
listener.onButton(PointerGestures.BTN_LEFT, true, x, y)
}
}
}
if (holdDrag) {
val dx = mx - moverLastX
val dy = my - moverLastY
moverLastX = mx; moverLastY = my
val speed = abs(dx) + abs(dy)
val gain = sensitivity * (1f + minOf(speed / 40f, 1.5f))
place(x + dx * gain, y + dy * gain)
listener.onMove(x, y)
return true
}
} else if (holdDrag) {
// Only the held finger is down: the drag waits for the next slide.
return true
}
if (twoFingers && event.pointerCount >= 2) {
val mid = (event.getY(0) + event.getY(1)) / 2f
val dy = mid - scrollAnchorY
@@ -109,6 +167,30 @@ class TouchpadGestures(
return true
}
MotionEvent.ACTION_POINTER_UP -> {
val gone = event.getPointerId(event.actionIndex)
if (holdDrag) {
if (gone == anchorId) {
// The held finger lifted: drop. The other finger carries on as the pointer.
listener.onButton(PointerGestures.BTN_LEFT, false, x, y)
dragging = false; holdDrag = false; twoFingers = false
anchorId = moverId; moverId = -1
event.findPointerIndex(anchorId).takeIf { it >= 0 }?.let {
lastX = event.getX(it); lastY = event.getY(it)
}
travelled = slopPx + 1f
} else if (gone == moverId) {
moverId = -1
}
return true
}
if (gone == moverId) moverId = -1
if (gone == anchorId) {
// The first finger left a two-finger gesture: the other one is the pointer now.
anchorId = event.getPointerId(if (event.actionIndex == 0) 1 else 0)
event.findPointerIndex(anchorId).takeIf { it >= 0 }?.let {
lastX = event.getX(it); lastY = event.getY(it)
}
}
// The second finger lifting quickly, with nothing scrolled: a right click.
if (twoFingers && secondWasTap && event.eventTime - secondDownAt < TAP_MS && !scrolled) {
listener.onButton(PointerGestures.BTN_RIGHT, true, x, y)
@@ -136,6 +218,7 @@ class TouchpadGestures(
}
}
dragging = false; held = false; twoFingers = false
holdDrag = false; anchorId = -1; moverId = -1
return true
}
}
@@ -170,7 +170,8 @@ class SessionService : Service() {
SessionState.steamUrl = intent?.getStringExtra(EXTRA_STEAM_URL)
// Another Steam client on the device signs ours out seconds after every login; the one that
// does it here runs from boot without being opened. Only the Steam session signs in.
if (SessionState.mode == MODE_STEAM) RivalClients.stopBeforeSession(this)
// The desktop too: its Steam launchers run the client right there (bannerlator-steam-launch).
if (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) RivalClients.stopBeforeSession(this)
SessionState.running = true
SessionState.firstFrameSeen = false
// This session's number, claimed here and not when its process starts: the session it
@@ -303,7 +304,10 @@ class SessionService : Service() {
if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true")
if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1")
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
// The desktop's Steam launchers start the client there (bannerlator-steam-launch), through the
// same set-up as a Steam session: it gets what the client and its games are started with.
val steamHere = SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP
if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
if (SessionState.mode == MODE_STEAM) {
guest.add("BL_GAMESCOPE_FORCE_FULLSCREEN=" + (if (SessionPrefs.forceFullscreen(this)) "1" else "0"))
SessionPrefs.writeForceFullscreenFlag(this)
@@ -315,7 +319,7 @@ class SessionService : Service() {
// The FEXCore preset for the x86 games the client launches: its FEX_* variables go in
// here, before the script, so every game process inherits them from the client. The
// default preset sets nothing, which is what every session ran on before.
if (SessionState.mode == MODE_STEAM) {
if (steamHere) {
val preset = SessionPrefs.fexPreset(this)
val vars = com.droiddeck.launcher.core.FexPreset.env(preset)
vars.forEach { guest.add(it) }
@@ -330,7 +334,7 @@ class SessionService : Service() {
// on, even naming every core: it exists to undo the pin Steam applies to its own interface
// renderer, and the scheduler's default is exactly what that pin takes away. A game's is
// sent only when it is a real restriction - a game has no pin of its own to undo.
if (SessionState.mode == MODE_STEAM && SessionPrefs.clientCpusOverride(this)) {
if (steamHere && SessionPrefs.clientCpusOverride(this)) {
guest.add("BL_CLIENT_CPUS=" + CpuCores.listOrAll(SessionPrefs.clientCpus(this)))
}
// The game cores also pin the desktop and a program from the rail; without a choice the
@@ -432,7 +436,7 @@ class SessionService : Service() {
}
// The user's own games, for the runtime's shortcuts writer to put in the client's library
// before the client starts (see frontend/AddedGames and bannerlator-steam-shortcuts).
if (SessionState.mode == MODE_STEAM) {
if (steamHere) {
val added = com.droiddeck.launcher.frontend.AddedGames.scan(this)
val listing = com.droiddeck.launcher.frontend.AddedGames.writeListing(this, added)
guest.add("BL_ADDED_GAMES=" + listing.path)
@@ -567,7 +571,7 @@ class SessionService : Service() {
// Whether the client signs in to Valve or starts offline: read once, while it starts, and
// rewritten by the client when it exits, so it is set again here at every session start.
if (SessionState.mode == MODE_STEAM) OfflineMode.apply(this, root)
if (SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP) OfflineMode.apply(this, root)
val networkLink = LinuxNetworkLinkComponent(this, root)
networkLink.attach(this)
@@ -383,7 +383,7 @@ Buttons/1 = `Button W`
Buttons/2 = `Button N`
Buttons/- = `Back`
Buttons/+ = `Start`
Buttons/Home = `Guide`
Buttons/Home = `Shoulder R`
D-Pad/Up = `Pad N`
D-Pad/Down = `Pad S`
D-Pad/Left = `Pad W`
@@ -510,10 +510,188 @@ def armsx2_bios(ini):
print(f"bannerlator-pad-defaults: ARMSX2: BIOS {pick}")
# Dolphin (PC) has no controller menu, so the guide button is its Toggle Fullscreen hotkey: out of full
# screen is Dolphin's own window with its Config / Graphics / Stop buttons. Hotkeys.ini is all-or-
# nothing - a file with one line drops every other default - so a new one carries Dolphin's defaults,
# generated from HotkeyManager.cpp's LoadDefaults (Linux branch, RetroAchievements on) plus the guide;
# an existing one only gets the guide added to its Toggle Fullscreen. The Wii remote's Home, which
# was on the guide button, is on the right shoulder button instead.
DOLPHIN_GUIDE = "`SDL/0/Xbox 360 Controller:Guide`"
DOLPHIN_HOTKEYS = """[Hotkeys]
Device = XInput2/0/Virtual core pointer
General/Open = @(Ctrl+O)
General/Toggle Pause = F10
General/Stop = Escape
General/Toggle Fullscreen = @(Alt+Return) | `SDL/0/Xbox 360 Controller:Guide`
General/Take Screenshot = F9
General/Open Achievements = @(Alt+A)
Emulation Speed/Disable Emulation Speed Limit = Tab
Stepping/Step Into = F11
Stepping/Step Over = @(Shift+F10)
Stepping/Step Out = @(Shift+F11)
Breakpoint/Toggle Breakpoint = @(Shift+F9)
Wii/Connect Wii Remote 1 = @(Alt+F5)
Wii/Connect Wii Remote 2 = @(Alt+F6)
Wii/Connect Wii Remote 3 = @(Alt+F7)
Wii/Connect Wii Remote 4 = @(Alt+F8)
Wii/Connect Balance Board = @(Alt+F9)
Load State/Load State Slot 1 = F1
Load State/Load State Slot 2 = F2
Load State/Load State Slot 3 = F3
Load State/Load State Slot 4 = F4
Load State/Load State Slot 5 = F5
Load State/Load State Slot 6 = F6
Load State/Load State Slot 7 = F7
Load State/Load State Slot 8 = F8
Save State/Save State Slot 1 = @(Shift+F1)
Save State/Save State Slot 2 = @(Shift+F2)
Save State/Save State Slot 3 = @(Shift+F3)
Save State/Save State Slot 4 = @(Shift+F4)
Save State/Save State Slot 5 = @(Shift+F5)
Save State/Save State Slot 6 = @(Shift+F6)
Save State/Save State Slot 7 = @(Shift+F7)
Save State/Save State Slot 8 = @(Shift+F8)
Other State Hotkeys/Undo Load State = F12
Other State Hotkeys/Undo Save State = @(Shift+F12)
GBA Core/Load ROM = @(`Ctrl`+`Shift`+`O`)
GBA Core/Unload ROM = @(`Ctrl`+`Shift`+`W`)
GBA Core/Reset = @(`Ctrl`+`Shift`+`R`)
GBA Volume/Volume Down = `KP_Subtract`
GBA Volume/Volume Up = `KP_Add`
GBA Volume/Volume Toggle Mute = `M`
GBA Window Size/1x = `KP_1`
GBA Window Size/2x = `KP_2`
GBA Window Size/3x = `KP_3`
GBA Window Size/4x = `KP_4`
USB Emulation Devices/Show Skylanders Portal = @(Ctrl+P)
USB Emulation Devices/Show Infinity Base = @(Ctrl+I)
"""
DOLPHIN_APPIMAGE = "/opt/appimages/dolphin.AppImage"
DOLPHIN_DATA = "/usr/share/dolphin-emu"
def dolphin_data():
"""The Dolphin build in the catalog finds its own data - the GameCube fonts, the per-game fixes
(GameSettings: GUT.ini puts Ultimate Spider-Man on the MMU), its icons and shaders - only at the
fixed /usr/share/dolphin-emu it was built for; there is no setting or variable to move it. Here it
runs from a copy unpacked under /tmp (no FUSE), so it found none of it: a GameCube game stopped
on "Windows-1252 fonts are not loaded", another on an invalid write the missing game fix would
have prevented. So the folder is unpacked from the AppImage to that path, once per AppImage."""
import shutil, subprocess, tempfile
try:
st = os.stat(DOLPHIN_APPIMAGE)
except OSError:
return
stamp = "%d:%d" % (st.st_size, int(st.st_mtime))
marker = os.path.join(DOLPHIN_DATA, ".droiddeck-from")
try:
if open(marker).read() == stamp and os.path.isfile(os.path.join(DOLPHIN_DATA, "Sys/GC/font_western.bin")):
return
except OSError:
pass
work = tempfile.mkdtemp(prefix="dolphin-data-", dir="/tmp")
try:
subprocess.run([DOLPHIN_APPIMAGE, "--appimage-extract", "usr/share/dolphin-emu/*"], cwd=work,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=600)
src = os.path.join(work, "squashfs-root/usr/share/dolphin-emu")
if not os.path.isfile(os.path.join(src, "Sys/GC/font_western.bin")):
print("bannerlator-pad-defaults: Dolphin: could not unpack its data from the AppImage")
return
new = DOLPHIN_DATA + ".new"
shutil.rmtree(new, ignore_errors=True)
shutil.move(src, new)
with open(os.path.join(new, ".droiddeck-from"), "w") as f:
f.write(stamp)
shutil.rmtree(DOLPHIN_DATA, ignore_errors=True)
os.rename(new, DOLPHIN_DATA)
print("bannerlator-pad-defaults: Dolphin: its data (fonts, per-game fixes) in " + DOLPHIN_DATA)
except (OSError, subprocess.SubprocessError) as e:
print("bannerlator-pad-defaults: Dolphin: unpacking its data failed: %s" % e)
finally:
shutil.rmtree(work, ignore_errors=True)
def dolphin():
if not os.path.isfile(DOLPHIN_APPIMAGE):
return
dolphin_data()
d = os.path.join(CONFIG, "dolphin-emu")
seed(os.path.join(d, "GCPadNew.ini"), DOLPHIN_GC, "Dolphin (GameCube)")
seed(os.path.join(d, "WiimoteNew.ini"), DOLPHIN_WII, "Dolphin (Wii)")
wii_pad = os.path.join(d, "WiimoteNew.ini")
if ini_get(wii_pad, "Wiimote1", "Buttons/Home") == "`Guide`":
ini_set(wii_pad, "Wiimote1", "Buttons/Home", "`Shoulder R`")
print("bannerlator-pad-defaults: Dolphin (Wii): Home on the right shoulder button (guide = Dolphin's window)")
hotkeys = os.path.join(d, "Hotkeys.ini")
if not os.path.isfile(hotkeys):
seed(hotkeys, DOLPHIN_HOTKEYS, "Dolphin", "the guide button shows Dolphin's window (Toggle Fullscreen)")
else:
current = ini_get(hotkeys, "Hotkeys", "General/Toggle Fullscreen")
if current is not None and DOLPHIN_GUIDE not in current:
ini_set(hotkeys, "Hotkeys", "General/Toggle Fullscreen",
(current + " | " if current else "") + DOLPHIN_GUIDE)
print("bannerlator-pad-defaults: Dolphin: the guide button shows Dolphin's window (Toggle Fullscreen)")
# Games from the ROMs folder's gamecube and wii folders, subfolders included; Vulkan; no
# NKit warning (it will not let a game start until a box is ticked - games run either way,
# the warning is about loading times, netplay and save states); no statistics question.
# Names from Dolphin's MainSettings.cpp / UISettings.cpp. Only what is not set yet is added,
# except the game folders, which are added to whatever the user has.
ini = os.path.join(d, "Dolphin.ini")
gc = system_folder(("gamecube", "gc", "ngc"), "gamecube")
wii = system_folder(("wii",), "wii")
if not os.path.isfile(ini):
os.makedirs(d, exist_ok=True)
with open(ini, "w", encoding="utf-8") as f:
f.write("[Analytics]\nEnabled = False\nPermissionAsked = True\n"
"[Core]\nGFXBackend = Vulkan\n"
"[General]\nISOPaths = 2\nISOPath0 = %s\nISOPath1 = %s\nRecursiveISOPaths = True\n"
"[Interface]\nSkipNKitWarning = True\n" % (gc, wii))
print("bannerlator-pad-defaults: Dolphin: first-time setup (Vulkan, games from %s and %s)" % (gc, wii))
return
try:
count = int(ini_get(ini, "General", "ISOPaths") or "0")
except ValueError:
count = 0
paths = [ini_get(ini, "General", "ISOPath%d" % i) for i in range(count)]
changed = []
# The whole ROMs folder as a game folder (what Dolphin's "set a games directory" was pointed at
# before) would, searched with its subfolders, have Dolphin open every PS2 and PS3 image too;
# the system folders below cover it.
if any((p or "").rstrip("/") == ROMS for p in paths):
paths = [p for p in paths if p and p.rstrip("/") != ROMS]
for i, p in enumerate(paths):
ini_set(ini, "General", "ISOPath%d" % i, p)
for i in range(len(paths), count):
ini_set(ini, "General", "ISOPath%d" % i, "")
count = len(paths)
ini_set(ini, "General", "ISOPaths", str(count))
changed.append("not the whole ROMs folder")
for folder in (gc, wii):
if folder not in paths:
ini_set(ini, "General", "ISOPath%d" % count, folder)
count += 1
paths.append(folder)
changed.append(folder)
if changed:
ini_set(ini, "General", "ISOPaths", str(count))
if ini_get(ini, "General", "RecursiveISOPaths") != "True":
ini_set(ini, "General", "RecursiveISOPaths", "True")
changed.append("subfolders")
if changed:
print("bannerlator-pad-defaults: Dolphin: games from " + ", ".join(changed))
# Once, for a Dolphin set up before this: Vulkan unless it was chosen already, and the NKit box.
marker = os.path.join(d, ".droiddeck-setup")
if os.path.exists(marker):
return
if not ini_get(ini, "Core", "GFXBackend"):
ini_set(ini, "Core", "GFXBackend", "Vulkan")
ini_set(ini, "Interface", "SkipNKitWarning", "True")
if ini_get(ini, "Analytics", "PermissionAsked") != "True":
ini_set(ini, "Analytics", "Enabled", "False")
ini_set(ini, "Analytics", "PermissionAsked", "True")
open(marker, "w").close()
print("bannerlator-pad-defaults: Dolphin: Vulkan, no NKit warning")
# Cemu: an SDLController profile for the GamePad. The uuid is "<index>_<SDL GUID>" and SDL's GUID
@@ -350,7 +350,8 @@ case $mode in
# The drawer's "Stretch games to fill the screen" works live: the app writes its choice to
# ~/.droiddeck-fill, and each change goes to gamescope as GAMESCOPE_FORCE_WINDOWS_FULLSCREEN on
# the root window, which it watches. What gamescope was started with is where this begins.
(
# Not on the desktop (BL_DESKTOP): no gamescope there, and the loop would outlive the client.
[ -z "${BL_DESKTOP:-}" ] && (
last=${BL_GAMESCOPE_FORCE_FULLSCREEN:-1}
while :; do
sleep 1
@@ -515,7 +516,17 @@ BUSCONF
# The URL arguments were acted on the first time; a restart must not repeat them.
set --
done
reap_orphans
if [ -n "${BL_DESKTOP:-}" ]; then
# Started from the desktop (bannerlator-steam-launch): the desktop's own programs share this
# script's parent, so reap_orphans - right under gamescope, where everything is the client's -
# is not for here. The helpers this run started (the cores and redistributables beats, the
# system bus and the NetworkManager stand-in) go with the client instead.
_bl_jobs=$(jobs -p)
[ -n "$_bl_jobs" ] && kill $_bl_jobs 2>/dev/null
echo "== steam: the client on the desktop has exited"
else
reap_orphans
fi
exit "$rc"
;;
*)
@@ -1,12 +1,18 @@
#!/bin/bash
# Asks the app to start the Steam client, from inside a session (the desktop's menu entries and
# the per-game launchers bannerlator-desktop-games writes):
# Starts the Steam client from inside a session (the desktop's menu entries and the per-game
# launchers bannerlator-desktop-games writes):
# bannerlator-steam-launch desktop [appid|steam-url] Steam's desktop UI, optionally a game
# bannerlator-steam-launch bigpicture [appid|steam-url] Big Picture
# The client cannot run on the desktop: labwc composites in software and offers no dma-buf, so a
# game (or the client's own Vulkan window) would die at its first frame. It runs under gamescope,
# which is a session of its own - so this writes a request into the directory the app watches,
# and the app ends the desktop session and starts a Steam session in its place.
#
# On a desktop that draws with the GPU (labwc on vulkan or gles2 - the default since the GPU
# desktop) the client runs right there: a window on the desktop, through the desktop's Xwayland,
# with the same set-up a Steam session gives it (bannerlator-session steam, minus gamescope), and
# its games open in windows of their own. A client already running there gets the request instead
# of a second one being started.
#
# On a software desktop (pixman: the GPU renderers failed on this device) there is no dma-buf, so a
# game - or the client's own Vulkan window - would die at its first frame. There the request goes
# to the app, which ends the desktop and starts a Steam session under gamescope in its place.
set -u
ui=${1:-desktop}
target=${2:-}
@@ -18,6 +24,30 @@ case $target in
*[!0-9]*) echo "not an app id or steam:// URL: $target" >&2; exit 64 ;;
*) url=steam://rungameid/$target ;;
esac
steam_root=${XDG_DATA_HOME:-${HOME:-/root}/.local/share}/Steam
client=$steam_root/steamrtarm64/steam
if [ "${BL_DESKTOP:-}" = 1 ] && [ -n "${WLR_RENDERER:-}" ] && [ "$WLR_RENDERER" != pixman ]; then
log=${BL_DEBUG_DIR:-$HOME/.cache/droiddeck}/steam-desktop.log
mkdir -p "$(dirname "$log")" 2>/dev/null
if pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1 && [ -x "$client" ]; then
# One client at a time: the running one takes the request (a second start hands its command
# line over and exits). No game asked for = switch it to the UI that was clicked.
[ -n "$url" ] || { [ "$ui" = bigpicture ] && url=steam://open/bigpicture || url=steam://open/main; }
echo "bannerlator-steam-launch: the client is running on the desktop; handing it $url" | tee -a "$log"
exec "$client" "$url"
fi
echo "bannerlator-steam-launch: starting the Steam client on the desktop ($ui${url:+, $url}); log: $log"
# BL_INSIDE: the session script's Steam start-up without gamescope in front of it. The desktop UI
# unless Big Picture was asked for; never Deck mode (its Quick Access Menu needs gamescope).
export BL_INSIDE=1 BL_STEAMDECK=0
if [ "$ui" = desktop ]; then export BL_STEAM_UI=desktop; else unset BL_STEAM_UI; fi
setsid /usr/local/bin/bannerlator-session steam ${url:+"$url"} >>"$log" 2>&1 < /dev/null &
exit 0
fi
# The software desktop, or outside the desktop: ask the app for a Steam session.
dir=${BL_LAUNCH_DIR:-}
if [ -z "$dir" ] || [ ! -d "$dir" ]; then
echo "bannerlator-steam-launch: not inside a DroidDeck session (BL_LAUNCH_DIR unset)" >&2
@@ -1,12 +1,19 @@
#!/bin/bash
# `steam` on the desktop. The client writes a menu entry per installed game that runs
# `steam steam://rungameid/<id>`, and its own launchers say `steam`; neither can run the client
# here (no dma-buf on the desktop), so on the desktop this hands off to the app for a Steam session
# in the client's desktop UI under gamescope. Anywhere else it is the client itself.
# `steam steam://rungameid/<id>`, and its own launchers say `steam`. On the desktop that goes
# through bannerlator-steam-launch - which runs the client on the desktop itself when it draws with
# the GPU, hands the URL to a client already running there, and asks the app for a Steam session
# on a software desktop. Anywhere else it is the client itself.
set -u
if [ "${BL_DESKTOP:-}" = 1 ]; then
url=
for arg in "$@"; do case $arg in steam://*) url=$arg ;; esac; done
exec /usr/local/bin/bannerlator-steam-launch desktop ${url:+"$url"}
ui=desktop
for arg in "$@"; do
case $arg in
steam://*) url=$arg ;;
-gamepadui|-bigpicture) ui=bigpicture ;;
esac
done
exec /usr/local/bin/bannerlator-steam-launch "$ui" ${url:+"$url"}
fi
exec "${HOME:-/root}/.local/share/Steam/steamrtarm64/steam" "$@"