Compare commits

...
26 changed files with 1108 additions and 315 deletions

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -22,8 +22,6 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
CrashHandler.install(this)
// Before anything else touches the session prefs, so the first session already runs in Deck mode.
com.droiddeck.launcher.session.SessionPrefs.settleDeckModeDefault(this)
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
com.droiddeck.launcher.update.AppUpdates.init(this)
GpuClockPin.clearLeftover(this)
@@ -121,7 +121,6 @@ class MainActivity : ComponentActivity() {
private var showNonAdreno by mutableStateOf<LinuxRuntimeInstaller.Release?>(null)
private var glThread by mutableStateOf(true)
private var noGlError by mutableStateOf(true)
private var steamDeckMode by mutableStateOf(false)
private var steamRepairQueued by mutableStateOf(false)
private var mangoapp by mutableStateOf(true)
private var steamController by mutableStateOf(SessionPrefs.CONTROLLER_DECK)
@@ -344,7 +343,6 @@ class MainActivity : ComponentActivity() {
private var resolution by mutableStateOf(com.droiddeck.launcher.session.SessionDisplay.DEFAULT_RESOLUTION)
private var fexPreset by mutableStateOf("")
private var forceSsbs by mutableStateOf(false)
private var steamChannel by mutableStateOf("steamdeck_publicbeta")
private var runSteamAtStartup by mutableStateOf(false)
private var theme by mutableStateOf("graphite")
private var appScale by mutableStateOf(com.droiddeck.launcher.core.AppUiPrefs.DEFAULT_SCALE)
@@ -1301,8 +1299,6 @@ class MainActivity : ComponentActivity() {
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
forceSsbs = forceSsbs,
syncBackend = if (mode == SessionService.MODE_STEAM) SessionPrefs.syncBackendOf(fastSync, fsyncFirst, syncFallback) else null,
steamChannel = if (mode == SessionService.MODE_STEAM) steamChannel else null,
steamDeckMode = mode == SessionService.MODE_STEAM && steamDeckMode,
steamRepairQueued = steamRepairQueued,
mangoapp = mangoapp,
steamController = if (mode == SessionService.MODE_STEAM) steamController else null,
@@ -1380,13 +1376,7 @@ class MainActivity : ComponentActivity() {
fsyncFirst = SessionPrefs.fsyncFirst(this)
syncFallback = SessionPrefs.syncFallback(this)
},
onSteamChannel = { id -> SessionPrefs.setSteamChannel(this, id); steamChannel = id },
onSteamRepair = { steamRepairQueued = SteamRepair.queue(this) },
onSteamDeckMode = { on ->
SessionPrefs.setSteamDeckMode(this, on)
steamDeckMode = on
steamChannel = SessionPrefs.steamChannel(this)
},
onMangoapp = { on -> SessionPrefs.setMangoapp(this, on); mangoapp = on },
onSteamController = { id -> SessionPrefs.setSteamController(this, id); steamController = id },
onRunSteamAtStartup = { on ->
@@ -1519,8 +1509,6 @@ class MainActivity : ComponentActivity() {
fastSync = SessionPrefs.fastSync(this)
fsyncFirst = SessionPrefs.fsyncFirst(this)
syncFallback = SessionPrefs.syncFallback(this)
steamChannel = SessionPrefs.steamChannel(this)
steamDeckMode = SessionPrefs.steamDeckMode(this)
steamRepairQueued = SteamRepair.queued(this)
mangoapp = SessionPrefs.mangoapp(this)
steamController = SessionPrefs.steamController(this)
@@ -1579,7 +1567,6 @@ class MainActivity : ComponentActivity() {
clientOverride = SessionPrefs.clientCpusOverride(this)
glThread = SessionPrefs.glThread(this)
noGlError = SessionPrefs.noGlError(this)
steamDeckMode = SessionPrefs.steamDeckMode(this)
clientCores = CpuCores.parse(SessionPrefs.clientCpus(this)).ifEmpty { CpuCores.all.toSet() }
gameCores = CpuCores.parse(SessionPrefs.gameCpus(this)).ifEmpty { CpuCores.all.toSet() }
tuSysmem = SessionPrefs.tuSysmem(this)
@@ -138,6 +138,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private fun openDeckRing(why: String) {
val deck = SessionState.deckPad
val opened = if (deck) padBridge?.start() else null
// Every pad already connected is a player from the start, before its first press.
padBridge?.claimConnected()
Log.i(TAG, "deck ring ($why): deck pad $deck" + when (opened) {
null -> ", left for first input"
true -> ", ring open"
@@ -250,6 +252,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private var drawerPage by mutableStateOf(SessionDrawerPage.CONTROLLER)
private var drawerControllerActive by mutableStateOf(false)
private var backActionsInverted by mutableStateOf(false)
/** What the pad is to the Steam client (SessionPrefs.CONTROLLER_*), switched from the drawer. */
private var padType by mutableStateOf(SessionPrefs.CONTROLLER_DECK)
/** The on-screen PC keyboard (ui/PcKeyboard): real key presses, Esc and F1 included. */
private var pcKeyboardOpen by mutableStateOf(false)
private var hudOn by mutableStateOf(true)
@@ -308,6 +312,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
private val deviceListener = object : InputManager.InputDeviceListener {
override fun onInputDeviceAdded(deviceId: Int) {
logInputDevice("connected", deviceId)
padBridge?.claimConnected()
updateOnScreenControls()
updatePointerCapture()
}
@@ -587,6 +592,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
onKeyboardButton = { on -> updateControllerPrefs { ControllerPrefs.setKeyboardButton(this@SessionActivity, on) } },
onSteamMenu = if (SessionState.mode == SessionService.MODE_STEAM) ({ sendSteamGuide() }) else null,
onQam = if (SessionState.mode == SessionService.MODE_STEAM) ({ triggerSteamQam() }) else null,
// Only where the session set the pad up to be switched (SessionService: deckPad).
padType = if (SessionState.mode == SessionService.MODE_STEAM && SessionState.deckPad) padType else null,
onPadType = { id ->
SessionPrefs.setSteamController(this@SessionActivity, id)
com.droiddeck.launcher.session.SteamDeckPad.showPad(SessionPrefs.padType(id))
com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(id))
padType = id
},
backActionsInverted = backActionsInverted,
onBackActionsInverted = { inverted ->
SessionPrefs.setBackActionsInverted(this@SessionActivity, inverted)
@@ -847,6 +860,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
steamDownloadsInBackground = SessionPrefs.steamDownloadsInBackground(this)
oscMode = SessionPrefs.oscMode(this)
backActionsInverted = SessionPrefs.backActionsInverted(this)
padType = SessionPrefs.steamController(this)
}
// ── Compositor ──────────────────────────────────────────────────────────────────────────
@@ -152,7 +152,6 @@ object DeviceReport {
k("Zink lazy descriptors", SessionPrefs.zinkLazy(context))
k("Threaded GL (glthread)", SessionPrefs.glThread(context))
k("No GL error checks", SessionPrefs.noGlError(context))
k("Steam Deck mode", SessionPrefs.steamDeckMode(context))
k("Steam controller", SessionPrefs.steamController(context))
k("Upscaler", (SessionPrefs.upscalerChoices.firstOrNull { it.first == SessionPrefs.upscaler(context) }?.second ?: "Off") +
" (sharpness ${SessionPrefs.upscaleSharpness(context)}%)")
@@ -107,8 +107,22 @@ public class FakeInputWriter {
private static final int DECK_PADS_OFFSET = IMU_OFFSET + 28; // short[4]: left X, Y, right X, Y
private static final int DECK_PRESSURE_OFFSET = IMU_OFFSET + 36; // short[2]: left, right
private static final int DECK_CONTROLS_OFFSET = IMU_OFFSET + 40; // int: DeckControls bits
// Slot 0 only: what the pad is to the Steam client (libfakeinput's PAD_TYPE_*), | PAD_TYPE_SET.
private static final int PAD_TYPE_OFFSET = IMU_OFFSET + 44;
private static final int PAD_TYPE_SET = 0x80000000;
public static final int PAD_TYPE_DECK = 0;
public static final int PAD_TYPE_STEAM = 1;
public static final int PAD_TYPE_XBOX = 2;
public static final int PAD_TYPE_DS4 = 3;
public static final int PAD_TYPE_DUALSENSE = 4;
/** The type chosen this process, written into a ring made after the choice too; -1 for none. */
private static volatile int padType = -1;
/** The paired controller's state, likewise written into a ring made after it connected. */
private static volatile boolean tritonConnected = false;
private static final int TRITON_OFFSET = RING_SIZE + 64;
private static final int TRITON_MAGIC = 0x4e525454; // TTRN
// Whether the paired controller is connected: libfakeinput offers its device only then.
private static final int TRITON_CONNECTED_OFFSET = TRITON_OFFSET + 4;
private static final int TRITON_INPUT_SEQ_OFFSET = TRITON_OFFSET + 8;
private static final int TRITON_REPORT_ID_OFFSET = TRITON_OFFSET + 16;
private static final int TRITON_REPORT_LENGTH_OFFSET = TRITON_OFFSET + 20;
@@ -241,7 +255,10 @@ public class FakeInputWriter {
data.putShort(IMU_ACCEL_OFFSET + (i * 2), (short) 0);
}
data.putInt(DECK_CONTROLS_OFFSET, 0);
int type = padType;
data.putInt(PAD_TYPE_OFFSET, type >= 0 ? PAD_TYPE_SET | type : 0);
data.putInt(TRITON_OFFSET, TRITON_MAGIC);
data.putInt(TRITON_CONNECTED_OFFSET, tritonConnected ? 1 : 0);
data.putLong(TRITON_INPUT_SEQ_OFFSET, 0L);
data.putInt(TRITON_REPORT_ID_OFFSET, 0);
data.putInt(TRITON_REPORT_LENGTH_OFFSET, 0);
@@ -549,6 +566,38 @@ public class FakeInputWriter {
}
}
/**
* Switches what the pad is to the Steam client while the session runs ([PAD_TYPE_DECK],
* [PAD_TYPE_STEAM] or [PAD_TYPE_XBOX]); libfakeinput unplugs the old device and plugs in the new.
*/
public static void setPadType(int type) {
padType = type;
RingSlot ringSlot;
synchronized (RING_LOCK) {
ringSlot = RING_SLOTS[0];
}
if (ringSlot == null) return;
synchronized (ringSlot) {
ByteBuffer ring = ringSlot.data;
if (ring == null) return;
nativeStoreFence();
ring.putInt(PAD_TYPE_OFFSET, PAD_TYPE_SET | type);
}
}
/** The paired Steam Controller came or went (SteamControllerBle): its device is plugged in or out. */
public static void setTritonConnected(int slot, boolean connected) {
tritonConnected = connected;
RingSlot ringSlot = tritonSlot(slot);
if (ringSlot == null) return;
synchronized (ringSlot) {
ByteBuffer ring = ringSlot.data;
if (ring == null || ring.getInt(TRITON_OFFSET) != TRITON_MAGIC) return;
nativeStoreFence();
ring.putInt(TRITON_CONNECTED_OFFSET, connected ? 1 : 0);
}
}
/** Publishes a native Triton input report (without the HID report ID byte). */
public static void writeTritonState(int slot, int reportId, byte[] report) {
if (report == null || report.length == 0 || report.length > TRITON_REPORT_MAX) return;
@@ -29,7 +29,9 @@ import java.util.concurrent.atomic.AtomicIntegerArray;
* Guide-then-A chord the client itself answers.
*
* <p>Each further controller is another player: slots 1 to 3, an Xbox 360 pad each, which in a
* Steam session only the client reads (libfakeinput) and hands games through Steam Input.
* Steam session only the client reads (libfakeinput) and hands games through Steam Input. Slot 0
* is the device's own controls wherever it has them: a Bluetooth pad pressed first used to take
* it, and played as the handheld while its built-in pad became player 2.
*/
public final class PadBridge {
private static final String TAG = "PadBridge";
@@ -101,6 +103,17 @@ public final class PadBridge {
/** No physical controller is driving the pad: nothing yet, or the on-screen controls were last. */
public static final int NO_CONTROLLER = -1;
// AYN's handhelds (the Thor) present their own pad as a USB device Android calls external,
// beside the 2020:0111 it re-presents each Bluetooth Xbox pad as.
private static final int AYN_VENDOR = 0x2020;
private static final int AYN_BUILT_IN_PAD = 0x0112;
/** Whether this controller is the device's own controls rather than a pad connected to it. */
static boolean isBuiltIn(InputDevice device) {
return !device.isExternal()
|| (device.getVendorId() == AYN_VENDOR && device.getProductId() == AYN_BUILT_IN_PAD);
}
/**
* The Android input device id of the physical controller that last drove any slot, or
* {@link #NO_CONTROLLER}: where rumble goes when it names no slot of ours (RumbleComponent).
@@ -411,6 +424,19 @@ public final class PadBridge {
}
}
/**
* Gives every connected controller its slot now, so the client lists each one as it is
* connected instead of on its first press: a further player's pad is plugged in, at rest.
*/
public synchronized void claimConnected() {
for (int id : InputDevice.getDeviceIds()) {
InputDevice device = InputDevice.getDevice(id);
if (!isFromController(device)) continue;
int slot = slotFor(device);
if (slot > 0 && playerWriters[slot] == null) publish(slot);
}
}
/** A controller went away: its player's buttons are let go, and a further player's pad is unplugged. */
public synchronized void onDeviceRemoved(int deviceId) {
for (int slot = 0; slot < SLOTS; slot++) {
@@ -471,23 +497,55 @@ public final class PadBridge {
publish();
}
/** The slot this controller plays in: the one it has, else the first free one, else player 1's. */
/**
* The slot this controller plays in: the one it has, else slot 0 for the device's own controls
* and a further player's slot for a connected pad (slot 0 too while the device has no controls
* of its own), else player 1's.
*/
private int slotFor(InputDevice device) {
int id = device.getId();
if (owners[0] == id) return 0;
for (int slot = 1; slot < SLOTS; slot++) {
for (int slot = 0; slot < SLOTS; slot++) {
if (owners[slot] == id) return slot;
}
for (int slot = 0; slot < SLOTS; slot++) {
if (owners[slot] != NO_CONTROLLER && InputDevice.getDevice(owners[slot]) == null) free(slot);
}
for (int slot = 0; slot < SLOTS; slot++) {
if (isBuiltIn(device)) {
// A connected pad playing as the device's own controls moves to a player's slot of its own.
int was = owners[0];
if (was != NO_CONTROLLER) {
free(0);
InputDevice other = InputDevice.getDevice(was);
if (other != null && !isBuiltIn(other)) {
int to = claim(other, 1);
if (to > 0) publish(to);
}
}
owners[0] = id;
Log.i(TAG, "\"" + device.getName() + "\" (id " + id + ") is player 1, the device's own controls");
return 0;
}
int slot = claim(device, builtInConnected() ? 1 : 0);
return slot >= 0 ? slot : 0;
}
/** Takes the first free slot from {@code from} for this controller, or -1 if none is free. */
private int claim(InputDevice device, int from) {
for (int slot = from; slot < SLOTS; slot++) {
if (owners[slot] != NO_CONTROLLER) continue;
owners[slot] = id;
Log.i(TAG, "\"" + device.getName() + "\" (id " + id + ") is player " + (slot + 1));
owners[slot] = device.getId();
Log.i(TAG, "\"" + device.getName() + "\" (id " + device.getId() + ") is player " + (slot + 1));
return slot;
}
return 0;
return -1;
}
private static boolean builtInConnected() {
for (int id : InputDevice.getDeviceIds()) {
InputDevice device = InputDevice.getDevice(id);
if (isFromController(device) && isBuiltIn(device)) return true;
}
return false;
}
private PadState stateFor(int slot) {
@@ -92,6 +92,7 @@ class SteamControllerBle(private val context: Context) {
queuedWrites.clear()
outputReports.clear()
connected = false
FakeInputWriter.setTritonConnected(SLOT, false)
g.disconnect()
g.close()
release()
@@ -129,6 +130,7 @@ class SteamControllerBle(private val context: Context) {
if (g !== gatt || descriptor.characteristic.uuid != input) return
Log.i(TAG, "subscribed to report 0x%02x (status %d)".format(reportId, status))
connected = status == BluetoothGatt.GATT_SUCCESS
FakeInputWriter.setTritonConnected(SLOT, connected)
listener?.invoke()
if (connected) {
refreshLizardMode()
@@ -19,6 +19,9 @@ object SessionPrefs {
const val CONTROLLER_DECK = "deck"
const val CONTROLLER_XBOX360 = "xbox360"
const val CONTROLLER_STEAM = "steamcontroller"
const val CONTROLLER_DS4 = "dualshock4"
const val CONTROLLER_DUALSENSE = "dualsense"
const val OSC_AUTO = "auto"
const val OSC_ALWAYS = "always"
const val OSC_STEAM_QAM = "steam-qam"
@@ -131,9 +134,9 @@ object SessionPrefs {
private fun hudKey(context: Context) = if (mangoappSession(context)) "hudDeck" else "hud"
/** A Steam session that runs Deck mode with its performance overlay (mangoapp). */
/** A Steam session with Deck mode's performance overlay (mangoapp). */
private fun mangoappSession(context: Context) =
SessionState.mode == SessionService.MODE_STEAM && steamDeckMode(context) && mangoapp(context)
SessionState.mode == SessionService.MODE_STEAM && mangoapp(context)
/** When enabled, a single Back opens Steam QAM and a double Back opens the session menu. */
fun backActionsInverted(context: Context): Boolean = prefs(context).getBoolean("backActionsInverted", false)
@@ -449,27 +452,21 @@ object SessionPrefs {
/**
* What the pad is to the Steam client: [CONTROLLER_DECK], a Steam Deck controller (Quick Access
* button, gyro, Steam Input's full treatment - SteamDeckPad), or [CONTROLLER_XBOX360], the plain
* Xbox 360 pad of earlier versions (QAM by the Guide+A chord).
* button, gyro, Steam Input's full treatment - SteamDeckPad), [CONTROLLER_STEAM], a 2025 Steam
* Controller (the same, plus grip sense and its own haptics), [CONTROLLER_DS4] or
* [CONTROLLER_DUALSENSE], a PlayStation pad with its touchpad fed by the trackpads, or
* [CONTROLLER_XBOX360], the plain Xbox 360 pad of earlier versions (QAM by the Guide+A chord).
*/
fun steamController(context: Context): String =
prefs(context).getString("steamController", CONTROLLER_DECK) ?: CONTROLLER_DECK
fun setSteamController(context: Context, id: String) { prefs(context).edit().putString("steamController", id).apply() }
/** Runs the SteamOS gamepad client with its Quick Access performance controls. On by default (settleDeckModeDefault). */
fun steamDeckMode(context: Context): Boolean = prefs(context).getBoolean("steamDeckMode", true)
fun setSteamDeckMode(context: Context, on: Boolean) { prefs(context).edit().putBoolean("steamDeckMode", on).apply() }
/**
* Deck mode is the default: every install, new or from before, is moved to it once. An install
* from before 0.3.0 had off written down for it whether or not anyone chose it, so the move
* can't tell a choice from that default and moves everyone; turning it off afterwards sticks.
* Run once at process start, before anything reads or writes these prefs.
*/
fun settleDeckModeDefault(context: Context) {
val p = prefs(context)
if (p.getBoolean("deckModeMoved", false)) return
p.edit().putBoolean("steamDeckMode", true).putBoolean("deckModeMoved", true).apply()
/** [steamController]'s choice as libfakeinput's pad type (FakeInputWriter.PAD_TYPE_*). */
fun padType(id: String): Int = when (id) {
CONTROLLER_STEAM -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_STEAM
CONTROLLER_XBOX360 -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_XBOX
CONTROLLER_DS4 -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DS4
CONTROLLER_DUALSENSE -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DUALSENSE
else -> com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DECK
}
/**
@@ -603,24 +600,6 @@ object SessionPrefs {
.onFailure { android.util.Log.e("GameEnvironment", "Could not update game environment", it) }
}
/**
* The Steam client branch used for the first download and forced on the command line:
* "steamdeck_publicbeta" by default, or "publicbeta" when chosen with Deck mode off.
* Deck mode always takes the Deck branch, whatever was chosen:
* with -steamos3 the client picks its own branch as SteamOS does, and on publicbeta it settled
* on steamdeck_stable - an older client it then offered as a "Software Update" in every session,
* which applying turns into the exit-42 restart loop (seen on device 2026-09-30). Earlier, Deck
* mode on publicbeta also reinstalled the same client at every start (2026-09-23). On
* steamdeck_publicbeta the client finds no update. The choice applies with Deck mode off.
*/
fun steamChannel(context: Context): String =
if (steamDeckMode(context)) "steamdeck_publicbeta"
else prefs(context).getString("steamChannel", null) ?: "steamdeck_publicbeta"
fun setSteamChannel(context: Context, id: String) {
prefs(context).edit().putString("steamChannel", id).apply()
}
/** Whether opening DroidDeck starts a Steam session instead of showing the front end. */
fun runSteamAtStartup(context: Context): Boolean = prefs(context).getBoolean("runSteamAtStartup", false)
@@ -625,8 +625,7 @@ class SessionService : Service() {
guest.add("ZINK_DEBUG=compact")
}
// The rest of the client-interface switches (SessionPrefs): GL marshalled off the calling
// thread, no GL error checks, and the client run as SteamOS runs it (the script reads
// BL_STEAMDECK; it is the one that builds the command line).
// thread and no GL error checks.
if (SessionPrefs.glThread(this)) guest.add("mesa_glthread=true")
if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1")
// Mesa's shader cache as one database instead of a file per entry. Every lookup in the
@@ -639,9 +638,7 @@ class SessionService : Service() {
// every thread start paid a SIGSYS that proot answers; the malloc top pad grows the heap
// 16 MB at a time instead of 128 KB, and every brk(2) is a proot stop too.
guest.add("GLIBC_TUNABLES=glibc.pthread.rseq=0:glibc.malloc.top_pad=16777216")
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
if (SessionState.mode == MODE_STEAM) guest.add("BL_MANGOAPP=" + (if (SessionPrefs.mangoapp(this)) "1" else "0"))
if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
if (SessionState.mode == MODE_STEAM) {
guest.add("BL_GAMESCOPE_STRETCH_16X9=" + (if (SessionPrefs.stretch16x9(this)) "1" else "0"))
}
@@ -745,16 +742,18 @@ class SessionService : Service() {
// hidraw (SteamDeckPad), which only works with Steam Input's virtual pad for games to read.
// Decided only once its sysfs is in place: without it the client would find no Deck, and
// with the Deck asked for the pad's own nodes are withdrawn - no controller at all.
// Set up whatever the pad is to be, so the drawer can switch it between a Deck, a Steam
// Controller and an Xbox 360 pad while the session runs (FakeInputWriter.setPadType).
val wantsDeck = uinput && SessionState.mode == MODE_STEAM &&
SessionState.steamUi != "desktop" &&
SessionPrefs.steamController(this) == SessionPrefs.CONTROLLER_DECK &&
!File(Environment.getExternalStorageDirectory(), NO_DECK_PAD_SWITCH).exists()
// Steam's touch controller rides on the Deck's sysfs and is offered only beside it.
val wantsTouch = wantsDeck && !File(Environment.getExternalStorageDirectory(), NO_STEAM_TOUCH_SWITCH).exists()
// A paired Triton is exposed as a second native Steam Controller HID, with its original
// report IDs and output path. The handheld's own pad remains the Deck target.
// report IDs and output path, beside whatever the handheld's own pad is.
val wantsTriton = wantsDeck && SteamControllerBle.hasPairedController(this)
deckBinds = if (wantsDeck) SteamDeckPad.prepare(this, fakeInputDir.parentFile!!.parentFile!!, wantsTouch, wantsTriton) else emptyList()
deckBinds = if (wantsDeck) SteamDeckPad.prepare(this, fakeInputDir.parentFile!!.parentFile!!, wantsTouch, wantsTriton,
SessionPrefs.padType(SessionPrefs.steamController(this))) else emptyList()
SessionState.deckPad = deckBinds.isNotEmpty()
if (wantsDeck && !SessionState.deckPad) Log.w(TAG, "deck pad: not available this session; the pad stays an Xbox 360 controller")
logControllersAtStart()
@@ -765,6 +764,10 @@ class SessionService : Service() {
if (SessionState.deckPad) {
guest.add("FAKE_EVDEV_DECK=1")
if (wantsTriton) guest.add("FAKE_EVDEV_TRITON=1")
// What the pad starts as; the drawer's changes reach libfakeinput through the ring.
val padType = SessionPrefs.steamController(this)
guest.add("FAKE_EVDEV_PAD_TYPE=$padType")
com.droiddeck.launcher.input.FakeInputWriter.setPadType(SessionPrefs.padType(padType))
// Steam's touch controller (SteamTouchDevice): the file the app and libfakeinput share.
if (wantsTouch) com.droiddeck.launcher.input.SteamTouchDevice.prepare(fakeInputDir)?.let {
guest.add("FAKE_TOUCHCTL_RING=" + it.path)
@@ -1779,7 +1782,7 @@ class SessionService : Service() {
pads.add(String.format(java.util.Locale.ROOT, "\"%s\" (%04x:%04x)", d.name, d.vendorId, d.productId))
}
Log.i(TAG, "controllers at start: " + (if (pads.isEmpty()) "none" else pads.joinToString(", ")) +
"; presented to the guest as " + if (SessionState.deckPad) "a Steam Deck controller" else "an Xbox 360 controller")
"; presented to the guest as " + if (SessionState.deckPad) "switchable, starting as " + SessionPrefs.steamController(this) else "an Xbox 360 controller")
}
companion object {
@@ -47,10 +47,32 @@ object SteamDeckPad {
// The paired Bluetooth Steam Controller is a separate Triton HID device. It must not inherit
// the Deck's identity or report format. Must match fakeinput_steam.cpp (TRITON_HIDRAW_*).
private const val TRITON_MINOR = 18
private const val TRITON_NODE = "hidraw$TRITON_MINOR"
private const val TRITON_HID = "$GUEST_DEVICES/bluetooth/0005:28DE:1303.0003"
private const val TRITON_HIDRAW = "$TRITON_HID/hidraw/$TRITON_NODE"
private const val TRITON_SERIAL = "DROIDDECKSC01"
// The handheld's own pad as the Steam Controller (SessionPrefs.CONTROLLER_STEAM): the wired
// controller (28de:1302) on a USB device of its own, since it is built in.
private const val TRITON_USB = "$GUEST_DEVICES/usb3"
private const val TRITON_INTERFACE = "$TRITON_USB/3-1:1.0"
private const val TRITON_WIRED_HID = "$TRITON_INTERFACE/0003:28DE:1302.0004"
/** Must match libfakeinput (DS4_HIDRAW_MINOR, DUALSENSE_HIDRAW_MINOR). */
private const val SONY_DS4_MINOR = 20
private const val SONY_DUALSENSE_MINOR = 21
/** Must match libfakeinput's kDs4ReportDescriptor and kDualSenseReportDescriptor. */
private val DS4_REPORT_DESCRIPTOR = intArrayOf(
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x00, 0xff, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08,
0x85, 0x01, 0x09, 0x20, 0x95, 0x3f, 0x81, 0x02, 0x85, 0x05, 0x09, 0x21, 0x95, 0x1f, 0x91, 0x02,
0x85, 0x02, 0x09, 0x24, 0x95, 0x24, 0xb1, 0x02, 0x85, 0x12, 0x09, 0x25, 0x95, 0x0f, 0xb1, 0x02,
0x85, 0xa3, 0x09, 0x26, 0x95, 0x30, 0xb1, 0x02, 0x85, 0x81, 0x09, 0x27, 0x95, 0x3f, 0xb1, 0x02,
0xc0,
).map { it.toByte() }.toByteArray()
private val DUALSENSE_REPORT_DESCRIPTOR = intArrayOf(
0x05, 0x01, 0x09, 0x05, 0xa1, 0x01, 0x06, 0x00, 0xff, 0x15, 0x00, 0x26, 0xff, 0x00, 0x75, 0x08,
0x85, 0x01, 0x09, 0x20, 0x95, 0x3f, 0x81, 0x02, 0x85, 0x02, 0x09, 0x21, 0x95, 0x2f, 0x91, 0x02,
0x85, 0x05, 0x09, 0x22, 0x95, 0x28, 0xb1, 0x02, 0x85, 0x09, 0x09, 0x23, 0x95, 0x13, 0xb1, 0x02,
0x85, 0x20, 0x09, 0x24, 0x95, 0x3f, 0xb1, 0x02, 0xc0,
).map { it.toByte() }.toByteArray()
/** Must match libfakeinput (TRITON_PAD_HIDRAW_*): its own node, so a paired controller keeps 18. */
private const val TRITON_PAD_MINOR = 19
// Steam's touch controller (SteamTouchDevice), a second device beside the Deck's: 0000:11fb,
// "Mobile Touch Control", on a USB device of its own. Must match libfakeinput (TOUCH_*).
@@ -93,11 +115,49 @@ object SteamDeckPad {
0xc0,
).map { it.toByte() }.toByteArray()
/** The wired controller's: its 0x42 state report in place of the Bluetooth 0x45. */
private val TRITON_WIRED_REPORT_DESCRIPTOR = TRITON_REPORT_DESCRIPTOR.copyOf().also { it[15] = 0x42 }
/** Where [prepare] puts the stand-in listings; bound into the guest at the same path. */
fun listingDir(sessionRoot: File) = File(sessionRoot, "sys/deck/listing")
/** Writes the tree and returns the `host:guest` binds for it, or none if it could not be made. */
fun prepare(context: Context, sessionRoot: File, touch: Boolean = false, triton: Boolean = false): List<String> {
/** The pad's own devices by minor (the Deck, its Steam Controller, the PlayStation pads): guest paths. */
private val padNodes = mutableMapOf<Int, String>()
@Volatile private var hidrawClassDir: File? = null
/**
* Lists only the device the pad is now ([padType], FakeInputWriter.PAD_TYPE_*) in
* /sys/class/hidraw, none for an Xbox 360 pad. Device scans then find what is plugged in: SDL's
* (the PlayStation pads' driver) keeps a device it found and could not open, and never tries it
* again, while one that disappears from the listing is let go. Before the shim is told.
*/
@Synchronized
fun showPad(padType: Int) {
val dir = hidrawClassDir ?: return
val shown = when (padType) {
com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DECK -> MINOR
com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_STEAM -> TRITON_PAD_MINOR
com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DS4 -> SONY_DS4_MINOR
com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DUALSENSE -> SONY_DUALSENSE_MINOR
else -> -1
}
for ((minor, target) in padNodes) {
val link = File(dir, "hidraw$minor")
try {
if (minor == shown) {
if (!link.exists()) { link.delete(); Os.symlink(target, link.path) }
} else {
link.delete()
}
} catch (e: Exception) {
Log.w(TAG, "pad: could not list /dev/hidraw$minor: $e")
}
}
}
fun prepare(context: Context, sessionRoot: File, touch: Boolean = false, triton: Boolean = false,
padType: Int = com.droiddeck.launcher.input.FakeInputWriter.PAD_TYPE_DECK): List<String> {
val base = File(sessionRoot, "sys/deck")
val devices = File(base, "devices")
val hidrawClass = File(base, "class-hidraw")
@@ -137,7 +197,8 @@ object SteamDeckPad {
link(File(dir(HIDRAW), "device"), HID)
hidrawClass.mkdirs()
link(File(hidrawClass, NODE), HIDRAW)
padNodes.clear()
padNodes[MINOR] = HIDRAW
// Only the names matter: libudev reads a listing for the subsystems to descend into and
// then opens /sys/class/hidraw itself, which is the bind above.
@@ -177,21 +238,87 @@ object SteamDeckPad {
Log.i(TAG, "deck pad: /dev/$TOUCH_NODE described as Steam's touch controller (0000:11fb)")
}
if (triton) {
write(TRITON_HID, "uevent", "DRIVER=hid-generic\nHID_ID=0005:000028DE:00001303\n" +
"HID_NAME=Valve Software Steam Controller\nHID_PHYS=bluetooth-droiddeck-1\n" +
"HID_UNIQ=$TRITON_SERIAL\nMODALIAS=hid:b0005g0001v000028DEp00001303\n")
File(dir(TRITON_HID), "report_descriptor").writeBytes(TRITON_REPORT_DESCRIPTOR)
link(File(dir(TRITON_HID), "subsystem"), "/sys/bus/hid")
write(TRITON_HIDRAW, "uevent", "MAJOR=$MAJOR\nMINOR=$TRITON_MINOR\nDEVNAME=$TRITON_NODE\n")
write(TRITON_HIDRAW, "dev", "$MAJOR:$TRITON_MINOR\n")
link(File(dir(TRITON_HIDRAW), "subsystem"), "/sys/class/hidraw")
link(File(dir(TRITON_HIDRAW), "device"), TRITON_HID)
link(File(hidrawClass, TRITON_NODE), TRITON_HIDRAW)
link(File(context.cacheDir, "drm/sys/$MAJOR:$TRITON_MINOR"), TRITON_HIDRAW)
File(udevData, "c$MAJOR:$TRITON_MINOR").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n")
Log.i(TAG, "steam controller: /dev/$TRITON_NODE described as Triton (28de:1303)")
// A Steam Controller's HID device: the handheld's own pad as the wired one, or the paired
// one on Bluetooth. Both are always described; libfakeinput offers whichever is in use.
fun describeTriton(wired: Boolean) {
val hid = if (wired) TRITON_WIRED_HID else TRITON_HID
val product = if (wired) "1302" else "1303"
val bus = if (wired) "0003" else "0005"
val minor = if (wired) TRITON_PAD_MINOR else TRITON_MINOR
val node = "hidraw$minor"
if (wired) {
write(TRITON_USB, "uevent", "DEVTYPE=usb_device\nPRODUCT=28de/1302/100\nTYPE=0/0/0\nBUSNUM=003\nDEVNUM=002\n")
write(TRITON_USB, "idVendor", "28de\n")
write(TRITON_USB, "idProduct", "1302\n")
write(TRITON_USB, "bcdDevice", "0100\n")
write(TRITON_USB, "manufacturer", "Valve Software\n")
write(TRITON_USB, "product", "Steam Controller\n")
write(TRITON_USB, "serial", "$TRITON_SERIAL\n")
link(File(dir(TRITON_USB), "subsystem"), "/sys/bus/usb")
write(TRITON_INTERFACE, "uevent", "DEVTYPE=usb_interface\nPRODUCT=28de/1302/100\nINTERFACE=3/0/0\n")
write(TRITON_INTERFACE, "bInterfaceNumber", "00\n")
write(TRITON_INTERFACE, "bInterfaceClass", "03\n")
link(File(dir(TRITON_INTERFACE), "subsystem"), "/sys/bus/usb")
}
write(hid, "uevent", "DRIVER=hid-generic\nHID_ID=$bus:000028DE:0000$product\n" +
"HID_NAME=Valve Software Steam Controller\n" +
"HID_PHYS=${if (wired) "usb-droiddeck-3/input0" else "bluetooth-droiddeck-1"}\n" +
"HID_UNIQ=$TRITON_SERIAL\nMODALIAS=hid:b${bus}g0001v000028DEp0000$product\n")
File(dir(hid), "report_descriptor").writeBytes(if (wired) TRITON_WIRED_REPORT_DESCRIPTOR else TRITON_REPORT_DESCRIPTOR)
link(File(dir(hid), "subsystem"), "/sys/bus/hid")
val hidraw = "$hid/hidraw/$node"
write(hidraw, "uevent", "MAJOR=$MAJOR\nMINOR=$minor\nDEVNAME=$node\n")
write(hidraw, "dev", "$MAJOR:$minor\n")
link(File(dir(hidraw), "subsystem"), "/sys/class/hidraw")
link(File(dir(hidraw), "device"), hid)
if (wired) padNodes[minor] = hidraw else link(File(hidrawClass, node), hidraw)
link(File(context.cacheDir, "drm/sys/$MAJOR:$minor"), hidraw)
File(udevData, "c$MAJOR:$minor").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n")
Log.i(TAG, "steam controller: /dev/$node described as Triton (28de:$product)")
}
// The handheld's own pad as a PlayStation controller, each a USB device of its own with
// the report descriptor libfakeinput serves (kDs4/kDualSenseReportDescriptor).
fun describeSony(dualSense: Boolean) {
val bus = if (dualSense) 5 else 4
val product = if (dualSense) "0ce6" else "09cc"
val minor = if (dualSense) SONY_DUALSENSE_MINOR else SONY_DS4_MINOR
val node = "hidraw$minor"
val name = if (dualSense) "DualSense Wireless Controller" else "Wireless Controller"
val usb = "$GUEST_DEVICES/usb$bus"
val iface = "$usb/$bus-1:1.3"
val hid = "$iface/0003:054C:${product.uppercase()}.000${bus + 1}"
write(usb, "uevent", "DEVTYPE=usb_device\nPRODUCT=54c/$product/100\nTYPE=0/0/0\nBUSNUM=00$bus\nDEVNUM=002\n")
write(usb, "idVendor", "054c\n")
write(usb, "idProduct", "$product\n")
write(usb, "bcdDevice", "0100\n")
write(usb, "manufacturer", "Sony Interactive Entertainment\n")
write(usb, "product", "$name\n")
link(File(dir(usb), "subsystem"), "/sys/bus/usb")
write(iface, "uevent", "DEVTYPE=usb_interface\nPRODUCT=54c/$product/100\nINTERFACE=3/0/0\n")
write(iface, "bInterfaceNumber", "03\n")
write(iface, "bInterfaceClass", "03\n")
link(File(dir(iface), "subsystem"), "/sys/bus/usb")
write(hid, "uevent", "DRIVER=hid-generic\nHID_ID=0003:0000054C:0000${product.uppercase()}\n" +
"HID_NAME=Sony Interactive Entertainment $name\nHID_PHYS=usb-droiddeck-$bus/input3\n" +
"HID_UNIQ=5e:43:4b:44:44:02\nMODALIAS=hid:b0003g0001v0000054Cp0000${product.uppercase()}\n")
File(dir(hid), "report_descriptor").writeBytes(if (dualSense) DUALSENSE_REPORT_DESCRIPTOR else DS4_REPORT_DESCRIPTOR)
link(File(dir(hid), "subsystem"), "/sys/bus/hid")
val hidraw = "$hid/hidraw/$node"
write(hidraw, "uevent", "MAJOR=$MAJOR\nMINOR=$minor\nDEVNAME=$node\n")
write(hidraw, "dev", "$MAJOR:$minor\n")
link(File(dir(hidraw), "subsystem"), "/sys/class/hidraw")
link(File(dir(hidraw), "device"), hid)
padNodes[minor] = hidraw
link(File(context.cacheDir, "drm/sys/$MAJOR:$minor"), hidraw)
File(udevData, "c$MAJOR:$minor").writeText("I:1\nE:ID_INPUT=1\nE:ID_INPUT_JOYSTICK=1\n")
Log.i(TAG, "playstation pad: /dev/$node described as 054c:$product")
}
describeSony(dualSense = false)
describeSony(dualSense = true)
describeTriton(wired = true)
if (triton) describeTriton(wired = false)
hidrawClassDir = hidrawClass
showPad(padType)
Log.i(TAG, "deck pad: /dev/$NODE described as a Steam Deck controller (28de:1205)")
listOf(devices.path + ":" + GUEST_DEVICES, hidrawClass.path + ":/sys/class/hidraw")
@@ -86,12 +86,8 @@ class ModeSettings(
/** Steam only: SessionPrefs.SYNC_* chosen for Proton games; null outside Steam. */
val syncBackend: String? = null,
val stretch16x9: Boolean? = null,
/** Steam only: the client branch forced on the command line. */
val steamChannel: String? = null,
/** Steam only: enable the SteamOS client interface and its performance controls. */
val steamDeckMode: Boolean = false,
val steamRepairQueued: Boolean = false,
/** Steam, Deck mode: the QAM's performance overlay (mangoapp) is started. */
/** Steam: the QAM's performance overlay (mangoapp) is started. */
val mangoapp: Boolean = true,
/** Steam only: what the pad is to the client (SessionPrefs.CONTROLLER_*); null outside Steam. */
val steamController: String? = null,
@@ -141,9 +137,7 @@ class ModeSettingsActions(
val onForceSsbs: (Boolean) -> Unit = {},
val onSyncBackend: (String) -> Unit = {},
val onStretch16x9: (Boolean) -> Unit = {},
val onSteamChannel: (String) -> Unit = {},
val onSteamRepair: () -> Unit = {},
val onSteamDeckMode: (Boolean) -> Unit = {},
val onMangoapp: (Boolean) -> Unit = {},
val onSteamController: (String) -> Unit = {},
val onUnfoldedControls: (Boolean) -> Unit = {},
@@ -269,6 +263,9 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
host, "controller", stringResource(R.string.mode_controller), stringResource(R.string.mode_controller_hint),
listOf(
SessionPrefs.CONTROLLER_DECK to stringResource(R.string.mode_controller_deck),
SessionPrefs.CONTROLLER_STEAM to stringResource(R.string.mode_controller_steam),
SessionPrefs.CONTROLLER_DS4 to stringResource(R.string.mode_controller_ds4),
SessionPrefs.CONTROLLER_DUALSENSE to stringResource(R.string.mode_controller_dualsense),
SessionPrefs.CONTROLLER_XBOX360 to stringResource(R.string.mode_controller_x360),
), s.steamController,
note = stringResource(R.string.mode_controller_note),
@@ -381,25 +378,12 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
)
}
}
if (steam && tab == ModeSettingsTab.STEAM && s.steamChannel != null) SettingsGroup(stringResource(R.string.mode_client)) {
if (steam && tab == ModeSettingsTab.STEAM) SettingsGroup(stringResource(R.string.mode_client)) {
ToggleRow(
host, "steamdeck", stringResource(R.string.mode_deck_mode),
stringResource(R.string.mode_deck_mode_hint),
s.steamDeckMode, onChange = a.onSteamDeckMode,
)
if (s.steamDeckMode) ToggleRow(
host, "mangoapp", stringResource(R.string.mode_mangoapp),
stringResource(R.string.mode_mangoapp_hint),
s.mangoapp, onChange = a.onMangoapp,
)
// Deck mode fixes the branch (SessionPrefs.steamChannel); the choice is for Deck mode off.
if (s.steamDeckMode) SettingsRow(stringResource(R.string.mode_branch), stringResource(R.string.mode_branch_deck)) {}
else ChoiceRow(
host, "channel", stringResource(R.string.mode_branch), stringResource(R.string.mode_branch_hint),
listOf("publicbeta" to stringResource(R.string.mode_branch_public), "steamdeck_publicbeta" to stringResource(R.string.mode_branch_deck_beta)), s.steamChannel,
note = stringResource(R.string.mode_branch_note),
onPick = a.onSteamChannel,
)
SettingsRow(
stringResource(R.string.mode_steam_repair),
stringResource(if (s.steamRepairQueued) R.string.mode_steam_repair_queued else R.string.mode_steam_repair_hint),
@@ -194,6 +194,9 @@ class DrawerActions(
val suspendPolicy: String,
val steamDownloadsInBackground: Boolean = false,
val backActionsInverted: Boolean,
/** What the pad is to Steam (SessionPrefs.CONTROLLER_*), switched live; null hides the row. */
val padType: String? = null,
val onPadType: (String) -> Unit = {},
val touchMode: String,
val touchAuto: String,
val fexPreset: String,
@@ -419,6 +422,15 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
}
SessionDrawerPage.CONTROLLER -> {
SettingsGroup(stringResource(R.string.drawer_page_controller)) {
a.padType?.let { type ->
ChoiceRow(host, "pad-type", stringResource(R.string.mode_controller), null,
listOf(SessionPrefs.CONTROLLER_DECK to stringResource(R.string.mode_controller_deck),
SessionPrefs.CONTROLLER_STEAM to stringResource(R.string.mode_controller_steam),
SessionPrefs.CONTROLLER_DS4 to stringResource(R.string.mode_controller_ds4),
SessionPrefs.CONTROLLER_DUALSENSE to stringResource(R.string.mode_controller_dualsense),
SessionPrefs.CONTROLLER_XBOX360 to stringResource(R.string.mode_controller_x360)),
type, chipModifier = focus.track(page, "pad-type"), onPick = a.onPadType)
}
ChoiceRow(host, "touch", stringResource(R.string.mode_touch), null,
listOf(SessionPrefs.TOUCH_AUTO to stringResource(R.string.drawer_touch_auto, a.touchAuto), SessionPrefs.TOUCH_PAD to stringResource(R.string.mode_touch_touchpad), SessionPrefs.TOUCH_DIRECT to stringResource(R.string.mode_touch_direct), SessionPrefs.TOUCH_OFF to stringResource(R.string.widgets_off)),
a.touchMode, chipModifier = focus.track(page, "touch"), onPick = a.onTouch)
+3 -8
View File
@@ -254,6 +254,9 @@
<string name="mode_controller">Mando</string>
<string name="mode_controller_hint">Cómo ve Steam tu mando. Se aplica en la próxima sesión.</string>
<string name="mode_controller_deck">Mando de Steam Deck</string>
<string name="mode_controller_steam">Steam Controller</string>
<string name="mode_controller_ds4">DualShock 4</string>
<string name="mode_controller_dualsense">DualSense</string>
<string name="mode_controller_x360">Mando Xbox 360</string>
<string name="mode_controller_note">Deck: botón de Quick Access y giroscopio. Xbox 360: abre Quick Access con Guide+A.</string>
<string name="mode_back">Atrás</string>
@@ -283,16 +286,8 @@
<string name="mode_decky_remove_title">¿Desinstalar Decky Loader?</string>
<string name="mode_decky_remove_text">Elimina el loader. Tus plugins y ajustes se conservan.</string>
<string name="mode_client">Cliente</string>
<string name="mode_deck_mode">Modo Steam Deck</string>
<string name="mode_deck_mode_hint">La interfaz Deck de Steam y los controles de rendimiento de Quick Access. Se aplica en la próxima sesión.</string>
<string name="mode_mangoapp">Superposición de rendimiento</string>
<string name="mode_mangoapp_hint">Desactívalo si los juegos fallan o se quedan en negro en modo Deck. Se aplica en la próxima sesión.</string>
<string name="mode_branch">Rama del cliente</string>
<string name="mode_branch_deck">Beta pública de Steam Deck (requerida por el modo Deck).</string>
<string name="mode_branch_hint">Se aplica en la próxima sesión. Steam puede actualizarse una vez.</string>
<string name="mode_branch_public">Beta pública</string>
<string name="mode_branch_deck_beta">Beta pública de Steam Deck</string>
<string name="mode_branch_note">El modo Deck siempre usa la beta pública de Steam Deck.</string>
<string name="mode_steam_repair">Reparar el cliente de Steam</string>
<string name="mode_steam_repair_hint">Vuelve a descargar el cliente de Steam la próxima vez que se inicie Steam. Úsalo si Steam se queda en la pantalla de carga. Se conservan los juegos, la sesión iniciada y los ajustes.</string>
<string name="mode_steam_repair_queued">El cliente de Steam se volverá a descargar la próxima vez que se inicie Steam.</string>
+3 -8
View File
@@ -232,6 +232,9 @@
<string name="mode_controller">Manette</string>
<string name="mode_controller_hint">Comment Steam reconnaît votre manette. Prendra effet à la prochaine session.</string>
<string name="mode_controller_deck">Manette Steam Deck</string>
<string name="mode_controller_steam">Steam Controller</string>
<string name="mode_controller_ds4">DualShock 4</string>
<string name="mode_controller_dualsense">DualSense</string>
<string name="mode_controller_x360">Manette Xbox 360</string>
<string name="mode_controller_note">Deck : fournit un bouton d\'accès rapide et un gyroscope. Xbox 360 : utilisez Guide+A pour ouvrir l\'accès rapide.</string>
<string name="mode_back">Retour</string>
@@ -257,16 +260,8 @@
<string name="mode_decky_remove_title">Désinstaller Decky Loader ?</string>
<string name="mode_decky_remove_text">Supprime le chargeur. Vos plugins et paramètres seront conservés.</string>
<string name="mode_client">Client</string>
<string name="mode_deck_mode">Mode Steam Deck</string>
<string name="mode_deck_mode_hint">Interface Steam Deck et contrôles de performance à accès rapide. Prendra effet à la prochaine session.</string>
<string name="mode_mangoapp">Superposition de performance</string>
<string name="mode_mangoapp_hint">Désactivez cette option si les jeux plantent ou affichent un écran noir en mode Deck. Prendra effet à la prochaine session.</string>
<string name="mode_branch">Branche du client</string>
<string name="mode_branch_deck">Bêta Steam Deck (requis pour le mode Deck).</string>
<string name="mode_branch_hint">Prendra effet à la prochaine session. Steam peut se mettre à jour une fois de lui-même.</string>
<string name="mode_branch_public">Bêta</string>
<string name="mode_branch_deck_beta">Bêta Steam Deck</string>
<string name="mode_branch_note">Le mode Deck utilise toujours la version bêta de Steam Deck.</string>
<string name="mode_added_art">Illustrations</string>
<string name="mode_sgdb_key">Clé API SteamGridDB</string>
<string name="mode_sgdb_key_yours">Votre clé</string>
+3 -8
View File
@@ -254,6 +254,9 @@
<string name="mode_controller">コントローラー</string>
<string name="mode_controller_hint">Steamがコントローラーを認識する方法。次回のセッションで有効になります。</string>
<string name="mode_controller_deck">Steam Deckコントローラー</string>
<string name="mode_controller_steam">Steam Controller</string>
<string name="mode_controller_ds4">DualShock 4</string>
<string name="mode_controller_dualsense">DualSense</string>
<string name="mode_controller_x360">Xbox 360コントローラー</string>
<string name="mode_controller_note">Deck:クイックアクセスボタンとジャイロを提供します。Xbox 360:Guide+Aでクイックアクセスを開きます。</string>
<string name="mode_back">戻る</string>
@@ -283,16 +286,8 @@
<string name="mode_decky_remove_title">Decky Loaderをアンインストールしますか?</string>
<string name="mode_decky_remove_text">Loaderを削除します。プラグインと設定は保持されます。</string>
<string name="mode_client">クライアント</string>
<string name="mode_deck_mode">Steam Deckモード</string>
<string name="mode_deck_mode_hint">Steam DeckのUIとクイックアクセスパフォーマンスコントロール。次回のセッションで有効になります。</string>
<string name="mode_mangoapp">パフォーマンスオーバーレイ</string>
<string name="mode_mangoapp_hint">Deckモードでゲームがクラッシュしたり画面が真っ暗になる場合は、これをオフにしてください。次回のセッションで有効になります。</string>
<string name="mode_branch">クライアントブランチ</string>
<string name="mode_branch_deck">Steam Deckベータ版(Deckモードに必要)。</string>
<string name="mode_branch_hint">次回のセッションで有効になります。Steamが自動的に一度アップデートされる場合があります。</string>
<string name="mode_branch_public">ベータ版</string>
<string name="mode_branch_deck_beta">Steam Deckベータ版</string>
<string name="mode_branch_note">Deckモードは常にSteam Deckベータ版を使用します。</string>
<string name="mode_steam_repair">Steamクライアントを修復</string>
<string name="mode_steam_repair_hint">次回Steamの起動時にSteamクライアントを再ダウンロードします。Steamが読み込み画面のまま進まない場合に使用してください。ゲーム、ログイン情報、設定は保持されます。</string>
<string name="mode_steam_repair_queued">次回Steamの起動時にSteamクライアントを再ダウンロードします。</string>
+3 -8
View File
@@ -254,6 +254,9 @@
<string name="mode_controller">컨트롤러</string>
<string name="mode_controller_hint">Steam이 컨트롤러를 인식하는 방식입니다. 다음 세션에 적용됩니다.</string>
<string name="mode_controller_deck">Steam Deck 컨트롤러</string>
<string name="mode_controller_steam">Steam Controller</string>
<string name="mode_controller_ds4">DualShock 4</string>
<string name="mode_controller_dualsense">DualSense</string>
<string name="mode_controller_x360">Xbox 360 컨트롤러</string>
<string name="mode_controller_note">Deck: 빠른 액세스 버튼과 자이로스코프를 제공합니다. Xbox 360: Guide+A로 빠른 액세스를 엽니다.</string>
<string name="mode_back">뒤로</string>
@@ -283,16 +286,8 @@
<string name="mode_decky_remove_title">Decky Loader를 제거하시겠습니까?</string>
<string name="mode_decky_remove_text">로더를 제거합니다. 플러그인과 설정은 유지됩니다.</string>
<string name="mode_client">클라이언트</string>
<string name="mode_deck_mode">Steam Deck 모드</string>
<string name="mode_deck_mode_hint">Steam Deck UI 및 빠른 액세스 성능 제어. 다음 세션에 적용됩니다.</string>
<string name="mode_mangoapp">성능 오버레이</string>
<string name="mode_mangoapp_hint">Deck 모드에서 게임이 충돌하거나 화면이 검게 변하면 이 옵션을 끄세요. 다음 세션에 적용됩니다.</string>
<string name="mode_branch">클라이언트 브랜치</string>
<string name="mode_branch_deck">Steam Deck 베타(Deck 모드에 필요).</string>
<string name="mode_branch_hint">다음 세션에 적용됩니다. Steam이 자체적으로 한 번 업데이트될 수 있습니다.</string>
<string name="mode_branch_public">베타</string>
<string name="mode_branch_deck_beta">Steam Deck 베타</string>
<string name="mode_branch_note">Deck 모드는 항상 Steam Deck 베타를 사용합니다.</string>
<string name="mode_steam_repair">Steam 클라이언트 복구</string>
<string name="mode_steam_repair_hint">다음에 Steam을 시작할 때 Steam 클라이언트를 다시 다운로드합니다. Steam이 로딩 화면에서 넘어가지 않을 때 사용하세요. 게임, 로그인 정보 및 설정은 유지됩니다.</string>
<string name="mode_steam_repair_queued">다음에 Steam을 시작할 때 Steam 클라이언트를 다시 다운로드합니다.</string>
+3 -8
View File
@@ -254,6 +254,9 @@
<string name="mode_controller">Контроллер</string>
<string name="mode_controller_hint">Как Steam распознает ваш контроллер. Вступит в силу в следующей сессии.</string>
<string name="mode_controller_deck">Контроллер Steam Deck</string>
<string name="mode_controller_steam">Steam Controller</string>
<string name="mode_controller_ds4">DualShock 4</string>
<string name="mode_controller_dualsense">DualSense</string>
<string name="mode_controller_x360">Контроллер Xbox 360</string>
<string name="mode_controller_note">Deck: кнопка быстрого доступа и гироскоп. Xbox 360: используйте Guide+A для быстрого доступа.</string>
<string name="mode_back">Назад</string>
@@ -281,16 +284,8 @@
<string name="mode_decky_remove_title">Удалить Decky Loader?</string>
<string name="mode_decky_remove_text">Удаляет загрузчик. Ваши плагины и настройки сохранятся.</string>
<string name="mode_client">Клиент</string>
<string name="mode_deck_mode">Режим Steam Deck</string>
<string name="mode_deck_mode_hint">Интерфейс Steam Deck и управление производительностью через быстрый доступ. Вступит в силу в следующей сессии.</string>
<string name="mode_mangoapp">Оверлей производительности</string>
<string name="mode_mangoapp_hint">Отключите, если игры вылетают или показывают черный экран в режиме Deck. Вступит в силу в следующей сессии.</string>
<string name="mode_branch">Ветка клиента</string>
<string name="mode_branch_deck">Публичная бета Steam Deck (требуется для режима Deck).</string>
<string name="mode_branch_hint">Вступит в силу в следующей сессии. Steam может обновиться самостоятельно один раз.</string>
<string name="mode_branch_public">Публичная бета</string>
<string name="mode_branch_deck_beta">Публичная бета Steam Deck</string>
<string name="mode_branch_note">Режим Deck всегда использует публичную бету Steam Deck.</string>
<string name="mode_steam_repair">Восстановить клиент Steam</string>
<string name="mode_steam_repair_hint">При следующем запуске Steam клиент будет скачан заново. Используйте, если Steam зависает на экране загрузки. Игры, вход и настройки сохраняются.</string>
<string name="mode_steam_repair_queued">Клиент Steam будет скачан заново при следующем запуске Steam.</string>
+3 -8
View File
@@ -254,6 +254,9 @@
<string name="mode_controller">手柄</string>
<string name="mode_controller_hint">决定 Steam 把你的手柄识别为哪种类型,将在下次会话生效。</string>
<string name="mode_controller_deck">Steam Deck 手柄</string>
<string name="mode_controller_steam">Steam Controller</string>
<string name="mode_controller_ds4">DualShock 4</string>
<string name="mode_controller_dualsense">DualSense</string>
<string name="mode_controller_x360">Xbox 360 手柄</string>
<string name="mode_controller_note">Deck:快捷访问按钮与陀螺仪。Xbox 360:用 Guide+A 打开快捷访问。</string>
<string name="mode_back">返回</string>
@@ -283,16 +286,8 @@
<string name="mode_decky_remove_title">卸载 Decky Loader?</string>
<string name="mode_decky_remove_text">移除加载器。你的插件和设置会保留。</string>
<string name="mode_client">客户端</string>
<string name="mode_deck_mode">Steam Deck 模式</string>
<string name="mode_deck_mode_hint">Steam 的 Deck 界面和快捷访问性能控制。将在下次会话生效。</string>
<string name="mode_mangoapp">性能覆盖层</string>
<string name="mode_mangoapp_hint">若游戏在 Deck 模式下崩溃或黑屏,请关闭性能覆盖层。将在下次会话生效。</string>
<string name="mode_branch">客户端分支</string>
<string name="mode_branch_deck">Steam Deck 公开测试版(Deck 模式所必需)。</string>
<string name="mode_branch_hint">将在下次会话生效。Steam 可能会自行更新一次。</string>
<string name="mode_branch_public">公开测试版</string>
<string name="mode_branch_deck_beta">Steam Deck 公开测试版</string>
<string name="mode_branch_note">Deck 模式始终使用 Steam Deck 公开测试版。</string>
<string name="mode_steam_repair">修复 Steam 客户端</string>
<string name="mode_steam_repair_hint">下次启动 Steam 时重新下载 Steam 客户端。如果 Steam 一直停在加载画面,请使用此项。游戏、登录信息和设置都会保留。</string>
<string name="mode_steam_repair_queued">下次启动 Steam 时将重新下载 Steam 客户端。</string>
+3 -8
View File
@@ -254,6 +254,9 @@
<string name="mode_controller">控制器</string>
<string name="mode_controller_hint">Steam 如何識別你的控制器。下次會話生效。</string>
<string name="mode_controller_deck">Steam Deck 控制器</string>
<string name="mode_controller_steam">Steam Controller</string>
<string name="mode_controller_ds4">DualShock 4</string>
<string name="mode_controller_dualsense">DualSense</string>
<string name="mode_controller_x360">Xbox 360 控制器</string>
<string name="mode_controller_note">Deck:提供快速存取按鈕和陀螺儀。Xbox 360:按 Guide+A 開啟快速存取。</string>
<string name="mode_back">返回</string>
@@ -283,16 +286,8 @@
<string name="mode_decky_remove_title">解除安裝 Decky Loader?</string>
<string name="mode_decky_remove_text">移除載入器,你的插件和設定會保留。</string>
<string name="mode_client">用戶端</string>
<string name="mode_deck_mode">Steam Deck 模式</string>
<string name="mode_deck_mode_hint">Steam 的 Deck 介面及快速存取效能控制。下次會話生效。</string>
<string name="mode_mangoapp">效能疊加層</string>
<string name="mode_mangoapp_hint">如果在 Deck 模式下遊戲當機或黑畫面,請關閉此項。下次會話生效。</string>
<string name="mode_branch">用戶端分支</string>
<string name="mode_branch_deck">Steam Deck 公開測試版(Deck 模式需要)。</string>
<string name="mode_branch_hint">下次會話生效。Steam 可能會自行更新一次。</string>
<string name="mode_branch_public">公開測試版</string>
<string name="mode_branch_deck_beta">Steam Deck 公開測試版</string>
<string name="mode_branch_note">Deck 模式一律使用 Steam Deck 公開測試版。</string>
<string name="mode_steam_repair">修復 Steam 用戶端</string>
<string name="mode_steam_repair_hint">下次啟動 Steam 時重新下載 Steam 用戶端。如果 Steam 一直停留在載入畫面,請使用此功能。遊戲、登入資料和設定都會保留。</string>
<string name="mode_steam_repair_queued">下次啟動 Steam 時會重新下載 Steam 用戶端。</string>
+3 -8
View File
@@ -254,6 +254,9 @@
<string name="mode_controller">控制器</string>
<string name="mode_controller_hint">Steam 辨識你的控制器的方式。下次工作階段生效。</string>
<string name="mode_controller_deck">Steam Deck 控制器</string>
<string name="mode_controller_steam">Steam Controller</string>
<string name="mode_controller_ds4">DualShock 4</string>
<string name="mode_controller_dualsense">DualSense</string>
<string name="mode_controller_x360">Xbox 360 控制器</string>
<string name="mode_controller_note">Deck:提供快速存取按鈕和陀螺儀。Xbox 360:使用 Guide+A 開啟快速存取。</string>
<string name="mode_back">返回</string>
@@ -283,16 +286,8 @@
<string name="mode_decky_remove_title">解除安裝 Decky Loader?</string>
<string name="mode_decky_remove_text">移除載入器。你的外掛程式和設定會保留。</string>
<string name="mode_client">用戶端</string>
<string name="mode_deck_mode">Steam Deck 模式</string>
<string name="mode_deck_mode_hint">Steam Deck 介面和快速存取效能控制。下次工作階段生效。</string>
<string name="mode_mangoapp">效能重疊層</string>
<string name="mode_mangoapp_hint">如果 Deck 模式下遊戲崩潰或黑畫面,請關閉此項。下次工作階段生效。</string>
<string name="mode_branch">用戶端分支</string>
<string name="mode_branch_deck">Steam Deck 公開測試版(Deck 模式所需)。</string>
<string name="mode_branch_hint">下次工作階段生效。Steam 可能會自行更新一次。</string>
<string name="mode_branch_public">公開測試版</string>
<string name="mode_branch_deck_beta">Steam Deck 公開測試版</string>
<string name="mode_branch_note">Deck 模式一律使用 Steam Deck 公開測試版。</string>
<string name="mode_steam_repair">修復 Steam 用戶端</string>
<string name="mode_steam_repair_hint">下次啟動 Steam 時重新下載 Steam 用戶端。如果 Steam 一直停在載入畫面,請使用此功能。遊戲、登入狀態和設定都會保留。</string>
<string name="mode_steam_repair_queued">下次啟動 Steam 時會重新下載 Steam 用戶端。</string>
+4 -9
View File
@@ -262,8 +262,11 @@
<string name="mode_controller">Controller</string>
<string name="mode_controller_hint">How Steam sees your controller. Applies next session.</string>
<string name="mode_controller_deck">Steam Deck controller</string>
<string name="mode_controller_steam">Steam Controller</string>
<string name="mode_controller_ds4">DualShock 4</string>
<string name="mode_controller_dualsense">DualSense</string>
<string name="mode_controller_x360">Xbox 360 controller</string>
<string name="mode_controller_note">Deck: Quick Access button and gyro. Xbox 360: open Quick Access with Guide+A.</string>
<string name="mode_controller_note">Deck: Quick Access button and gyro. Steam Controller: the same, with grip sense on the back buttons. DualShock 4 and DualSense: gyro, and the trackpads as their touchpad. Xbox 360: open Quick Access with Guide+A.</string>
<string name="mode_back">Back</string>
<string name="back_menu_then_qam">Menu → QAM</string>
<string name="back_qam_then_menu">QAM → Menu</string>
@@ -293,16 +296,8 @@
<string name="mode_decky_remove_title">Uninstall Decky Loader?</string>
<string name="mode_decky_remove_text">Removes the loader. Your plugins and settings are kept.</string>
<string name="mode_client">Client</string>
<string name="mode_deck_mode">Steam Deck mode</string>
<string name="mode_deck_mode_hint">Steam\'s Deck interface and Quick Access performance controls. Applies next session.</string>
<string name="mode_mangoapp">Performance overlay</string>
<string name="mode_mangoapp_hint">Turn off if games crash or go black in Deck mode. Applies next session.</string>
<string name="mode_branch">Client branch</string>
<string name="mode_branch_deck">Steam Deck public beta (required by Deck mode).</string>
<string name="mode_branch_hint">Applies next session. Steam may update itself once.</string>
<string name="mode_branch_public">Public beta</string>
<string name="mode_branch_deck_beta">Steam Deck public beta</string>
<string name="mode_branch_note">Deck mode always uses Steam Deck public beta.</string>
<string name="mode_steam_repair">Repair Steam client</string>
<string name="mode_steam_repair_hint">Downloads the Steam client again the next time Steam starts. Use it if Steam stays on its loading screen. Games, sign-in and settings are kept.</string>
<string name="mode_steam_repair_queued">The Steam client will be downloaded again the next time Steam starts.</string>
@@ -1,42 +0,0 @@
package com.droiddeck.launcher.session
import android.content.Context
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], manifest = Config.NONE)
class SteamChannelTest {
private lateinit var context: Context
@Before fun setUp() {
context = RuntimeEnvironment.getApplication()
context.getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit()
}
@Test fun freshSettingsDefaultToDeckBetaWithOrWithoutDeckMode() {
assertEquals("steamdeck_publicbeta", SessionPrefs.steamChannel(context))
SessionPrefs.setSteamDeckMode(context, false)
assertEquals("steamdeck_publicbeta", SessionPrefs.steamChannel(context))
}
@Test fun explicitChannelIsPreservedWhenDeckModeIsOff() {
SessionPrefs.setSteamDeckMode(context, false)
SessionPrefs.setSteamChannel(context, "publicbeta")
assertEquals("publicbeta", SessionPrefs.steamChannel(context))
SessionPrefs.setSteamChannel(context, "steamdeck_publicbeta")
assertEquals("steamdeck_publicbeta", SessionPrefs.steamChannel(context))
}
@Test fun deckModeUsesDeckBetaAndRestoresSavedSelectionWhenDisabled() {
SessionPrefs.setSteamChannel(context, "publicbeta")
assertEquals("steamdeck_publicbeta", SessionPrefs.steamChannel(context))
SessionPrefs.setSteamDeckMode(context, false)
assertEquals("publicbeta", SessionPrefs.steamChannel(context))
}
}
@@ -264,12 +264,12 @@ if [ -z "${BL_INSIDE:-}" ]; then
[ "${BL_HDR:-0}" = 1 ] && args+=(--hdr-enabled)
# Extra gamescope arguments for an experiment (--debug-focus, say), from Download/droiddeck-env.
[ -n "${BL_GAMESCOPE_ARGS:-}" ] && read -r -a _bl_extra <<<"$BL_GAMESCOPE_ARGS" && args+=("${_bl_extra[@]}")
# Deck mode's performance overlay (Quick Access Menu → Performance → Overlay Level) is drawn by
# The performance overlay (Quick Access Menu → Performance → Overlay Level) is drawn by
# mangoapp, which gamescope starts beside the client; the client sets the level by rewriting
# MANGOHUD_CONFIGFILE. The apk stages Valve's aarch64 build (tools/mangoapp) - without it the menu
# still offers the levels and nothing appears. Starts hidden until the client writes a level.
# BL_MANGOAPP=0 turns it off (one community build found Valve's mangoapp crashing on Turnip).
if [ "$mode" = steam ] && [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_MANGOAPP:-1}" = 1 ] \
if [ "$mode" = steam ] && [ "${BL_MANGOAPP:-1}" = 1 ] \
&& [ "${BL_STEAM_UI:-}" != desktop ] && command -v mangoapp >/dev/null 2>&1; then
MANGOHUD_CONFIGFILE=$(mktemp /tmp/mangohud.XXXXXXXX)
echo no_display > "$MANGOHUD_CONFIGFILE"
@@ -821,7 +821,7 @@ BUSCONF
# desktop's launchers ask for; gamescope shows one window at a time and the game takes over.
_bl_ui=(-gamepadui)
[ "${BL_STEAM_UI:-}" = desktop ] && _bl_ui=()
# Deck mode (BL_STEAMDECK=1, the Steam page's Client settings): -steamdeck is the flag that puts
# Deck mode, always outside the desktop UI: -steamdeck is the flag that puts
# the Quick Access Menu and the Deck settings pages on screen; the QAM's performance overlay
# controls stay greyed out without -steamos3, as SteamOS and Armada launch it. -steamos3 makes
# the client manage itself as SteamOS does - it calls steamos-select-branch and writes
@@ -829,7 +829,7 @@ BUSCONF
# is an update that never installs: exit 42, a restart, for ever (Bannerlator 38996ea1). The
# loop below puts package/beta back before every start. The SteamOS helpers the client calls
# in this mode are staged as no-op stubs under /usr/bin.
if [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_STEAM_UI:-}" != desktop ]; then
if [ "${BL_STEAM_UI:-}" != desktop ]; then
_bl_ui+=(-steamdeck -steamos3)
# The QAM's Scaling Filter lists NVIDIA Image Scaling only when told gamescope has it (it does).
export STEAM_GAMESCOPE_NIS_SUPPORTED=1
@@ -851,13 +851,16 @@ BUSCONF
[ -n "$_stale" ] && echo "== steam: cleared what an earlier session left:$_stale"
unset _stale _f
fi
# Use the same configured branch as the first-run download. Whatever wrote package/beta, the branch on
# disk has to agree with the one forced here - a disagreement is the restart loop above, and
# it is silent apart from rc=42 in this log.
# The Deck's beta branch, the one the first-run download takes: with -steamos3 on publicbeta
# the client settled on steamdeck_stable, an older client it then offered as a "Software
# Update" every session (2026-09-30); on steamdeck_publicbeta it finds no update. Whatever
# wrote package/beta, the branch on disk has to agree with the one forced here - a
# disagreement is the restart loop above, and it is silent apart from rc=42 in this log. A
# client installed from publicbeta by an older app moves over this way at its next start.
# Checked before every start, not once: the client rewrites package/beta during its own run,
# and a restart below (rc=42, or the one after the ARM64 Proton lands) would otherwise start it
# again on the branch it just wrote.
steam_channel=${BL_STEAM_CHANNEL:-steamdeck_publicbeta}
steam_channel=steamdeck_publicbeta
# The app's language (Setup, or the system's): the client's interface takes -language over its
# registry, and the registry is rewritten before each start below for its update window.
_bl_lang=()
@@ -3,11 +3,9 @@
# The client is not redistributable, so the runtime ships this script instead of the client.
set -eu
cdn=https://client-update.fastly.steamstatic.com
steam_channel=${BL_STEAM_CHANNEL:-steamdeck_publicbeta}
case "$steam_channel" in
publicbeta|steamdeck_publicbeta) ;;
*) echo "droiddeck-steam-install: unsupported Steam channel: $steam_channel" >&2; exit 1 ;;
esac
# The Deck's beta branch, which droiddeck-session forces on the command line too. A client already
# on disk from another branch is left for the client to move (droiddeck-session's package/beta).
steam_channel=steamdeck_publicbeta
manifest=steam_client_${steam_channel}_linuxarm64
steam_root=${XDG_DATA_HOME:-$HOME/.local/share}/Steam
stamp=$steam_root/package/droiddeck-installed
+26 -43
View File
@@ -45,8 +45,7 @@ cp "$TEST_CDN/${2##*/}" "$4"
self.env = dict(os.environ, HOME=str(self.home), XDG_DATA_HOME=str(self.data),
PATH=str(self.bin) + os.pathsep + os.environ['PATH'],
TEST_CDN=str(self.cdn), TEST_REQUESTS=str(self.requests))
self.env.pop('BL_STEAM_CHANNEL', None)
for channel in ('publicbeta', 'steamdeck_publicbeta'):
for channel in ('steamdeck_publicbeta',):
archive = self.cdn / (channel + '.zip')
with zipfile.ZipFile(archive, 'w') as package:
package.writestr('steamrtarm64/steam', channel)
@@ -58,14 +57,11 @@ cp "$TEST_CDN/${2##*/}" "$4"
f'\t\t"file" "{archive.name}"\n\t\t"sha2" "{digest}"\n'
'\t}\n}\n')
def install(self, channel=None):
env = self.env.copy()
if channel is not None:
env['BL_STEAM_CHANNEL'] = channel
return subprocess.run(['bash', str(SCRIPT)], env=env, text=True, capture_output=True)
def install(self):
return subprocess.run(['bash', str(SCRIPT)], env=self.env, text=True, capture_output=True)
def assert_installed(self, channel):
result = self.install(channel)
def assert_installed(self, channel='steamdeck_publicbeta'):
result = self.install()
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(channel + '\n', (self.steam / 'package/beta').read_text())
self.assertEqual('123456\n', (self.steam / 'package/droiddeck-installed').read_text())
@@ -78,38 +74,31 @@ cp "$TEST_CDN/${2##*/}" "$4"
'https://client-update.fastly.steamstatic.com/' + channel + '.zip',
], self.requests.read_text().splitlines())
def test_unset_channel_downloads_deck_beta(self):
def test_downloads_the_deck_beta_client(self):
self.assert_installed()
def test_an_install_from_another_branch_is_left_for_steam_to_move(self):
# An older app could install publicbeta; droiddeck-session moves the client at its start.
self.assert_installed()
requests = self.requests.read_text()
(self.steam / 'package/beta').write_text('publicbeta\n')
result = self.install()
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual('steamdeck_publicbeta\n', (self.steam / 'package/beta').read_text())
self.assertIn('/steam_client_steamdeck_publicbeta_linuxarm64\n', self.requests.read_text())
def test_explicit_deck_beta_downloads_matching_client(self):
self.assert_installed('steamdeck_publicbeta')
def test_explicit_public_beta_downloads_matching_client(self):
self.assert_installed('publicbeta')
def test_existing_install_is_left_for_steam_to_update(self):
self.assert_installed('steamdeck_publicbeta')
requests = self.requests.read_text()
result = self.install('publicbeta')
self.assertEqual(0, result.returncode, result.stderr)
self.assertEqual(requests, self.requests.read_text())
self.assertEqual('steamdeck_publicbeta\n', (self.steam / 'package/beta').read_text())
def test_failed_download_has_no_ready_marker_and_can_retry(self):
archive = self.cdn / 'publicbeta.zip'
original = archive.read_bytes()
archive.write_bytes(b'corrupt download')
self.assertNotEqual(0, self.install('publicbeta').returncode)
self.assertFalse((self.steam / 'package/droiddeck-installed').exists())
archive.write_bytes(original)
self.assertEqual(0, self.install('publicbeta').returncode)
self.assertEqual('publicbeta\n', (self.steam / 'package/beta').read_text())
def test_failed_download_has_no_ready_marker_and_can_retry(self):
archive = self.cdn / 'steamdeck_publicbeta.zip'
original = archive.read_bytes()
archive.write_bytes(b'corrupt download')
self.assertNotEqual(0, self.install().returncode)
self.assertFalse((self.steam / 'package/droiddeck-installed').exists())
archive.write_bytes(original)
self.assertEqual(0, self.install().returncode)
self.assertEqual('steamdeck_publicbeta\n', (self.steam / 'package/beta').read_text())
def break_client(self, starts):
self.assert_installed('steamdeck_publicbeta')
self.assert_installed()
self.requests.unlink()
package = self.steam / 'package'
(self.steam / 'steamrtarm64/steam').write_text('half updated')
@@ -125,7 +114,7 @@ cp "$TEST_CDN/${2##*/}" "$4"
def test_two_starts_with_nothing_shown_fetch_the_client_again(self):
package = self.break_client(2)
self.assert_installed('steamdeck_publicbeta')
self.assert_installed()
self.assertEqual('repaired\n', (package / 'droiddeck-unconfirmed').read_text())
self.assertFalse((self.steam / '.crash').exists())
self.assertEqual([], sorted(p.name for p in package.glob('steam_client_*')))
@@ -161,18 +150,12 @@ cp "$TEST_CDN/${2##*/}" "$4"
self.assertFalse((package / 'droiddeck-installed').exists())
archive.write_bytes(original)
self.requests.unlink()
self.assert_installed('steamdeck_publicbeta')
self.assert_installed()
self.assertFalse(request.exists())
self.requests.unlink()
self.assertEqual(0, self.install().returncode)
self.assertFalse(self.requests.exists())
def test_invalid_channel_fails_before_any_download(self):
result = self.install('../unknown')
self.assertNotEqual(0, result.returncode)
self.assertIn('unsupported Steam channel', result.stderr)
self.assertFalse(self.requests.exists())
if __name__ == '__main__':
unittest.main()