diff --git a/app/src/main/java/com/droiddeck/launcher/frontend/SteamLaunchOptions.kt b/app/src/main/java/com/droiddeck/launcher/frontend/SteamLaunchOptions.kt new file mode 100644 index 000000000..cc832db34 --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/frontend/SteamLaunchOptions.kt @@ -0,0 +1,162 @@ +package com.droiddeck.launcher.frontend + +import android.content.Context +import android.util.Log +import com.droiddeck.launcher.session.SessionPrefs +import com.droiddeck.launcher.session.SessionService +import com.droiddeck.launcher.session.SessionState +import com.droiddeck.launcher.stores.SteamLiveShortcuts +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +object SteamLaunchOptions { + private const val TAG = "SteamLaunchOptions" + private val STEAM = listOf("UserLocalConfigStore", "Software", "Valve", "Steam") + + fun current(context: Context, appId: Int): String = + SteamLiveShortcuts.launchOptions(context, appId.toLong()) ?: steam(SessionPrefs.activeSteamRoot(context), appId).orEmpty() + + enum class Saved { YES, NO, DESKTOP } + + fun set(context: Context, appId: Int, options: String, shown: String): Saved { + val value = options.trim() + if (SessionState.running && SessionState.mode == SessionService.MODE_DESKTOP) return Saved.DESKTOP + if (SessionState.running && SessionState.mode == SessionService.MODE_STEAM) { + val live = SteamLiveShortcuts.launchOptions(context, appId.toLong()) ?: return Saved.NO + if (live.trim() != shown.trim()) return Saved.NO + return if (SteamLiveShortcuts.setLaunchOptions(context, appId.toLong(), value)) Saved.YES else Saved.NO + } + val home = SessionPrefs.activeSteamRoot(context) + if (steam(home, appId).orEmpty().trim() != shown.trim()) return Saved.NO + return if (write(home, appId, value)) Saved.YES else Saved.NO + } + + internal fun steam(steamHome: File, appId: Int): String? { + val config = AddedGames.accountConfig(steamHome) ?: return null + val text = runCatching { File(config, "localconfig.vdf").readText() }.getOrNull() ?: return null + return launchOptions(text, appId) + } + + internal fun write(steamHome: File, appId: Int, options: String): Boolean { + val config = AddedGames.accountConfig(steamHome)?.let { File(it, "localconfig.vdf") } ?: return false + val text = runCatching { config.readText() }.getOrNull() ?: return false + val updated = withLaunchOptions(text, appId, options) ?: return false + if (updated == text) return true + val staged = File(config.parentFile, config.name + ".staged") + return try { + staged.writeText(updated) + if (config.readText() != text) { + staged.delete() + return false + } + Files.move(staged.toPath(), config.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE) + true + } catch (e: Exception) { + staged.delete() + Log.w(TAG, "could not write launch options to ${config.path}", e) + false + } + } + + internal fun launchOptions(text: String, appId: Int): String? { + val app = section(text, STEAM + "apps" + appId.toString()) ?: return null + return entries(text, app.valueStart, app.end).firstOrNull { !it.section && it.key.equals("LaunchOptions", true) } + ?.let { unescape(text.substring(it.valueStart + 1, it.end)) } + } + + internal fun withLaunchOptions(text: String, appId: Int, options: String): String? { + val newline = if (text.contains("\r\n")) "\r\n" else "\n" + var updated = text + var path = STEAM + for (key in listOf("apps", appId.toString())) { + updated = withSection(updated, section(updated, path) ?: return null, key, newline) ?: return null + path = path + key + } + val app = section(updated, path) ?: return null + val value = "\"" + escape(options) + "\"" + entries(updated, app.valueStart, app.end).firstOrNull { !it.section && it.key.equals("LaunchOptions", true) }?.let { + return updated.replaceRange(it.valueStart, it.end + 1, value) + } + val lineStart = closingLine(updated, app) ?: return null + val indent = updated.substring(lineStart, app.end) + "\t" + return updated.substring(0, lineStart) + indent + "\"LaunchOptions\"\t\t" + value + newline + updated.substring(lineStart) + } + + private class Entry(val key: String, val valueStart: Int, val end: Int, val section: Boolean) + + private fun section(text: String, path: List): Entry? { + var found: Entry? = null + for (key in path) { + found = entries(text, found?.valueStart ?: -1, found?.end ?: text.length) + .firstOrNull { it.section && it.key.equals(key, true) } ?: return null + } + return found + } + + private fun entries(text: String, open: Int, close: Int): List { + val found = ArrayList() + var key: String? = null + var i = open + 1 + while (i < close) { + when { + text[i] == '"' -> { + val end = stringEnd(text, i) ?: return found + if (key == null) key = unescape(text.substring(i + 1, end)) else { found += Entry(key, i, end, false); key = null } + i = end + 1 + } + text[i] == '{' -> { + val end = sectionEnd(text, i) ?: return found + key?.let { found += Entry(it, i, end, true) } + key = null + i = end + 1 + } + text.startsWith("//", i) -> i = text.indexOf('\n', i).let { if (it < 0) close else it } + else -> i++ + } + } + return found + } + + private fun stringEnd(text: String, open: Int): Int? { + var i = open + 1 + while (i < text.length) { + when (text[i]) { + '\\' -> i += 2 + '"' -> return i + else -> i++ + } + } + return null + } + + private fun sectionEnd(text: String, open: Int): Int? { + var depth = 0 + var i = open + while (i < text.length) { + when (text[i]) { + '"' -> i = stringEnd(text, i) ?: return null + '{' -> depth++ + '}' -> if (--depth == 0) return i + } + i++ + } + return null + } + + private fun withSection(text: String, parent: Entry, key: String, newline: String): String? { + if (entries(text, parent.valueStart, parent.end).any { it.section && it.key.equals(key, true) }) return text + val lineStart = closingLine(text, parent) ?: return null + val indent = text.substring(lineStart, parent.end) + "\t" + return text.substring(0, lineStart) + "$indent\"$key\"$newline$indent{$newline$indent}$newline" + text.substring(lineStart) + } + + private fun closingLine(text: String, section: Entry): Int? { + val lineStart = text.lastIndexOf('\n', section.end - 1) + 1 + return lineStart.takeIf { lineStart > section.valueStart && text.substring(lineStart, section.end).all { it == '\t' || it == ' ' } } + } + + private fun escape(value: String) = value.replace("\\", "\\\\").replace("\"", "\\\"") + + private fun unescape(value: String) = value.replace(Regex("\\\\(.)"), "$1") +} diff --git a/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt b/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt index 0cc03b702..027ab1243 100644 --- a/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt +++ b/app/src/main/java/com/droiddeck/launcher/stores/SteamLiveShortcuts.kt @@ -92,6 +92,28 @@ object SteamLiveShortcuts { if (evaluate(context, js) != null) Log.i(TAG, "live shortcuts: updated $id") } + fun launchOptions(context: Context, appId: Long): String? { + if (!clientRunning()) return null + val js = """ + new Promise(done => { + let handle = null, answer; + const finish = d => { clearTimeout(timer); try { handle?.unregister(); } catch (e) {} done(d); }; + const timer = setTimeout(() => finish(null), 4000); + handle = SteamClient.Apps.RegisterForAppDetails(${appId and 0xFFFFFFFFL}, d => { answer = d; if (handle) finish(d); }); + if (answer !== undefined) finish(answer); + }).then(d => typeof d?.strLaunchOptions === 'string' ? d.strLaunchOptions : null) + """.trimIndent() + return evaluate(context, js) + } + + fun setLaunchOptions(context: Context, appId: Long, options: String): Boolean { + if (!clientRunning()) return false + val id = appId and 0xFFFFFFFFL + val done = evaluate(context, "(async () => { SteamClient.Apps.SetAppLaunchOptions($id, ${JSONObject.quote(options)}); return 1; })()") != null + if (done) Log.i(TAG, "live: launch options for $id") + return done + } + fun setArt(context: Context, appId: Long, assetType: Int?, file: java.io.File?, iconPath: String? = null) { if (!clientRunning()) return val id = appId and 0xFFFFFFFFL diff --git a/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt b/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt index 6e2ae810c..e000c996a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/AddedGameEditor.kt @@ -68,6 +68,7 @@ import com.droiddeck.launcher.frontend.AddedGames import com.droiddeck.launcher.frontend.LogoPosition import com.droiddeck.launcher.frontend.SteamGameArt import com.droiddeck.launcher.frontend.SteamGridDb +import com.droiddeck.launcher.frontend.SteamLaunchOptions import com.droiddeck.launcher.session.SessionPrefs import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -191,7 +192,7 @@ internal fun SteamGameArtEditor( game: com.droiddeck.launcher.frontend.Library.SteamGame, onClose: () -> Unit, onChanged: () -> Unit, ) { val target = remember(game.appId) { ArtTarget.of(game) } - ArtOnlyEditor(game.name, target, game, { SteamGameArt.describe(it, game) }, onClose, onChanged) + ArtOnlyEditor(game.name, target, game, { SteamGameArt.describe(it, game) }, onClose, onChanged, launchAppId = game.appId) } @Composable @@ -207,18 +208,25 @@ internal fun StoreGameArtEditor(folder: String, name: String, onClose: () -> Uni @Composable private fun ArtOnlyEditor( name: String, target: ArtTarget?, refresh: Any?, describe: (Context) -> List, - onClose: () -> Unit, onChanged: () -> Unit, + onClose: () -> Unit, onChanged: () -> Unit, launchAppId: Int? = null, ) { val app = LocalContext.current.applicationContext val colors = MaterialTheme.colorScheme + val scope = rememberCoroutineScope() val shown = rememberShown(onClose) val close = { shown.targetState = false } var version by remember { mutableIntStateOf(0) } var arts by remember { mutableStateOf>(emptyList()) } var artSlot by remember { mutableStateOf(null) } var logoOpen by remember { mutableStateOf(false) } + var launchOptions by remember { mutableStateOf(null) } + var launchVersion by remember { mutableIntStateOf(0) } + var editingLaunch by remember { mutableStateOf(null) } + var launchSaving by remember { mutableStateOf(false) } + var launchError by remember { mutableStateOf(null) } val position = rememberLogoPosition(target, version) LaunchedEffect(version, refresh) { if (target != null) arts = withContext(Dispatchers.IO) { describe(app) } } + LaunchedEffect(launchAppId, launchVersion) { if (launchAppId != null) launchOptions = withContext(Dispatchers.IO) { SteamLaunchOptions.current(app, launchAppId) } } val first = remember { FocusRequester() } LaunchedEffect(Unit) { focusWithinFrames({ false }) { first } } AppDialog(shown, close, "artEditor", wide = false, maxWidth = 460.dp) { @@ -228,10 +236,19 @@ private fun ArtOnlyEditor( verticalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.fillMaxWidth().heightIn(max = maxHeight).verticalScroll(rememberScrollState()), ) { - Text(stringResource(R.string.edit_artwork), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 4.dp, top = 2.dp)) + if (launchAppId != null) { + FieldRow(stringResource(R.string.edit_launch_options), focus = first, onClick = { if (!launchSaving) launchOptions?.let { editingLaunch = it } }) { + val options = launchOptions + if (options == null || launchSaving) Loading() else ValueText(options.ifEmpty { "—" }, muted = options.isEmpty()) + } + val error = launchError + if (error != null) Text(stringResource(error), fontSize = 12.sp, color = colors.error, modifier = Modifier.padding(start = 4.dp)) + else Text(stringResource(R.string.edit_launch_options_steam_hint), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 4.dp)) + } + Text(stringResource(R.string.edit_artwork), fontSize = 12.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(start = 4.dp, top = if (launchAppId != null) 8.dp else 2.dp)) for (slot in AddedGameArt.Slot.values()) key(slot) { val state = arts.firstOrNull { it.slot == slot } - FieldRow(slot.label(), focus = if (slot == AddedGameArt.Slot.COVER) first else null, onClick = { if (target != null) artSlot = slot }) { + FieldRow(slot.label(), focus = if (slot == AddedGameArt.Slot.COVER && launchAppId == null) first else null, onClick = { if (target != null) artSlot = slot }) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { SlotThumb(slot, state?.file) if (state != null) ValueText(artSourceLabel(state.source), muted = true) else Loading() @@ -241,6 +258,27 @@ private fun ArtOnlyEditor( PickerRow(Icons.Outlined.Tune, stringResource(R.string.art_adjust_logo), logoPositionSummary(position?.value), enabled = target != null && position != null) { logoOpen = true } } } + val before = editingLaunch + if (before != null && launchAppId != null) TextEntryDialog(stringResource(R.string.edit_launch_options), before, onDismiss = { editingLaunch = null }) { value -> + if (launchSaving || value == before.trim()) return@TextEntryDialog + launchSaving = true + scope.launch { + try { + val saved = withContext(Dispatchers.IO) { SteamLaunchOptions.set(app, launchAppId, value, before) } + launchError = when (saved) { + SteamLaunchOptions.Saved.YES -> null + SteamLaunchOptions.Saved.DESKTOP -> R.string.edit_launch_options_desktop + SteamLaunchOptions.Saved.NO -> R.string.edit_launch_options_not_saved + } + if (saved == SteamLaunchOptions.Saved.YES) launchOptions = value else { + launchOptions = null + launchVersion++ + } + } finally { + launchSaving = false + } + } + } val chosen = target ?: return artSlot?.let { slot -> ArtChooserDialog(chosen, slot, arts.firstOrNull { it.slot == slot }, onBack = { artSlot = null }, onCloseAll = { artSlot = null; close() }) { version++; onChanged() } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index e14bb90e5..a70a67de3 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -2473,6 +2473,9 @@ الهدف بدء التشغيل في خيارات التشغيل + مشتركة مع خصائص Steam: أي تغيير في أحدهما يظهر في كليهما. + تعذّر حفظ خيارات التشغيل في Steam. حاول مرة أخرى. + قد يكون Steam مفتوحًا في جلسة سطح المكتب. غيّر خيارات التشغيل من خصائصه، أو أغلق الجلسة أولًا. الصور الفنية ملف exe آخر… إزالة diff --git a/app/src/main/res/values-b+es+419/strings.xml b/app/src/main/res/values-b+es+419/strings.xml index 9358f7d87..6525fb886 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -2422,6 +2422,9 @@ Destino Iniciar en Opciones de lanzamiento + Se comparten con las Propiedades de Steam: un cambio en cualquiera de los dos se ve en ambos. + No se pudieron guardar las opciones de lanzamiento en Steam. Inténtalo de nuevo. + Es posible que Steam esté abierto en la sesión de escritorio. Cambia las opciones de lanzamiento en sus Propiedades o cierra primero la sesión. Ilustraciones Otro exe… Quitar diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index ccb08323d..8b19eb84f 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -2381,6 +2381,9 @@ Цел Старт в Настройки за стартиране + Общи са със „Свойства“ в Steam: промяна на едното място се вижда и на двете. + Настройките за стартиране не можаха да бъдат запазени в Steam. Опитайте отново. + Steam може да е отворен в сесията на работния плот. Променете настройките за стартиране в неговите „Свойства“ или първо затворете сесията. Визия Друг exe… Премахни diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 6ab10c305..f3fd6ccb2 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -2427,6 +2427,9 @@ Cíl Spustit v Možnosti spuštění + Sdíleno s Vlastnostmi ve Steamu: změna na jednom místě se projeví na obou. + Možnosti spuštění se nepodařilo uložit do Steamu. Zkuste to znovu. + Steam může být otevřený v relaci Plochy. Změňte možnosti spuštění v jeho Vlastnostech, nebo nejdřív relaci ukončete. Grafika Jiný exe… Odstranit diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index dc191215d..b3010fd6e 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -2381,6 +2381,9 @@ Mål Start i Startindstillinger + Delt med Egenskaber i Steam: en ændring det ene sted vises begge steder. + Startindstillingerne kunne ikke gemmes i Steam. Prøv igen. + Steam kan være åben i skrivebordssessionen. Skift startindstillingerne under dens Egenskaber, eller luk sessionen først. Grafik Anden exe… Fjern diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 038a2760a..94820d51b 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2370,6 +2370,9 @@ Ziel Ausführen in Startoptionen + Gemeinsam mit den Eigenschaften in Steam: Eine Änderung an einer Stelle gilt für beide. + Die Startoptionen konnten nicht in Steam gespeichert werden. Versuche es erneut. + Steam ist eventuell in der Desktop-Sitzung geöffnet. Ändere die Startoptionen dort in den Eigenschaften oder beende zuerst die Sitzung. Grafiken Andere EXE… Entfernen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e2e3e73cb..fe7436ed0 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -2381,6 +2381,9 @@ Προορισμός Εκκίνηση σε Επιλογές εκκίνησης + Κοινές με τις Ιδιότητες του Steam: μια αλλαγή στο ένα σημείο εμφανίζεται και στα δύο. + Δεν ήταν δυνατή η αποθήκευση των επιλογών εκκίνησης στο Steam. Δοκιμάστε ξανά. + Το Steam μπορεί να είναι ανοιχτό στη συνεδρία επιφάνειας εργασίας. Αλλάξτε τις επιλογές εκκίνησης από τις Ιδιότητές του ή κλείστε πρώτα τη συνεδρία. Γραφικά Άλλο exe… Κατάργηση diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 902e89891..1926467ed 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2401,6 +2401,9 @@ Destino Iniciar en Opciones de lanzamiento + Se comparten con las Propiedades de Steam: un cambio en cualquiera de los dos se ve en ambos. + No se pudieron guardar las opciones de lanzamiento en Steam. Inténtalo de nuevo. + Puede que Steam esté abierto en la sesión de escritorio. Cambia las opciones de lanzamiento en sus Propiedades o cierra antes la sesión. Ilustraciones Otro exe… Quitar diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 04048ab0a..0d15d50ed 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -2381,6 +2381,9 @@ Kohde Käynnistyskansio Käynnistysasetukset + Jaettu Steamin Ominaisuuksien kanssa: muutos jommassakummassa näkyy molemmissa. + Käynnistysasetuksia ei voitu tallentaa Steamiin. Yritä uudelleen. + Steam voi olla auki työpöytäistunnossa. Muuta käynnistysasetuksia sen Ominaisuuksista tai sulje istunto ensin. Kuvitus Toinen exe… Poista diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index fa208f964..4d20f39d8 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2401,6 +2401,9 @@ Cible Démarrer dans Options de lancement + Partagées avec les Propriétés de Steam : une modification d’un côté apparaît des deux. + Impossible d’enregistrer les options de lancement dans Steam. Réessayez. + Steam est peut-être ouvert dans la session Bureau. Modifiez les options de lancement dans ses Propriétés, ou fermez d’abord la session. Illustrations Autre exe… Retirer diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 3f1f1c5f4..cf457b84c 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -2381,6 +2381,9 @@ टार्गेट यहां शुरू करें लॉन्च विकल्प + Steam की Properties के साथ साझा: किसी एक जगह किया गया बदलाव दोनों में दिखता है। + लॉन्च विकल्प Steam में सहेजे नहीं जा सके। फिर से कोशिश करें। + Steam डेस्कटॉप सेशन में खुला हो सकता है। लॉन्च विकल्प उसकी Properties में बदलें, या पहले सेशन बंद करें। आर्टवर्क दूसरा exe… हटाएं diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 452f12604..48094b1e5 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -2381,6 +2381,9 @@ Cél Indítás innen Indítási beállítások + Közös a Steam Tulajdonságaival: az egyik helyen végzett módosítás mindkettőben megjelenik. + Az indítási beállításokat nem sikerült menteni a Steambe. Próbáld újra. + Lehet, hogy a Steam nyitva van az asztali munkamenetben. Módosítsd az indítási beállításokat a Tulajdonságaiban, vagy előbb zárd be a munkamenetet. Illusztráció Másik exe… Eltávolítás diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index f062fb4ed..00cf8f78f 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -2358,6 +2358,9 @@ Target Mulai di Opsi peluncuran + Dibagikan dengan Properti Steam: perubahan di salah satunya muncul di keduanya. + Tidak dapat menyimpan opsi peluncuran ke Steam. Coba lagi. + Steam mungkin terbuka di sesi desktop. Ubah opsi peluncuran di Properti Steam, atau tutup sesinya terlebih dahulu. Artwork Exe lain… Hapus diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 15dae30de..f149bdf12 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2393,6 +2393,9 @@ Destinazione Avvia in Opzioni di avvio + Condivise con le Proprietà di Steam: una modifica in uno dei due punti compare in entrambi. + Impossibile salvare le opzioni di avvio in Steam. Riprova. + Steam potrebbe essere aperto nella sessione desktop. Cambia le opzioni di avvio nelle sue Proprietà o chiudi prima la sessione. Illustrazioni Altro exe… Rimuovi diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 9cc31a47f..28592e569 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -2325,6 +2325,9 @@ ターゲット 開始フォルダ 起動オプション + Steam のプロパティと共通です。どちらかで変更すると両方に反映されます。 + 起動オプションを Steam に保存できませんでした。もう一度お試しください。 + デスクトップのセッションで Steam が開いている可能性があります。Steam のプロパティで起動オプションを変更するか、先にセッションを終了してください。 アートワーク 別の exe… 削除 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f905c3dce..e8bf23000 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2358,6 +2358,9 @@ 대상 시작 위치 실행 옵션 + Steam 속성과 공유됩니다. 어느 한쪽에서 바꾸면 양쪽에 모두 반영됩니다. + 실행 옵션을 Steam에 저장하지 못했습니다. 다시 시도하세요. + 데스크톱 세션에서 Steam이 열려 있을 수 있습니다. Steam 속성에서 실행 옵션을 바꾸거나 먼저 세션을 닫으세요. 아트워크 다른 exe… 제거 diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index ffb5c74f8..e3f0442f7 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -2358,6 +2358,9 @@ Sasaran Mula dalam Pilihan pelancaran + Dikongsi dengan Properties dalam Steam: perubahan di salah satu tempat dipaparkan di kedua-duanya. + Tidak dapat menyimpan pilihan pelancaran ke Steam. Cuba lagi. + Steam mungkin terbuka dalam sesi desktop. Tukar pilihan pelancaran dalam Properties Steam, atau tutup sesi itu dahulu. Seni Exe lain… Alih keluar diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index cd244a855..9fd07d25a 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -2381,6 +2381,9 @@ Mål Start i Startalternativer + Delt med Egenskaper i Steam: en endring det ene stedet vises begge steder. + Kunne ikke lagre startalternativene i Steam. Prøv igjen. + Steam kan være åpent i skrivebordssesjonen. Endre startalternativene under Egenskaper der, eller lukk sesjonen først. Grafikk Annen exe… Fjern diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index d0af5040f..4ed7d9246 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -2381,6 +2381,9 @@ Doel Beginnen in Opstartopties + Gedeeld met de Eigenschappen in Steam: een wijziging op de ene plek is op beide te zien. + De opstartopties konden niet in Steam worden opgeslagen. Probeer het opnieuw. + Steam is misschien geopend in de desktopsessie. Wijzig de opstartopties daar in de Eigenschappen, of sluit eerst de sessie. Artwork Andere exe… Verwijderen diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 85c9ceb57..7825c8dae 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -2427,6 +2427,9 @@ Element docelowy Uruchom w Opcje uruchamiania + Wspólne z Właściwościami w Steam: zmiana w jednym miejscu jest widoczna w obu. + Nie udało się zapisać opcji uruchamiania w Steam. Spróbuj ponownie. + Steam może być otwarty w sesji pulpitu. Zmień opcje uruchamiania w jego Właściwościach albo najpierw zamknij sesję. Grafika Inny plik exe… Usuń diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index c92425f3b..72a0a5302 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -2393,6 +2393,9 @@ Destino Iniciar em Opções de arranque + Partilhadas com as Propriedades do Steam: uma alteração num dos locais aparece em ambos. + Não foi possível guardar as opções de arranque no Steam. Tenta novamente. + O Steam pode estar aberto na sessão do ambiente de trabalho. Altera as opções de arranque nas Propriedades dele, ou fecha primeiro a sessão. Imagens Outro exe… Remover diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index dd3f1b174..ce64e5a1b 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -2393,6 +2393,9 @@ Destino Iniciar em Opções de inicialização + Compartilhadas com as Propriedades da Steam: uma alteração em um dos lugares aparece nos dois. + Não foi possível salvar as opções de inicialização na Steam. Tente novamente. + A Steam pode estar aberta na sessão da área de trabalho. Altere as opções de inicialização nas Propriedades dela ou feche a sessão primeiro. Artes Outro exe… Remover diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 9d24ffa6f..573852195 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2404,6 +2404,9 @@ Țintă Pornește în Opțiuni de lansare + Comune cu Proprietățile din Steam: o modificare într-unul dintre locuri apare în ambele. + Opțiunile de lansare nu au putut fi salvate în Steam. Încearcă din nou. + Steam poate fi deschis în sesiunea desktop. Schimbă opțiunile de lansare din Proprietățile lui sau închide mai întâi sesiunea. Grafică Alt exe… Elimină diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b3d8964f3..3c83e4f84 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2424,6 +2424,9 @@ Объект Рабочая папка Параметры запуска + Общие со «Свойствами» Steam: изменение в одном месте видно в обоих. + Не удалось сохранить параметры запуска в Steam. Попробуйте ещё раз. + Steam может быть открыт в сессии рабочего стола. Измените параметры запуска в его «Свойствах» или сначала закройте сессию. Обложки Другой exe… Убрать diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index fe55a9a9b..7f6e707ff 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -2381,6 +2381,9 @@ Mål Starta i Startalternativ + Delas med Egenskaper i Steam: en ändring på det ena stället syns på båda. + Det gick inte att spara startalternativen i Steam. Försök igen. + Steam kan vara öppet i skrivbordssessionen. Ändra startalternativen under Egenskaper där, eller stäng sessionen först. Bildmaterial Annan exe… Ta bort diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index e940ffe6f..a733c3701 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -2358,6 +2358,9 @@ เป้าหมาย เริ่มที่ ตัวเลือกการเปิด + ใช้ร่วมกับคุณสมบัติใน Steam: เปลี่ยนที่ใดที่หนึ่งก็จะแสดงในทั้งสองที่ + บันทึกตัวเลือกการเปิดลงใน Steam ไม่ได้ ลองอีกครั้ง + Steam อาจเปิดอยู่ในเซสชันเดสก์ท็อป เปลี่ยนตัวเลือกการเปิดในคุณสมบัติของ Steam หรือปิดเซสชันก่อน ภาพปก exe อื่น… ลบ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 2ff060d9c..72e503ff0 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -2370,6 +2370,9 @@ Hedef Başlangıç yeri Başlatma seçenekleri + Steam Özellikleri ile ortaktır: birinde yapılan değişiklik ikisinde de görünür. + Başlatma seçenekleri Steam\'e kaydedilemedi. Tekrar deneyin. + Steam masaüstü oturumunda açık olabilir. Başlatma seçeneklerini Steam Özellikleri\'nden değiştirin veya önce oturumu kapatın. Görsel Diğer exe… Kaldır diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 51913b805..64f9e0d48 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2427,6 +2427,9 @@ Ціль Робоча папка Параметри запуску + Спільні з «Властивостями» Steam: зміна в одному місці видна в обох. + Не вдалося зберегти параметри запуску в Steam. Спробуйте ще раз. + Steam може бути відкритий у сеансі робочого столу. Змініть параметри запуску в його «Властивостях» або спершу закрийте сеанс. Графіка Інший exe… Вилучити diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 77c746636..bbe26802f 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -2358,6 +2358,9 @@ Đích Khởi chạy trong Tùy chọn khởi chạy + Dùng chung với Thuộc tính của Steam: thay đổi ở một nơi sẽ hiện ở cả hai. + Không thể lưu tùy chọn khởi chạy vào Steam. Hãy thử lại. + Steam có thể đang mở trong phiên desktop. Hãy đổi tùy chọn khởi chạy trong Thuộc tính của Steam, hoặc đóng phiên trước. Hình ảnh Exe khác… Xóa diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1f5e13cba..d4548e42d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2400,6 +2400,9 @@ 目标 起始位置 启动选项 + 与 Steam 的“属性”共用:在任一处更改,两处都会更新。 + 无法将启动选项保存到 Steam。请重试。 + Steam 可能正在桌面会话中运行。请在 Steam 的“属性”中更改启动选项,或先关闭该会话。 美术图 其他 exe… 移除 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 647ca1b0e..9cda1d2d3 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -2326,6 +2326,9 @@ 目標 開始位置 啟動選項 + 與 Steam 的「內容」共用:在任一處更改,兩處都會更新。 + 無法將啟動選項儲存到 Steam。請再試一次。 + Steam 可能正在桌面會話中執行。請在 Steam 的「內容」中更改啟動選項,或先關閉該會話。 圖片 其他 exe… 移除 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 913a8d50b..f2d2b3a85 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2326,6 +2326,9 @@ 目標 開始位置 啟動選項 + 與 Steam 的「內容」共用:在任一處變更,兩處都會更新。 + 無法將啟動選項儲存至 Steam。請再試一次。 + Steam 可能正在桌面工作階段中執行。請在 Steam 的「內容」中變更啟動選項,或先關閉該工作階段。 圖片 其他 exe… 移除 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a75e668c..d16af65ea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2508,6 +2508,9 @@ Target Start in Launch options + Shared with Steam\'s Properties: a change in either one shows in both. + Could not save the launch options to Steam. Try again. + Steam may be open in the desktop session. Change the launch options in its Properties, or close the session first. Artwork Other exe… Remove diff --git a/app/src/test/java/com/droiddeck/launcher/frontend/SteamLaunchOptionsTest.kt b/app/src/test/java/com/droiddeck/launcher/frontend/SteamLaunchOptionsTest.kt new file mode 100644 index 000000000..46cdd111c --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/frontend/SteamLaunchOptionsTest.kt @@ -0,0 +1,163 @@ +package com.droiddeck.launcher.frontend + +import com.droiddeck.launcher.session.SessionPrefs +import com.droiddeck.launcher.session.SessionService +import com.droiddeck.launcher.session.SessionState +import java.io.File +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [33]) +class SteamLaunchOptionsTest { + @get:Rule val tmp = TemporaryFolder() + + private fun config(apps: String) = listOf( + "\"UserLocalConfigStore\"", "{", "\t\"Software\"", "\t{", "\t\t\"Valve\"", "\t\t{", "\t\t\t\"Steam\"", "\t\t\t{", + apps, "\t\t\t}", "\t\t}", "\t}", "\t\"WebStorage\"", "\t{", "\t\t\"x\"\t\t\"1\"", "\t}", "}", "", + ).joinToString("\n") + + private fun apps(vararg lines: String) = (listOf("\t\t\t\t\"apps\"", "\t\t\t\t{") + lines + "\t\t\t\t}").joinToString("\n") + + private val sparking = arrayOf( + "\t\t\t\t\t\"1790600\"", "\t\t\t\t\t{", "\t\t\t\t\t\t\"LastPlayed\"\t\t\"1761897600\"", "\t\t\t\t\t\t\"LaunchOptions\"\t\t\"-dx11\"", "\t\t\t\t\t}", + ) + + @Test fun replacesTheOptionsSteamHas() { + val text = config(apps(*sparking)) + val updated = SteamLaunchOptions.withLaunchOptions(text, 1790600, "-dx12")!! + assertEquals(text.replace("\"-dx11\"", "\"-dx12\""), updated) + assertEquals("-dx12", SteamLaunchOptions.launchOptions(updated, 1790600)) + } + + @Test fun addsOptionsToAGameThatHasNone() { + val text = config(apps("\t\t\t\t\t\"1790600\"", "\t\t\t\t\t{", "\t\t\t\t\t\t\"LastPlayed\"\t\t\"1761897600\"", "\t\t\t\t\t}")) + assertEquals(config(apps(*sparking)).replace("-dx11", "-vulkan"), SteamLaunchOptions.withLaunchOptions(text, 1790600, "-vulkan")) + } + + @Test fun addsTheGameAndTheAppsSectionWhenSteamHasNeither() { + val updated = SteamLaunchOptions.withLaunchOptions(config("\t\t\t\t\"language\"\t\t\"english\""), 1790600, "-dx11")!! + assertEquals( + config( + "\t\t\t\t\"language\"\t\t\"english\"\n" + + apps("\t\t\t\t\t\"1790600\"", "\t\t\t\t\t{", "\t\t\t\t\t\t\"LaunchOptions\"\t\t\"-dx11\"", "\t\t\t\t\t}"), + ), + updated, + ) + assertEquals("-dx11", SteamLaunchOptions.launchOptions(updated, 1790600)) + } + + @Test fun quotesAndBackslashesAreEscapedAndReadBack() { + val options = "PROTON_LOG=1 %command% -path \"C:\\Games\\DB\"" + val updated = SteamLaunchOptions.withLaunchOptions(config(apps(*sparking)), 1790600, options)!! + assertEquals(true, updated.contains("\"PROTON_LOG=1 %command% -path \\\"C:\\\\Games\\\\DB\\\"\"")) + assertEquals(options, SteamLaunchOptions.launchOptions(updated, 1790600)) + } + + @Test fun emptyOptionsClearSteams() { + val updated = SteamLaunchOptions.withLaunchOptions(config(apps(*sparking)), 1790600, "")!! + assertEquals("", SteamLaunchOptions.launchOptions(updated, 1790600)) + } + + @Test fun windowsLineEndsAreKept() { + val text = config("\t\t\t\t\"language\"\t\t\"english\"").replace("\n", "\r\n") + val updated = SteamLaunchOptions.withLaunchOptions(text, 1790600, "-dx11")!! + assertEquals(false, updated.replace("\r\n", "").contains('\n')) + assertEquals("-dx11", SteamLaunchOptions.launchOptions(updated, 1790600)) + } + + @Test fun onlyTheGamesOwnSectionChanges() { + val other = arrayOf( + "\t\t\t\t\t\"7\"", "\t\t\t\t\t{", "\t\t\t\t\t\t\"1790600\"", "\t\t\t\t\t\t{", "\t\t\t\t\t\t\t\"LaunchOptions\"\t\t\"keep\"", "\t\t\t\t\t\t}", "\t\t\t\t\t}", + ) + val updated = SteamLaunchOptions.withLaunchOptions(config(apps(*other, *sparking)), 1790600, "-dx12")!! + assertEquals(true, updated.contains("\"keep\"")) + assertEquals("-dx12", SteamLaunchOptions.launchOptions(updated, 1790600)) + assertNull(SteamLaunchOptions.launchOptions(config(apps(*other)), 1790600)) + } + + @Test fun otherFilesAreLeftAlone() { + assertNull(SteamLaunchOptions.withLaunchOptions("\"InstallConfigStore\"\n{\n}\n", 1790600, "-dx11")) + assertNull(SteamLaunchOptions.withLaunchOptions("", 1790600, "-dx11")) + assertNull(SteamLaunchOptions.launchOptions("", 1790600)) + } + + private fun steamHome(home: File = tmp.newFolder("Steam")): File = home.also { + File(home, "config").mkdirs() + File(home, "config/loginusers.vdf").writeText( + "\"users\"\n{\n\t\"76561198021239924\"\n\t{\n\t\t\"AccountName\"\t\t\"a\"\n\t\t\"MostRecent\"\t\t\"1\"\n\t}\n}\n") + } + + private fun localConfig(home: File, account: String) = File(home, "userdata/$account/config/localconfig.vdf").apply { parentFile!!.mkdirs() } + + @Test fun aChangeGoesToTheSignedInAccountOnly() { + val home = steamHome() + val signedIn = localConfig(home, "60974196").apply { writeText(config(apps(*sparking))) } + val other = localConfig(home, "123").apply { writeText(config("\t\t\t\t\"language\"\t\t\"english\"")) } + val untouched = other.readText() + assertTrue(SteamLaunchOptions.write(home, 1790600, "-dx12")) + assertEquals("-dx12", SteamLaunchOptions.launchOptions(signedIn.readText(), 1790600)) + assertEquals(untouched, other.readText()) + assertFalse(File(signedIn.parentFile, "localconfig.vdf.staged").exists()) + } + + @Test fun whatSteamWroteLastIsWhatTheGameHas() { + val home = steamHome() + val config = localConfig(home, "60974196").apply { writeText(config(apps(*sparking))) } + assertTrue(SteamLaunchOptions.write(home, 1790600, "-dx12")) + config.writeText(SteamLaunchOptions.withLaunchOptions(config.readText(), 1790600, "-vulkan")!!) + assertEquals("-vulkan", SteamLaunchOptions.steam(home, 1790600)) + assertNull(SteamLaunchOptions.steam(home, 400)) + } + + private val app get() = RuntimeEnvironment.getApplication() + + private val written = ArrayList() + + private fun sessionConfig(): File { + val home = steamHome(SessionPrefs.activeSteamRoot(app)) + return localConfig(home, "60974196").apply { writeText(config(apps(*sparking))) } + .also { written += it; written += File(home, "config/loginusers.vdf") } + } + + @After fun noSession() { + SessionState.running = false + SessionState.mode = SessionService.MODE_STEAM + written.forEach { it.delete() } + } + + @Test fun aValueChangedInSteamSinceItWasShownIsNotOverwritten() { + val config = sessionConfig() + assertEquals(SteamLaunchOptions.Saved.NO, SteamLaunchOptions.set(app, 1790600, "-dx12", shown = "-vulkan")) + assertEquals("-dx11", SteamLaunchOptions.launchOptions(config.readText(), 1790600)) + assertEquals(SteamLaunchOptions.Saved.YES, SteamLaunchOptions.set(app, 1790600, "-dx12", shown = "-dx11")) + assertEquals("-dx12", SteamLaunchOptions.launchOptions(config.readText(), 1790600)) + } + + @Test fun aDesktopSessionLeavesTheFileToItsClient() { + val config = sessionConfig() + val before = config.readText() + SessionState.running = true + SessionState.mode = SessionService.MODE_DESKTOP + assertEquals(SteamLaunchOptions.Saved.DESKTOP, SteamLaunchOptions.set(app, 1790600, "-dx12", shown = "-dx11")) + assertEquals(before, config.readText()) + } + + @Test fun nothingIsWrittenWithoutASignedInAccountsConfig() { + assertFalse(SteamLaunchOptions.write(tmp.newFolder("NoSteam"), 1790600, "-dx11")) + val home = steamHome() + assertFalse(SteamLaunchOptions.write(home, 1790600, "-dx11")) + localConfig(home, "60974196").writeText("\"InstallConfigStore\"\n{\n}\n") + assertFalse(SteamLaunchOptions.write(home, 1790600, "-dx11")) + } +}