From a309428a2aa8f963aa26bc4e2ef8638d296cfced Mon Sep 17 00:00:00 2001 From: MaxsTechReview Date: Thu, 5 Nov 2026 02:04:03 -0500 Subject: [PATCH] feat(battlenet): import, list and delete World of Warcraft addons (#2111) World of Warcraft, Classic and Forever get an Addons tab in the Library. It imports addon zips into the right Interface/AddOns folder, lists the installed addons, and deletes one after a confirmation. --- README.md | 1 + .../droiddeck/launcher/FrontEndActivity.kt | 28 +++ .../droiddeck/launcher/runtime/WowAddons.kt | 164 +++++++++++++ .../droiddeck/launcher/ui/FrontEndScreen.kt | 2 + .../com/droiddeck/launcher/ui/GameDetail.kt | 47 +++- app/src/main/res/values-ar/strings.xml | 28 +++ app/src/main/res/values-b+es+419/strings.xml | 22 ++ app/src/main/res/values-bg/strings.xml | 20 ++ app/src/main/res/values-cs/strings.xml | 24 ++ app/src/main/res/values-da/strings.xml | 20 ++ app/src/main/res/values-de/strings.xml | 20 ++ app/src/main/res/values-el/strings.xml | 20 ++ app/src/main/res/values-es/strings.xml | 22 ++ app/src/main/res/values-fi/strings.xml | 20 ++ app/src/main/res/values-fr/strings.xml | 22 ++ app/src/main/res/values-hi/strings.xml | 20 ++ app/src/main/res/values-hu/strings.xml | 20 ++ app/src/main/res/values-in/strings.xml | 18 ++ app/src/main/res/values-it/strings.xml | 22 ++ app/src/main/res/values-ja/strings.xml | 18 ++ app/src/main/res/values-ko/strings.xml | 18 ++ app/src/main/res/values-ms/strings.xml | 18 ++ app/src/main/res/values-nb/strings.xml | 20 ++ app/src/main/res/values-nl/strings.xml | 20 ++ app/src/main/res/values-pl/strings.xml | 24 ++ app/src/main/res/values-pt-rPT/strings.xml | 22 ++ app/src/main/res/values-pt/strings.xml | 22 ++ app/src/main/res/values-ro/strings.xml | 22 ++ app/src/main/res/values-ru/strings.xml | 24 ++ app/src/main/res/values-sv/strings.xml | 20 ++ app/src/main/res/values-th/strings.xml | 18 ++ app/src/main/res/values-tr/strings.xml | 20 ++ app/src/main/res/values-uk/strings.xml | 24 ++ app/src/main/res/values-vi/strings.xml | 18 ++ app/src/main/res/values-zh-rCN/strings.xml | 18 ++ app/src/main/res/values-zh-rHK/strings.xml | 18 ++ app/src/main/res/values-zh-rTW/strings.xml | 18 ++ app/src/main/res/values/strings.xml | 20 ++ .../launcher/runtime/WowAddonsTest.kt | 221 ++++++++++++++++++ 39 files changed, 1142 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/droiddeck/launcher/runtime/WowAddons.kt create mode 100644 app/src/test/java/com/droiddeck/launcher/runtime/WowAddonsTest.kt diff --git a/README.md b/README.md index cc233c380..8b834ff00 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V - Epic's official Linux launcher preview runs through FEX. That setup is outside Epic's listed Linux support. - Battle.net is by [@kayakyakr](https://github.com/kayakyakr). It runs Blizzard's own client in Proton (downloaded from Blizzard, not included) and shows key art from the [Battle.net Shop](https://shop.battle.net). Without Steam, its games see the pad as an Xbox 360 controller via `PROTON_SPOOF_STEAMINPUT_VIDPID` in [Proton](https://github.com/ValveSoftware/wine)'s `dlls/winebus.sys`. - Battle.net installs drive Blizzard's Agent the way [@barncastle](https://github.com/barncastle)'s [Battle.Net-Installer](https://github.com/barncastle/Battle.Net-Installer) does, with requests from [wowdev.wiki](https://wowdev.wiki/Agent). Read without taking code: [@mubaraknumann](https://github.com/mubaraknumann)'s [unifideck](https://github.com/mubaraknumann/unifideck), [@djmittens](https://github.com/djmittens)'s [neo-mittens](https://github.com/djmittens/neo-mittens) `bnetswitch`, [@BCD1210](https://github.com/BCD1210)'s [soju](https://github.com/BCD1210/soju) and [Lutris](https://github.com/lutris/lutris)'s `product.db` reader. +- World of Warcraft addon imports replace an addon's folder from a temporary copy, as [WowUp](https://github.com/WowUp/WowUp) does, and read flavour folders the way [WoWDeck](https://github.com/sigboe/wowdeck) does. No code from either is used. - Xbox games load [@domsvoboda](https://github.com/domsvoboda)'s [xodus-nexus](https://github.com/domsvoboda/xodus-nexus) modules (LGPL-2.1-or-later), including `wintypes.dll`. Minecraft's GameInput setup was checked against [@Wyze3306](https://github.com/Wyze3306)'s [BedrockOnLinux](https://github.com/Wyze3306/BedrockOnLinux) (MIT). - The Xbox Cloud Gaming player uses the WebRTC client of [@unknownskl](https://github.com/unknownskl)'s [Greenlight](https://github.com/unknownskl/greenlight) (MIT), and its offer tweaks and sharpening follow [@redphx](https://github.com/redphx)'s [Better xCloud](https://github.com/redphx/better-xcloud) (MIT). See `tools/xcloud-player/NOTICE.md`. - The itch.io butler pack ships butler's `7z.so`, itch.io's build of [7-Zip](https://www.7-zip.org) by Igor Pavlov (LGPL-2.1, unRAR restriction on RAR code), unmodified. Other archives use Apache Commons Compress and the runtime's bsdtar (libarchive). diff --git a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt index 14e7f5722..a7edaa369 100644 --- a/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/FrontEndActivity.kt @@ -433,6 +433,27 @@ class FrontEndActivity : ComponentActivity() { pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), getString(R.string.main_saves_pick_zip, name), GameSaves.savesDir().parentFile?.parentFile?.path)) } + private fun importAddons(name: String, target: com.droiddeck.launcher.runtime.WowAddons.Target) { + if (SessionState.running) { + android.widget.Toast.makeText(this, R.string.main_addons_close_session, android.widget.Toast.LENGTH_LONG).show() + return + } + onSavePicked = { zip -> + saveAction(getString(R.string.main_addons_importing, name)) { + if (SessionState.running) return@saveAction getString(R.string.main_addons_close_session) + val imported = try { + com.droiddeck.launcher.runtime.WowAddons.import(zip, target) + } catch (_: com.droiddeck.launcher.runtime.WowAddons.TooLarge) { + return@saveAction getString(R.string.main_addons_too_large, zip.name) + } + if (imported.isEmpty()) getString(R.string.main_addons_none, zip.name) + else resources.getQuantityString(R.plurals.main_addons_imported, imported.size, imported.size, target.flavor) + } + } + val downloads = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS).path + pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), getString(R.string.main_addons_pick_zip, name), downloads)) + } + private fun exportSaves(name: String, layout: GameSaves.Layout, game: () -> GameSaves.Game) { onSavePicked = { dir -> saveAction(getString(R.string.main_saves_exporting, name)) { @@ -1282,6 +1303,13 @@ class FrontEndActivity : ComponentActivity() { onGameComponents = { game -> openComponents(gameKey = game.profileKey) }, onComponentsTab = { tab -> openComponents(focusContent = true, tab = tab) }, onSaveImport = { sg -> importSaves(sg.name) { GameSaves.game(sg) } }, + onAddonImport = { sg, target -> importAddons(sg.name, target) }, + onAddonDelete = { target, name -> + saveAction(getString(R.string.main_addons_deleting, name)) { + com.droiddeck.launcher.runtime.WowAddons.delete(target, name) + getString(R.string.main_addons_deleted, name) + } + }, onSaveExport = { sg, layout -> exportSaves(sg.name, layout) { GameSaves.game(sg) } }, onPerformance = { refreshCores(); showComponents = false; showMapping = false; showPerformance = true }, onRoms = { showRoms = true }, diff --git a/app/src/main/java/com/droiddeck/launcher/runtime/WowAddons.kt b/app/src/main/java/com/droiddeck/launcher/runtime/WowAddons.kt new file mode 100644 index 000000000..60cfbf37c --- /dev/null +++ b/app/src/main/java/com/droiddeck/launcher/runtime/WowAddons.kt @@ -0,0 +1,164 @@ +package com.droiddeck.launcher.runtime + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.setValue +import com.droiddeck.launcher.frontend.Library +import com.droiddeck.launcher.stores.Store +import com.droiddeck.launcher.stores.StoreInstalls +import org.apache.commons.compress.archivers.zip.ZipFile +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.LinkOption + +object WowAddons { + var imports by mutableIntStateOf(0) + private set + + class TooLarge : IOException() + + class Target(val flavor: String, val addOns: File) + + class Addon(val name: String, val root: String) + + internal const val MAX_EXPANDED_BYTES = 4L * 1024 * 1024 * 1024 + + private val SUFFIX = Regex("[-_](standard|mainline|classic|vanilla|tbc|bcc|wrath|wotlkc|cata|mists|forever)$", RegexOption.IGNORE_CASE) + + fun targets(game: Library.SteamGame): List { + if (game.source != Store.BATTLENET.id) return emptyList() + val title = game.storeId?.let(BattleNet::title) ?: return emptyList() + if (title.flavor == null) return emptyList() + val folder = game.gameFiles ?: return emptyList() + return targets(title, folder) + } + + internal fun targets(title: BattleNet.Title, folder: File): List { + val flavors = BattleNet.TITLES.filter { it.code == title.code }.mapNotNull { it.flavor } + .flatMap { listOf(it, it.dropLast(1) + "_beta_") }.distinct() + val root = if (folder.name in flavors) folder.parentFile ?: return emptyList() else folder + return flavors.map { File(root, it) }.filter { it.isDirectory }.map { Target(it.name, addOnsFolder(it)) } + } + + fun installed(target: Target): List = + target.addOns.listFiles().orEmpty().filter { it.isDirectory && !it.name.startsWith(".") } + .map { it.name }.sortedBy { it.lowercase() } + + fun delete(target: Target, name: String) { + val folder = target.addOns.listFiles()?.firstOrNull { it.name == name && !it.name.startsWith(".") } + ?: throw IOException("Cannot find $name") + StoreInstalls.deleteTree(folder) + if (Files.exists(folder.toPath(), LinkOption.NOFOLLOW_LINKS)) throw IOException("Cannot delete $name") + imports++ + } + + fun import(zipFile: File, target: Target): List { + ZipFile.builder().setFile(zipFile).get().use { zip -> + val all = zip.entries.toList() + if (all.any { it.isUnixSymlink }) return emptyList() + val entries = all.filter { clean(it.name) != null } + val addons = addons(entries.filter { !it.isDirectory }.mapNotNull { clean(it.name) }) + if (addons.isEmpty()) return emptyList() + val addOns = target.addOns + if (!addOns.isDirectory && !addOns.mkdirs()) throw IOException("Cannot create ${addOns.path}") + val stage = File(addOns.parentFile, ".addon-import-${System.nanoTime()}") + try { + val fresh = File(stage, "new") + val buffer = ByteArray(64 * 1024) + var written = 0L + for (entry in entries) { + if (entry.isDirectory) continue + val path = clean(entry.name) ?: continue + val addon = addons.firstOrNull { path.startsWith(it.root) } ?: continue + val out = inside(fresh, addon.name + "/" + path.removePrefix(addon.root)) ?: continue + out.parentFile?.mkdirs() + zip.getInputStream(entry).use { input -> + out.outputStream().use { output -> + while (true) { + val read = input.read(buffer) + if (read < 0) break + written += read + if (written > MAX_EXPANDED_BYTES) throw TooLarge() + output.write(buffer, 0, read) + } + } + } + } + swap(addons.map { File(fresh, it.name) to it.name }, addOns, File(stage, "old").apply { mkdirs() }) + } finally { + StoreInstalls.deleteTree(stage) + } + imports++ + return addons.map { it.name } + } + } + + internal fun addons(files: List): List { + val tocs = files.filter { it.endsWith(".toc", ignoreCase = true) } + val folders = tocs.map { it.substringBeforeLast('/', "") }.distinct().sortedBy { it.length } + val roots = ArrayList() + for (folder in folders) { + if (roots.none { it.isEmpty() || folder.startsWith("$it/") }) roots.add(folder) + } + return roots.mapNotNull { root -> + val inFolder = tocs.filter { it.substringBeforeLast('/', "") == root } + .map { it.substringAfterLast('/').dropLast(4) } + val name = addonName(root.substringAfterLast('/'), inFolder) + name.takeIf { it.isNotEmpty() && !it.startsWith(".") }?.let { Addon(it, if (root.isEmpty()) "" else "$root/") } + }.distinctBy { it.name.lowercase() } + } + + private fun addonName(folder: String, tocs: List): String { + val match = tocs.firstOrNull { it.equals(folder, ignoreCase = true) } + ?: tocs.firstOrNull { it.replace(SUFFIX, "").equals(folder, ignoreCase = true) } + if (match != null) return folder + return tocs.map { it.replace(SUFFIX, "") }.minByOrNull { it.length }.orEmpty() + } + + private fun clean(name: String): String? { + val parts = name.replace('\\', '/').trim('/').split('/') + if (name.startsWith("/") || name.startsWith("\\") || parts.any { it.isEmpty() || it == "." || it == ".." || it.endsWith(":") }) return null + if (parts.first() == "__MACOSX" || parts.last().startsWith("._")) return null + return parts.joinToString("/") + } + + private fun inside(root: File, path: String): File? { + val out = File(root, path) + return out.takeIf { it.canonicalPath.startsWith(root.canonicalPath + File.separator) } + } + + private fun addOnsFolder(flavor: File): File = child(child(flavor, "Interface"), "AddOns") + + private fun child(parent: File, name: String): File = + parent.listFiles()?.firstOrNull { it.name.equals(name, ignoreCase = true) } ?: File(parent, name) + + internal fun swap(fresh: List>, addOns: File, old: File) { + val done = ArrayList() + try { + for ((folder, name) in fresh) done.add(replace(folder, addOns, name, old)) + } catch (e: IOException) { + done.asReversed().forEach { undo(it) } + throw e + } + } + + private class Replaced(val installed: File, val previous: File?, val kept: File?) + + private fun undo(replaced: Replaced) { + StoreInstalls.deleteTree(replaced.installed) + if (replaced.previous != null) replaced.kept?.renameTo(replaced.previous) + } + + private fun replace(fresh: File, addOns: File, name: String, old: File): Replaced { + val current = addOns.listFiles()?.firstOrNull { it.name.equals(name, ignoreCase = true) } + val kept = current?.let { File(old, it.name) } + if (current != null && !current.renameTo(kept!!)) throw IOException("Cannot replace ${current.path}") + val installed = File(addOns, name) + if (!fresh.renameTo(installed)) { + kept?.renameTo(current) + throw IOException("Cannot install $name") + } + return Replaced(installed, current, kept) + } +} diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index 30acb4d85..44f629b34 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -219,6 +219,8 @@ class FrontEndActions( val onGameComponents: (Library.SteamGame) -> Unit = { onComponents(true) }, val onComponentsTab: (String) -> Unit = {}, val onSaveImport: (Library.SteamGame) -> Unit = {}, + val onAddonImport: (Library.SteamGame, com.droiddeck.launcher.runtime.WowAddons.Target) -> Unit = { _, _ -> }, + val onAddonDelete: (com.droiddeck.launcher.runtime.WowAddons.Target, String) -> Unit = { _, _ -> }, val onSaveExport: (Library.SteamGame, com.droiddeck.launcher.session.GameSaves.Layout) -> Unit = { _, _ -> }, val onPerformance: () -> Unit, val onRoms: () -> Unit, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GameDetail.kt b/app/src/main/java/com/droiddeck/launcher/ui/GameDetail.kt index f7efc043f..915a62175 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GameDetail.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GameDetail.kt @@ -73,6 +73,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics @@ -86,7 +87,9 @@ import coil.compose.AsyncImage import com.droiddeck.launcher.R import com.droiddeck.launcher.frontend.GameRemoval import com.droiddeck.launcher.frontend.Library +import com.droiddeck.launcher.runtime.WowAddons import com.droiddeck.launcher.session.GameSaves +import com.droiddeck.launcher.session.SessionState import com.droiddeck.launcher.stores.CatalogItem import com.droiddeck.launcher.stores.CloudSaves import com.droiddeck.launcher.stores.InstalledStoreGame @@ -136,7 +139,7 @@ internal class GameRef(val appId: Int?, val store: Store?, val storeId: String?) } } -internal enum class GameTab { ABOUT, ACHIEVEMENTS, LAUNCH, SAVES, INSTALL } +internal enum class GameTab { ABOUT, ACHIEVEMENTS, LAUNCH, SAVES, ADDONS, INSTALL } @Composable internal fun GameDetail( @@ -180,12 +183,14 @@ internal fun GameDetail( .takeUnless { it.trim().equals(title.trim(), ignoreCase = true) }.orEmpty() val scored = scores != null && (scores.metacritic != Lookup.None || scores.hltb != Lookup.None) val prefix = game?.protonPrefix?.takeIf { it.isDirectory } + val addonTargets = remember(game?.gameId, game?.gameFiles) { game?.let(WowAddons::targets).orEmpty() } val tabs = buildList { if (description.isNotBlank() || developer.isNotBlank() || publisher.isNotBlank() || releaseDate.isNotBlank() || scored || (!s.shortcutPicker && title.isNotBlank() && title != id)) add(GameTab.ABOUT) if (achievementState != null) add(GameTab.ACHIEVEMENTS) val cloud = game?.library == Library.XBOX_CLOUD if (game != null && !cloud) add(GameTab.LAUNCH) if (game != null && game.library == Library.ADDED && prefix != null) add(GameTab.SAVES) + if (addonTargets.isNotEmpty()) add(GameTab.ADDONS) if ((game != null && !cloud) || installed != null) add(GameTab.INSTALL) } var picked by remember(key) { mutableStateOf(null) } @@ -248,6 +253,7 @@ internal fun GameDetail( } ?: stringResource(R.string.achievements_title) GameTab.LAUNCH -> stringResource(R.string.game_tab_launch) GameTab.SAVES -> stringResource(if (Store.byId(source).let { it == Store.GOG || it == Store.EPIC }) R.string.game_tab_saves_cloud else R.string.games_saves) + GameTab.ADDONS -> stringResource(R.string.game_tab_addons) GameTab.INSTALL -> stringResource(R.string.game_tab_install) } } @@ -263,6 +269,7 @@ internal fun GameDetail( GameTab.ACHIEVEMENTS -> achievementState?.let { st -> AchievementsTab(StoreAchievements.forSource(store?.id)?.label.orEmpty(), st) } GameTab.LAUNCH -> if (game != null) LaunchTab(game, s, a, hopRail) GameTab.SAVES -> if (game != null && prefix != null) SavesTab(game, prefix, a) + GameTab.ADDONS -> if (game != null) AddonsTab(game, addonTargets, a) GameTab.INSTALL -> InstallTab(game, installed, item, store) } } @@ -857,6 +864,44 @@ private fun SavesTab(g: Library.SteamGame, prefix: File, a: FrontEndActions) { } } +@Composable +private fun AddonsTab(g: Library.SteamGame, targets: List, a: FrontEndActions) { + Column(Modifier.fillMaxWidth().tabPanel(), verticalArrangement = Arrangement.spacedBy(2.dp)) { + targets.forEach { target -> + val names by produceState(emptyList(), target.addOns, WowAddons.imports) { + value = withContext(Dispatchers.IO) { WowAddons.installed(target) } + } + if (targets.size > 1) SectionLabel(target.flavor) + MenuItem( + stringResource(R.string.games_import_addons), checked = false, + detail = pluralStringResource(R.plurals.games_addons_count, names.size, names.size), + ) { a.onAddonImport(g, target) } + MenuItem(stringResource(R.string.games_open_addons_folder), checked = false) { + target.addOns.mkdirs() + a.onBrowseFiles(target.addOns) + } + names.forEach { name -> AddonRow(name, target, a) } + } + } +} + +@Composable +private fun AddonRow(name: String, target: WowAddons.Target, a: FrontEndActions) { + var asking by remember(target.addOns, name) { mutableStateOf(false) } + val delete = stringResource(R.string.common_delete) + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 2.dp, bottom = 2.dp)) { + Text(name, fontSize = 14.sp, color = MaterialTheme.colorScheme.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f)) + if (!SessionState.running) IconAction(Icons.Outlined.Delete, delete, "addon:delete:${target.flavor}:$name", danger = true) { asking = true } + } + if (asking) ConfirmDialog( + title = stringResource(R.string.games_delete_addon_title, name), + text = stringResource(R.string.games_delete_addon_body), + confirm = delete, + onConfirm = { a.onAddonDelete(target, name) }, + onDismiss = { asking = false }, + ) +} + @Composable private fun CloudRows(g: Library.SteamGame) { val store = Store.byId(g.source)?.takeIf { it != Store.AMAZON } ?: return diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index b3068a6f5..20c77a137 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1207,6 +1207,18 @@ تصدير بصيغة GameHub المضغوطة… تصدير بصيغة Winlator المضغوطة… فتح مجلد الحفظ + استيراد الإضافات من zip… + فتح مجلد الإضافات + حذف %1$s؟ + يؤدي هذا إلى حذف مجلد الإضافة. تبقى إعداداتها. + + %1$d إضافة + %1$d إضافة + %1$d إضافتان + %1$d إضافات + %1$d إضافة + %1$d إضافة + لم يتم تشغيلها أبدًا بيئة التشغيل مشغولة · %1$d%% بيئة التشغيل مشغولة @@ -1393,6 +1405,21 @@ تم استيراد %1$d ملفًا من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups تم استيراد %1$d ملف من %2$s إلى %3$s. تم نسخ ملفات الحفظ القديمة احتياطيًا إلى Download/DroidDeck/Saves/backups + أغلِق الجلسة أولاً حتى لا تستخدم اللعبة إضافاتها أثناء الاستيراد + استيراد الإضافات إلى %1$s + اختر ملف zip للإضافات الخاصة بـ %1$s + %1$s كبير جدًا بحيث لا يمكن استيراده + جارٍ حذف %1$s + تم حذف %1$s + لم يتم العثور على إضافات في %1$s + + تم استيراد %1$d إضافة إلى %2$s + تم استيراد %1$d إضافة إلى %2$s + تم استيراد %1$d إضافتين إلى %2$s + تم استيراد %1$d إضافات إلى %2$s + تم استيراد %1$d إضافة إلى %2$s + تم استيراد %1$d إضافة إلى %2$s + اختر ملف zip لملفات حفظ %1$s تصدير %1$s @@ -3616,6 +3643,7 @@ Steam Cloud للألعاب التي تُحفظ بياناتها في حساب Steam الخاص بك البحث في الويب + الإضافات تثبيت الموقع مجلد 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 4f608ee52..73698d9d5 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -1259,6 +1259,15 @@ Exportar como zip de GameHub… Exportar como zip de Winlator… Abrir carpeta de guardado + Importar addons desde zip… + Abrir carpeta de addons + ¿Eliminar %1$s? + Esto elimina la carpeta del addon. Sus ajustes se conservan. + + %1$d addon + %1$d addons + %1$d de addons + Nunca jugado Runtime ocupado · %1$d%% Runtime ocupado @@ -1438,6 +1447,18 @@ Se importaron %1$d archivos del %2$s en %3$s. Copia de las partidas anteriores en Download/DroidDeck/Saves/backups Se importaron %1$d archivos del %2$s en %3$s. Copia de las partidas anteriores en Download/DroidDeck/Saves/backups + Cierra primero la sesión para que el juego no use sus addons durante la importación + Importando addons en %1$s + Elige un zip de addons para %1$s + %1$s es demasiado grande para importarlo + Eliminando %1$s + Se eliminó %1$s + No se encontraron addons en %1$s + + Se importó %1$d addon en %2$s + Se importaron %1$d addons en %2$s + Se importaron %1$d de addons en %2$s + Elige un zip de partidas guardadas para %1$s Exportando %1$s @@ -3481,6 +3502,7 @@ Steam Cloud Para juegos con partidas guardadas en tu cuenta de Steam Búsqueda web + Addons Instalar Ubicación Carpeta diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 4f5c248d6..678a118f7 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -1183,6 +1183,14 @@ Експортиране като GameHub zip… Експортиране като Winlator zip… Отваряне на папката за записи + Импортиране на добавки от ZIP… + Отваряне на папката с добавки + Да се изтрие ли %1$s? + Това изтрива папката на добавката. Настройките ѝ се запазват. + + %1$d добавка + %1$d добавки + Никога не е играна Средата за изпълнение е заета · %1$d%% Средата за изпълнение е заета @@ -1361,6 +1369,17 @@ Импортиран %1$d файл от %2$s в %3$s. Старите запазвания са архивирани в Download/DroidDeck/Saves/backups Импортирани %1$d файла от %2$s в %3$s. Старите запазвания са архивирани в Download/DroidDeck/Saves/backups + Затвори сесията първо, за да не използва играта добавките си по време на импортирането + Импортиране на добавки в %1$s + Избор на ZIP файл с добавки за %1$s + %1$s е твърде голям за импортиране + Изтриване на %1$s + Изтрито: %1$s + Не са намерени добавки в %1$s + + Импортирана %1$d добавка в %2$s + Импортирани %1$d добавки в %2$s + Избор на ZIP файл със запазвания за %1$s Експортиране на %1$s @@ -3472,6 +3491,7 @@ Steam Cloud За игри със записи в акаунта ви в Steam Търсене в мрежата + Добавки Инсталиране Местоположение Папка diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 954f6e48e..158be592d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -1195,6 +1195,16 @@ Exportovat jako GameHub zip… Exportovat jako Winlator zip… Otevřít složku s uloženými pozicemi + Importovat doplňky ze ZIP… + Otevřít složku s doplňky + Smazat %1$s? + Tím se smaže složka doplňku. Jeho nastavení zůstane zachováno. + + %1$d doplněk + %1$d doplňky + %1$d doplňku + %1$d doplňků + Nikdy nehráno Runtime zaneprázdněn · %1$d%% Runtime zaneprázdněn @@ -1377,6 +1387,19 @@ Importováno %1$d souborů z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups Importováno %1$d souborů z %2$s do %3$s. Staré uložené pozice byly zálohovány do Download/DroidDeck/Saves/backups + Nejprve zavřete relaci, aby hra během importu nepoužívala své doplňky. + Importování doplňků do %1$s + Vyberte ZIP s doplňky pro %1$s + %1$s je pro import příliš velký + Mazání: %1$s + Smazáno: %1$s + V %1$s nebyly nalezeny žádné doplňky + + Importován %1$d doplněk do %2$s + Importovány %1$d doplňky do %2$s + Importováno %1$d doplňku do %2$s + Importováno %1$d doplňků do %2$s + Vyberte ZIP s uloženou pozicí pro %1$s Exportování %1$s @@ -3544,6 +3567,7 @@ Steam Cloud Pro hry s uloženými pozicemi ve vašem účtu Steam Hledat na webu + Doplňky Instalovat Umístění Složka diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 05cf267e7..b673ffe66 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -1183,6 +1183,14 @@ Eksporter som GameHub-zip… Eksporter som Winlator-zip… Åbn savemappe + Importer addons fra zip… + Åbn addon-mappe + Slet %1$s? + Dette fjerner addonens mappe. Dens indstillinger bevares. + + %1$d addon + %1$d addons + Aldrig spillet Runtime optaget · %1$d%% Runtime optaget @@ -1361,6 +1369,17 @@ Importerede %1$d fil fra %2$s til %3$s. Gamle gemte spil sikkerhedskopieret til Download/DroidDeck/Saves/backups Importerede %1$d filer fra %2$s til %3$s. Gamle gemte spil sikkerhedskopieret til Download/DroidDeck/Saves/backups + Luk sessionen først, så spillet ikke bruger sine addons under importen + Importerer addons til %1$s + Vælg en zip med addons til %1$s + %1$s er for stor til at blive importeret + Sletter %1$s + %1$s slettet + Ingen addons fundet i %1$s + + Importerede %1$d addon til %2$s + Importerede %1$d addons til %2$s + Vælg en zip med gemte spil til %1$s Eksporterer %1$s @@ -3472,6 +3491,7 @@ Steam Cloud Til spil med gemte data på din Steam-konto Websøgning + Addons Installer Placering Mappe diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index bf8236f70..6bb4a27a1 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1175,6 +1175,14 @@ Als GameHub-ZIP exportieren… Als Winlator-ZIP exportieren… Spielstandordner öffnen + Addons aus ZIP importieren… + Addon-Ordner öffnen + %1$s löschen? + Dadurch wird der Ordner des Addons entfernt. Die Einstellungen bleiben erhalten. + + %1$d Addon + %1$d Addons + Nie gespielt Laufzeit beschäftigt · %1$d %% Laufzeit beschäftigt @@ -1353,6 +1361,17 @@ %1$d Datei aus %2$s in %3$s importiert. Alte Spielstände gesichert unter Download/DroidDeck/Saves/backups %1$d Dateien aus %2$s in %3$s importiert. Alte Spielstände gesichert unter Download/DroidDeck/Saves/backups + Schließe zuerst die Sitzung, damit das Spiel seine Addons während des Imports nicht verwendet + Addons werden in %1$s importiert + Addon-ZIP für %1$s auswählen + %1$s ist zu groß zum Importieren + %1$s wird gelöscht + %1$s gelöscht + Keine Addons in %1$s gefunden + + %1$d Addon in %2$s importiert + %1$d Addons in %2$s importiert + Spielstand-ZIP für %1$s auswählen Export von %1$s @@ -3472,6 +3491,7 @@ Steam Cloud Für Spiele mit Speicherständen in deinem Steam-Konto Websuche + Addons Installieren Speicherort Ordner diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index ad9263c98..17be0c8bc 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -1183,6 +1183,14 @@ Εξαγωγή ως zip GameHub… Εξαγωγή ως zip Winlator… Άνοιγμα φακέλου αποθήκευσης + Εισαγωγή προσθέτων από zip… + Άνοιγμα φακέλου προσθέτων + Διαγραφή του %1$s; + Ο φάκελος του πρόσθετου θα διαγραφεί. Οι ρυθμίσεις του παραμένουν. + + %1$d πρόσθετο + %1$d πρόσθετα + Δεν έχει παιχτεί ποτέ Το runtime είναι απασχολημένο · %1$d%% Το runtime είναι απασχολημένο @@ -1361,6 +1369,17 @@ Έγινε εισαγωγή %1$d αρχείου από το %2$s στο %3$s. Τα παλιά αρχεία αποθήκευσης κρατήθηκαν ως αντίγραφο ασφαλείας στο Download/DroidDeck/Saves/backups Έγινε εισαγωγή %1$d αρχείων από το %2$s στο %3$s. Τα παλιά αρχεία αποθήκευσης κρατήθηκαν ως αντίγραφο ασφαλείας στο Download/DroidDeck/Saves/backups + Κλείστε πρώτα τη συνεδρία, ώστε το παιχνίδι να μη χρησιμοποιεί τα πρόσθετά του κατά την εισαγωγή + Εισαγωγή προσθέτων στο %1$s + Επιλέξτε ένα zip προσθέτων για το %1$s + Το %1$s είναι πολύ μεγάλο για εισαγωγή + Γίνεται διαγραφή του %1$s + Το %1$s διαγράφηκε + Δεν βρέθηκαν πρόσθετα στο %1$s + + Έγινε εισαγωγή %1$d προσθέτου στο %2$s + Έγινε εισαγωγή %1$d προσθέτων στο %2$s + Επιλέξτε ένα zip αποθηκευμένων αρχείων για το %1$s Εξαγωγή %1$s @@ -3472,6 +3491,7 @@ Steam Cloud Για παιχνίδια με αποθηκεύσεις στον λογαριασμό σας Steam Αναζήτηση στον ιστό + Πρόσθετα Εγκατάσταση Τοποθεσία Φάκελος diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f7cc0570d..b9ad21bbe 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1189,6 +1189,15 @@ Exportar como zip de GameHub… Exportar como zip de Winlator… Abrir carpeta de guardado + Importar addons desde zip… + Abrir carpeta de addons + ¿Eliminar %1$s? + Esto elimina la carpeta del addon. Sus ajustes se conservan. + + %1$d addon + %1$d de addons + %1$d addons + Nunca jugado Runtime ocupado · %1$d%% Runtime ocupado @@ -1369,6 +1378,18 @@ Se importaron %1$d archivos del %2$s en %3$s. Copia de las partidas anteriores en Download/DroidDeck/Saves/backups Se importaron %1$d archivos del %2$s en %3$s. Copia de las partidas anteriores en Download/DroidDeck/Saves/backups + Cierra primero la sesión para que el juego no use sus addons durante la importación + Importando addons en %1$s + Elige un zip de addons para %1$s + %1$s es demasiado grande para importarlo + Eliminando %1$s + Se eliminó %1$s + No se encontraron addons en %1$s + + Se importó %1$d addon en %2$s + Se importaron %1$d de addons en %2$s + Se importaron %1$d addons en %2$s + Elige un zip de partidas guardadas para %1$s Exportando %1$s @@ -3508,6 +3529,7 @@ Steam Cloud Para juegos con partidas guardadas en tu cuenta de Steam Búsqueda web + Addons Instalar Ubicación Carpeta diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 921ab377c..06dd2d1a1 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -1183,6 +1183,14 @@ Vie GameHub-zipinä… Vie Winlator-zipinä… Avaa tallennuskansio + Tuo lisäosat zip-tiedostosta… + Avaa lisäosakansio + Poistetaanko %1$s? + Tämä poistaa lisäosan kansion. Sen asetukset säilyvät. + + %1$d lisäosa + %1$d lisäosaa + Ei pelattu koskaan Ajoympäristö varattu · %1$d %% Ajoympäristö varattu @@ -1361,6 +1369,17 @@ Tuotiin %1$d tiedosto %2$s-muodosta kohteeseen %3$s. Vanhat tallennukset varmuuskopioitu kansioon Download/DroidDeck/Saves/backups Tuotiin %1$d tiedostoa %2$s-muodosta kohteeseen %3$s. Vanhat tallennukset varmuuskopioitu kansioon Download/DroidDeck/Saves/backups + Sulje istunto ensin, jotta peli ei käytä lisäosiaan tuonnin aikana + Tuodaan lisäosia kohteeseen %1$s + Valitse lisäosien zip-tiedosto kohteelle %1$s + %1$s on liian suuri tuotavaksi + Poistetaan %1$s + %1$s poistettu + Lisäosia ei löytynyt kohteesta %1$s + + Tuotiin %1$d lisäosa kohteeseen %2$s + Tuotiin %1$d lisäosaa kohteeseen %2$s + Valitse tallennusten zip-tiedosto kohteelle %1$s Viedään %1$s @@ -3472,6 +3491,7 @@ Steam Cloud Peleille, joiden tallennukset ovat Steam-tililläsi Verkkohaku + Lisäosat Asenna Sijainti Kansio diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3c3a65ebf..7a2200fa9 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1189,6 +1189,15 @@ Exporter en tant qu\'archive GameHub… Exporter en tant qu\'archive Winlator… Ouvrir le dossier de sauvegarde + Importer des addons depuis un zip… + Ouvrir le dossier des addons + Supprimer %1$s ? + Cela supprime le dossier de l\'addon. Ses paramètres sont conservés. + + %1$d addon + %1$d d\'addons + %1$d addons + Jamais lancé Environnement d\'exécution occupé · %1$d%% Environnement d\'exécution occupé @@ -1369,6 +1378,18 @@ %1$d fichiers importés (%2$s) dans %3$s. Anciennes sauvegardes copiées dans Download/DroidDeck/Saves/backups %1$d fichiers importés (%2$s) dans %3$s. Anciennes sauvegardes copiées dans Download/DroidDeck/Saves/backups + Fermez d\'abord la session, pour que le jeu n\'utilise pas ses addons pendant l\'importation + Importation des addons dans %1$s + Choisir un zip d\'addons pour %1$s + %1$s est trop volumineux pour être importé + Suppression de %1$s + %1$s supprimé + Aucun addon trouvé dans %1$s + + %1$d addon importé dans %2$s + %1$d d\'addons importés dans %2$s + %1$d addons importés dans %2$s + Choisir un zip de sauvegarde pour %1$s Exportation de %1$s @@ -3508,6 +3529,7 @@ Steam Cloud Pour les jeux dont les sauvegardes sont sur votre compte Steam Recherche web + Addons Installer Emplacement Dossier diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 68fe01045..0005d2dfa 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1183,6 +1183,14 @@ GameHub zip के रूप में एक्सपोर्ट करें… Winlator zip के रूप में एक्सपोर्ट करें… सेव फ़ोल्डर खोलें + zip से ऐडऑन इंपोर्ट करें… + ऐडऑन फ़ोल्डर खोलें + %1$s हटाएं? + यह ऐडऑन का फ़ोल्डर हटा देता है। इसकी सेटिंग्स बनी रहती हैं। + + %1$d ऐडऑन + %1$d ऐडऑन + कभी नहीं खेला रनटाइम व्यस्त · %1$d%% रनटाइम व्यस्त @@ -1361,6 +1369,17 @@ %2$s से %1$d फ़ाइल %3$s में इंपोर्ट हुई। पुराने सेव Download/DroidDeck/Saves/backups में बैकअप हो गए %2$s से %1$d फ़ाइलें %3$s में इंपोर्ट हुईं। पुराने सेव Download/DroidDeck/Saves/backups में बैकअप हो गए + पहले सेशन बंद करें, ताकि इंपोर्ट के दौरान गेम अपने ऐडऑन का उपयोग न करे + %1$s में ऐडऑन इंपोर्ट हो रहे हैं + %1$s के लिए ऐडऑन zip चुनें + %1$s इंपोर्ट करने के लिए बहुत बड़ा है + %1$s हटाया जा रहा है + %1$s हटाया गया + %1$s में कोई ऐडऑन नहीं मिला + + %1$d ऐडऑन %2$s में इंपोर्ट हुआ + %1$d ऐडऑन %2$s में इंपोर्ट हुए + %1$s के लिए एक सेव zip चुनें %1$s एक्सपोर्ट हो रहा है @@ -3472,6 +3491,7 @@ Steam Cloud उन गेम के लिए जिनके सेव आपके Steam अकाउंट पर हैं वेब सर्च + ऐडऑन इंस्टॉल करें जगह फ़ोल्डर diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index cc32a257a..86fe25afa 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1183,6 +1183,14 @@ Exportálás GameHub zipként… Exportálás Winlator zipként… Mentésmappa megnyitása + Kiegészítők importálása zipből… + Kiegészítők mappájának megnyitása + Törlöd ezt: %1$s? + Ez törli a kiegészítő mappáját. A beállításai megmaradnak. + + %1$d kiegészítő + %1$d kiegészítő + Még nem játszott Futtatókörnyezet dolgozik · %1$d%% Futtatókörnyezet dolgozik @@ -1361,6 +1369,17 @@ %1$d fájl importálva innen: %2$s, ide: %3$s. A régi mentések mentve itt: Download/DroidDeck/Saves/backups %1$d fájl importálva innen: %2$s, ide: %3$s. A régi mentések mentve itt: Download/DroidDeck/Saves/backups + Előbb zárja be a munkamenetet, hogy a játék ne használja a kiegészítőit az importálás alatt + Kiegészítők importálása ide: %1$s + Válasszon kiegészítő zip fájlt ehhez: %1$s + A(z) %1$s túl nagy az importáláshoz + %1$s törlése folyamatban + %1$s törölve + Nem található kiegészítő itt: %1$s + + %1$d kiegészítő importálva ide: %2$s + %1$d kiegészítő importálva ide: %2$s + Válasszon mentés zip fájlt ehhez: %1$s %1$s exportálása @@ -3472,6 +3491,7 @@ Steam Cloud A Steam-fiókjában mentéseket tároló játékokhoz Webes keresés + Kiegészítők Telepítés Hely Mappa diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 1ef496df4..0aa46114f 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1177,6 +1177,13 @@ Ekspor sebagai zip GameHub… Ekspor sebagai zip Winlator… Buka folder simpanan + Impor addon dari zip… + Buka folder addon + Hapus %1$s? + Ini menghapus folder addon. Pengaturannya tetap ada. + + %1$d addon + Belum pernah dimainkan Runtime sibuk · %1$d%% Runtime sibuk @@ -1353,6 +1360,16 @@ %1$d file diimpor dari %2$s ke %3$s. Save lama dicadangkan ke Download/DroidDeck/Saves/backups + Tutup sesi dulu, agar game tidak memakai addon-nya saat impor + Mengimpor addon ke %1$s + Pilih zip addon untuk %1$s + %1$s terlalu besar untuk diimpor + Menghapus %1$s + %1$s dihapus + Tidak ada addon di %1$s + + %1$d addon diimpor ke %2$s + Pilih zip save untuk %1$s Mengekspor %1$s @@ -3445,6 +3462,7 @@ Steam Cloud Untuk gim dengan save di akun Steam Anda Penelusuran web + Addon Pasang Lokasi Folder diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index a1419b704..4f816b30b 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -1181,6 +1181,15 @@ Esporta come zip GameHub… Esporta come zip Winlator… Apri cartella di salvataggio + Importa addon da zip… + Apri cartella degli addon + Eliminare %1$s? + Questo rimuove la cartella dell\'addon. Le sue impostazioni restano. + + %1$d addon + %1$d di addon + %1$d addon + Mai giocato Runtime occupato · %1$d%% Runtime occupato @@ -1361,6 +1370,18 @@ %1$d file importati (%2$s) in %3$s. Backup dei vecchi salvataggi in Download/DroidDeck/Saves/backups %1$d file importati (%2$s) in %3$s. Backup dei vecchi salvataggi in Download/DroidDeck/Saves/backups + Chiudi prima la sessione, così il gioco non usa i suoi addon durante l\'importazione + Importazione degli addon in %1$s + Scegli uno zip di addon per %1$s + %1$s è troppo grande per essere importato + Eliminazione di %1$s + %1$s eliminato + Nessun addon trovato in %1$s + + %1$d addon importato in %2$s + %1$d di addon importati in %2$s + %1$d addon importati in %2$s + Scegli uno zip di salvataggi per %1$s Esportazione di %1$s @@ -3508,6 +3529,7 @@ Steam Cloud Per i giochi con salvataggi nel tuo account Steam Ricerca web + Addon Installa Posizione Cartella diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index b5a2fc424..f72c30c8b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1177,6 +1177,13 @@ GameHubアーカイブとしてエクスポート… Winlatorアーカイブとしてエクスポート… セーブフォルダを開く + zipからアドオンをインポート… + アドオンフォルダを開く + %1$sを削除しますか? + アドオンのフォルダが削除されます。設定は残ります。 + + %1$d個のアドオン + 未プレイ 実行環境は使用中 · %1$d%% 実行環境は使用中 @@ -1353,6 +1360,16 @@ %2$sから%1$d個のファイルを%3$sにインポートしました。以前のセーブデータはDownload/DroidDeck/Saves/backupsにバックアップされました + インポート中にゲームがアドオンを使用しないよう、先にセッションを終了してください + %1$sにアドオンをインポート中 + %1$sのアドオンzipを選択 + %1$s は大きすぎるためインポートできません + %1$sを削除中 + %1$sを削除しました + %1$sにアドオンが見つかりません + + %1$d個のアドオンを%2$sにインポートしました + %1$sのセーブデータzipを選択 %1$sをエクスポート中 @@ -3445,6 +3462,7 @@ Steam Cloud Steamアカウントにセーブデータがあるゲーム向け ウェブ検索 + アドオン インストール 場所 フォルダ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4ede4632a..16d3a1e5c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1177,6 +1177,13 @@ GameHub 아카이브로 내보내기… Winlator 아카이브로 내보내기… 저장 폴더 열기 + zip에서 애드온 가져오기… + 애드온 폴더 열기 + %1$s을(를) 삭제할까요? + 애드온 폴더가 삭제됩니다. 설정은 유지됩니다. + + 애드온 %1$d개 + 플레이한 적 없음 런타임 사용 중 · %1$d%% 런타임 사용 중 @@ -1353,6 +1360,16 @@ %2$s에서 %3$s(으)로 파일 %1$d개를 가져왔습니다. 이전 저장 데이터는 Download/DroidDeck/Saves/backups에 백업되었습니다 + 가져오는 동안 게임이 애드온을 사용하지 않도록 먼저 세션을 종료하세요 + %1$s(으)로 애드온 가져오는 중 + %1$s의 애드온 zip 선택 + %1$s은(는) 너무 커서 가져올 수 없습니다 + %1$s 삭제 중 + %1$s 삭제됨 + %1$s에서 애드온을 찾을 수 없습니다 + + 애드온 %1$d개를 %2$s(으)로 가져왔습니다 + %1$s의 저장 데이터 zip 선택 %1$s 내보내는 중 @@ -3445,6 +3462,7 @@ Steam Cloud Steam 계정에 저장 파일이 있는 게임용 웹 검색 + 애드온 설치 위치 폴더 diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index 4dcdb4e3f..14dc814a7 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -1177,6 +1177,13 @@ Eksport sebagai zip GameHub… Eksport sebagai zip Winlator… Buka folder simpanan + Import addon daripada zip… + Buka folder addon + Padam %1$s? + Ini memadam folder addon. Tetapannya kekal. + + %1$d addon + Belum pernah dimainkan Masa jalan sibuk · %1$d%% Masa jalan sibuk @@ -1353,6 +1360,16 @@ Mengimport %1$d fail daripada %2$s ke %3$s. Simpanan lama disandarkan ke Download/DroidDeck/Saves/backups + Tutup sesi dahulu, supaya permainan tidak menggunakan addonnya semasa import + Mengimport addon ke %1$s + Pilih zip addon untuk %1$s + %1$s terlalu besar untuk diimport + Memadam %1$s + %1$s dipadam + Tiada addon ditemui dalam %1$s + + Berjaya mengimport %1$d addon ke %2$s + Pilih zip simpanan untuk %1$s Mengeksport %1$s @@ -3445,6 +3462,7 @@ Steam Cloud Untuk permainan dengan simpanan pada akaun Steam anda Carian web + Addon Pasang Lokasi Folder diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index a2b8f0ed8..7a644c67e 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -1183,6 +1183,14 @@ Eksporter som GameHub-zip… Eksporter som Winlator-zip… Åpne lagringsmappe + Importer tillegg fra zip… + Åpne tilleggsmappe + Slett %1$s? + Dette fjerner tilleggets mappe. Innstillingene beholdes. + + %1$d tillegg + %1$d tillegg + Aldri spilt Kjøretid opptatt · %1$d%% Kjøretid opptatt @@ -1361,6 +1369,17 @@ Importerte %1$d fil fra %2$s til %3$s. Gamle lagringer sikkerhetskopiert til Download/DroidDeck/Saves/backups Importerte %1$d filer fra %2$s til %3$s. Gamle lagringer sikkerhetskopiert til Download/DroidDeck/Saves/backups + Lukk sesjonen først, så spillet ikke bruker tilleggene under importen + Importerer tillegg til %1$s + Velg en zip med tillegg for %1$s + %1$s er for stor til å importeres + Sletter %1$s + Slettet %1$s + Fant ingen tillegg i %1$s + + Importerte %1$d tillegg til %2$s + Importerte %1$d tillegg til %2$s + Velg en lagrings-zip for %1$s Eksporterer %1$s @@ -3472,6 +3491,7 @@ Steam Cloud For spill med lagringer på Steam-kontoen din Nettsøk + Tillegg Installer Plassering Mappe diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index aacc9dd20..26caeb73c 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1183,6 +1183,14 @@ Exporteren als GameHub-zip… Exporteren als Winlator-zip… Opslagmap openen + Addons importeren uit zip… + Addonmap openen + %1$s verwijderen? + Hiermee wordt de map van de addon verwijderd. De instellingen blijven behouden. + + %1$d addon + %1$d addons + Nooit gespeeld Runtime bezig · %1$d%% Runtime bezig @@ -1361,6 +1369,17 @@ %1$d bestand uit de %2$s geïmporteerd in %3$s. Oude savegames als back-up bewaard in Download/DroidDeck/Saves/backups %1$d bestanden uit de %2$s geïmporteerd in %3$s. Oude savegames als back-up bewaard in Download/DroidDeck/Saves/backups + Sluit eerst de sessie, zodat de game tijdens de import geen addons gebruikt + Addons importeren in %1$s + Kies een addon-zip voor %1$s + %1$s is te groot om te importeren + %1$s wordt verwijderd + %1$s verwijderd + Geen addons gevonden in %1$s + + %1$d addon geïmporteerd in %2$s + %1$d addons geïmporteerd in %2$s + Kies een savegame-zip voor %1$s %1$s exporteren @@ -3472,6 +3491,7 @@ Steam Cloud Voor games met opslagbestanden in je Steam-account Zoeken op het web + Addons Installeren Locatie Map diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index bf3d93d8b..2304171da 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -1195,6 +1195,16 @@ Eksportuj jako zip GameHub… Eksportuj jako zip Winlator… Otwórz folder zapisów + Importuj dodatki z zip… + Otwórz folder dodatków + Usunąć %1$s? + Spowoduje to usunięcie folderu dodatku. Jego ustawienia zostaną zachowane. + + %1$d dodatek + %1$d dodatki + %1$d dodatków + %1$d dodatku + Nigdy nie grano Środowisko zajęte · %1$d%% Środowisko zajęte @@ -1377,6 +1387,19 @@ Zaimportowano %1$d plików z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups Zaimportowano %1$d pliku z %2$s do %3$s. Kopię starych zapisów umieszczono w Download/DroidDeck/Saves/backups + Najpierw zamknij sesję, aby gra nie używała dodatków podczas importu + Importowanie dodatków do %1$s + Wybierz plik zip z dodatkami dla %1$s + Plik %1$s jest za duży, aby go zaimportować + Usuwanie: %1$s + Usunięto: %1$s + Nie znaleziono dodatków w %1$s + + Zaimportowano %1$d dodatek do %2$s + Zaimportowano %1$d dodatki do %2$s + Zaimportowano %1$d dodatków do %2$s + Zaimportowano %1$d dodatku do %2$s + Wybierz plik zip z zapisami dla %1$s Eksportowanie %1$s @@ -3544,6 +3567,7 @@ Steam Cloud Dla gier z zapisami na Twoim koncie Steam Wyszukiwanie w sieci + Dodatki Zainstaluj Lokalizacja Folder diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index ca404e28f..43cb70df8 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -1181,6 +1181,15 @@ Exportar como zip do GameHub… Exportar como zip do Winlator… Abrir pasta de gravações + Importar addons de zip… + Abrir pasta de addons + Eliminar %1$s? + Isto elimina a pasta do addon. As definições dele mantêm-se. + + %1$d addon + %1$d de addons + %1$d addons + Nunca jogado Ambiente de execução ocupado · %1$d%% Ambiente de execução ocupado @@ -1361,6 +1370,18 @@ %1$d ficheiros importados do %2$s para %3$s. Cópia de segurança dos dados guardados antigos em Download/DroidDeck/Saves/backups %1$d ficheiros importados do %2$s para %3$s. Cópia de segurança dos dados guardados antigos em Download/DroidDeck/Saves/backups + Feche primeiro a sessão, para que o jogo não use os addons durante a importação + A importar addons para %1$s + Escolha um zip de addons para %1$s + %1$s é demasiado grande para importar + A eliminar %1$s + %1$s eliminado + Nenhum addon encontrado em %1$s + + %1$d addon importado para %2$s + %1$d de addons importados para %2$s + %1$d addons importados para %2$s + Escolha um zip de dados guardados para %1$s A exportar %1$s @@ -3508,6 +3529,7 @@ Steam Cloud Para jogos com guardados na sua conta Steam Pesquisa na Web + Addons Instalar Localização Pasta diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 5376a6f73..ba5bbf147 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1181,6 +1181,15 @@ Exportar como zip do GameHub… Exportar como zip do Winlator… Abrir pasta de jogos salvos + Importar addons de zip… + Abrir pasta de addons + Excluir %1$s? + Isso remove a pasta do addon. As configurações dele permanecem. + + %1$d addon + %1$d de addons + %1$d addons + Nunca jogado Runtime ocupado · %1$d%% Runtime ocupado @@ -1361,6 +1370,18 @@ %1$d arquivos importados de %2$s para %3$s. Backup dos saves antigos em Download/DroidDeck/Saves/backups %1$d arquivos importados de %2$s para %3$s. Backup dos saves antigos em Download/DroidDeck/Saves/backups + Feche a sessão primeiro, para que o jogo não use os addons durante a importação + Importando addons para %1$s + Escolha um zip de addons para %1$s + %1$s é grande demais para importar + Excluindo %1$s + %1$s excluído + Nenhum addon encontrado em %1$s + + %1$d addon importado para %2$s + %1$d de addons importados para %2$s + %1$d addons importados para %2$s + Escolha um zip de saves para %1$s Exportando %1$s @@ -3508,6 +3529,7 @@ Steam Cloud Para jogos com salvamentos na sua conta Steam Pesquisa na web + Addons Instalar Local Pasta diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 0104424cd..dd0580d81 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -1189,6 +1189,15 @@ Exportă ca arhivă GameHub… Exportă ca arhivă Winlator… Deschide folderul de salvare + Importă suplimente din zip… + Deschide folderul de suplimente + Ștergi %1$s? + Aceasta șterge folderul suplimentului. Setările sale rămân. + + %1$d supliment + %1$d suplimente + %1$d de suplimente + Niciodată jucat Mediu de execuție ocupat · %1$d%% Mediu de execuție ocupat @@ -1369,6 +1378,18 @@ S-au importat %1$d fișiere din %2$s în %3$s. Salvările vechi au fost copiate de rezervă în Download/DroidDeck/Saves/backups S-au importat %1$d de fișiere din %2$s în %3$s. Salvările vechi au fost copiate de rezervă în Download/DroidDeck/Saves/backups + Închide mai întâi sesiunea, ca jocul să nu folosească suplimentele în timpul importului + Se importă suplimente în %1$s + Alege o arhivă zip de suplimente pentru %1$s + %1$s este prea mare pentru a fi importat + Se șterge %1$s + S-a șters %1$s + Nu s-au găsit suplimente în %1$s + + S-a importat %1$d supliment în %2$s + S-au importat %1$d suplimente în %2$s + S-au importat %1$d de suplimente în %2$s + Alege o arhivă de salvare pentru %1$s Se exportă %1$s @@ -3508,6 +3529,7 @@ Steam Cloud Pentru jocurile cu salvări în contul tău Steam Căutare pe web + Suplimente Instalează Locație Folder diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 38d47aa15..55287bdd9 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1195,6 +1195,16 @@ Экспорт как архив GameHub… Экспорт как архив Winlator… Открыть папку сохранений + Импорт аддонов из zip… + Открыть папку аддонов + Удалить %1$s? + Папка аддона будет удалена. Его настройки сохранятся. + + %1$d аддон + %1$d аддона + %1$d аддонов + %1$d аддона + Никогда не запускалась Среда выполнения занята · %1$d%% Среда выполнения занята @@ -1377,6 +1387,19 @@ Импортировано %1$d файлов (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups Импортировано %1$d файла (%2$s) в %3$s. Старые сохранения скопированы в Download/DroidDeck/Saves/backups + Сначала закройте сессию, чтобы игра не использовала аддоны во время импорта + Импорт аддонов в %1$s + Выберите zip-архив аддонов для %1$s + %1$s слишком большой для импорта + Удаление: %1$s + Удалено: %1$s + Аддоны не найдены в %1$s + + Импортирован %1$d аддон в %2$s + Импортировано %1$d аддона в %2$s + Импортировано %1$d аддонов в %2$s + Импортировано %1$d аддона в %2$s + Выберите zip-архив сохранений для %1$s Экспорт %1$s @@ -3544,6 +3567,7 @@ Steam Cloud Для игр с сохранениями в аккаунте Steam Поиск в интернете + Аддоны Установить Расположение Папка diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f94dc7d29..6de4336e3 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -1183,6 +1183,14 @@ Exportera som GameHub-zip… Exportera som Winlator-zip… Öppna sparmappen + Importera tillägg från zip… + Öppna tilläggsmappen + Ta bort %1$s? + Detta tar bort tilläggets mapp. Inställningarna behålls. + + %1$d tillägg + %1$d tillägg + Aldrig spelat Körmiljö upptagen · %1$d %% Körmiljö upptagen @@ -1361,6 +1369,17 @@ Importerade %1$d fil från %2$s till %3$s. Gamla sparfiler säkerhetskopierades till Download/DroidDeck/Saves/backups Importerade %1$d filer från %2$s till %3$s. Gamla sparfiler säkerhetskopierades till Download/DroidDeck/Saves/backups + Stäng sessionen först, så att spelet inte använder sina tillägg under importen + Importerar tillägg till %1$s + Välj en zip-fil med tillägg för %1$s + %1$s är för stor för att importeras + Tar bort %1$s + Tog bort %1$s + Inga tillägg hittades i %1$s + + Importerade %1$d tillägg till %2$s + Importerade %1$d tillägg till %2$s + Välj en zip-fil med sparfiler för %1$s Exporterar %1$s @@ -3472,6 +3491,7 @@ Steam Cloud För spel med sparningar på ditt Steam-konto Webbsökning + Tillägg Installera Plats Mapp diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index faaf350e2..538a3817d 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -1177,6 +1177,13 @@ ส่งออกเป็น GameHub zip… ส่งออกเป็น Winlator zip… เปิดโฟลเดอร์เซฟ + นำเข้าแอดออนจาก zip… + เปิดโฟลเดอร์แอดออน + ลบ %1$s ไหม + การดำเนินการนี้จะลบโฟลเดอร์ของแอดออน ส่วนการตั้งค่ายังคงอยู่ + + %1$d แอดออน + ยังไม่เคยเล่น รันไทม์กำลังทำงาน · %1$d%% รันไทม์กำลังทำงาน @@ -1353,6 +1360,16 @@ นำเข้า %1$d ไฟล์จาก %2$s สู่ %3$s แล้ว เซฟเก่าสำรองไว้ที่ Download/DroidDeck/Saves/backups + ปิดเซสชันก่อน เพื่อไม่ให้เกมใช้แอดออนระหว่างการนำเข้า + กำลังนำเข้าแอดออนสู่ %1$s + เลือกไฟล์ zip แอดออนสำหรับ %1$s + %1$s มีขนาดใหญ่เกินกว่าจะนำเข้าได้ + กำลังลบ %1$s + ลบ %1$s แล้ว + ไม่พบแอดออนใน %1$s + + นำเข้า %1$d แอดออนสู่ %2$s แล้ว + เลือกไฟล์ zip เซฟสำหรับ %1$s กำลังส่งออก %1$s @@ -3445,6 +3462,7 @@ Steam Cloud สำหรับเกมที่มีเซฟอยู่ในบัญชี Steam ของคุณ ค้นหาเว็บ + แอดออน ติดตั้ง ตำแหน่ง โฟลเดอร์ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1b5613944..8a864ce99 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1175,6 +1175,14 @@ GameHub zip\'i olarak dışa aktar… Winlator zip\'i olarak dışa aktar… Kayıt klasörünü aç + Eklentileri zip\'ten içe aktar… + Eklentiler klasörünü aç + %1$s silinsin mi? + Bu işlem eklentinin klasörünü kaldırır. Ayarları korunur. + + %1$d eklenti + %1$d eklenti + Hiç oynanmadı Çalışma ortamı meşgul · %1$d%% Çalışma ortamı meşgul @@ -1353,6 +1361,17 @@ %2$s içinden %1$d dosya %3$s oyununa aktarıldı. Eski kayıtlar Download/DroidDeck/Saves/backups klasörüne yedeklendi %2$s içinden %1$d dosya %3$s oyununa aktarıldı. Eski kayıtlar Download/DroidDeck/Saves/backups klasörüne yedeklendi + Önce oturumu kapatın; böylece oyun içe aktarma sırasında eklentilerini kullanmaz + Eklentiler içe aktarılıyor: %1$s + %1$s için bir eklenti zip dosyası seçin + %1$s içe aktarılamayacak kadar büyük + %1$s siliniyor + %1$s silindi + %1$s içinde eklenti bulunamadı + + %1$d eklenti %2$s oyununa aktarıldı + %1$d eklenti %2$s oyununa aktarıldı + %1$s için bir kayıt zip dosyası seçin Dışa aktarılıyor: %1$s @@ -3472,6 +3491,7 @@ Steam Cloud Kayıtları Steam hesabınızda olan oyunlar için Web\'de ara + Eklentiler Yükle Konum Klasör diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d80343472..e317a43f6 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1195,6 +1195,16 @@ Експортувати як zip GameHub… Експортувати як zip Winlator… Відкрити папку збережень + Імпортувати аддони з zip… + Відкрити папку аддонів + Видалити %1$s? + Папку аддона буде видалено. Його налаштування збережуться. + + %1$d аддон + %1$d аддони + %1$d аддонів + %1$d аддона + Ще не грали Середовище зайняте · %1$d%% Середовище зайняте @@ -1377,6 +1387,19 @@ Імпортовано %1$d файлів з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups Імпортовано %1$d файлу з %2$s у %3$s. Резервну копію старих збережень записано в Download/DroidDeck/Saves/backups + Спершу закрийте сеанс, щоб гра не використовувала аддони під час імпорту + Імпорт аддонів у %1$s + Виберіть zip-архів аддонів для %1$s + %1$s завеликий для імпорту + Видалення: %1$s + Видалено: %1$s + Аддони не знайдено в %1$s + + Імпортовано %1$d аддон у %2$s + Імпортовано %1$d аддони в %2$s + Імпортовано %1$d аддонів у %2$s + Імпортовано %1$d аддона в %2$s + Виберіть zip-архів збережень для %1$s Експорт %1$s @@ -3544,6 +3567,7 @@ Steam Cloud Для ігор зі збереженнями в обліковому записі Steam Пошук в інтернеті + Аддони Установити Розташування Папка diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 6a2d94d83..7af05a293 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1177,6 +1177,13 @@ Xuất dưới dạng zip GameHub… Xuất dưới dạng zip Winlator… Mở thư mục lưu + Nhập addon từ zip… + Mở thư mục addon + Xóa %1$s? + Thao tác này xóa thư mục của addon. Cài đặt của addon vẫn được giữ lại. + + %1$d addon + Chưa chơi bao giờ Runtime đang bận · %1$d%% Runtime đang bận @@ -1353,6 +1360,16 @@ Đã nhập %1$d tệp từ %2$s vào %3$s. Tệp lưu cũ đã được sao lưu vào Download/DroidDeck/Saves/backups + Hãy đóng phiên trước, để game không dùng addon trong lúc nhập + Đang nhập addon vào %1$s + Chọn một tệp zip addon cho %1$s + %1$s quá lớn để nhập + Đang xóa %1$s + Đã xóa %1$s + Không tìm thấy addon nào trong %1$s + + Đã nhập %1$d addon vào %2$s + Chọn một tệp zip lưu cho %1$s Đang xuất %1$s @@ -3445,6 +3462,7 @@ Steam Cloud Cho game có bản lưu trong tài khoản Steam của bạn Tìm kiếm trên web + Addon Cài đặt Vị trí Thư mục diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4d23bbf24..34d9c5023 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1261,6 +1261,13 @@ 导出为 GameHub zip 包… 导出为 Winlator zip 包… 打开存档文件夹 + 从 zip 导入插件… + 打开插件文件夹 + 删除 %1$s? + 这会移除插件的文件夹,其设置会保留。 + + %1$d 个插件 + 从未游玩 运行时繁忙 · %1$d%% 运行时繁忙 @@ -1437,6 +1444,16 @@ 已从 %2$s 导入 %1$d 个文件到 %3$s。旧存档已备份到 Download/DroidDeck/Saves/backups + 请先关闭会话,以免游戏在导入期间使用其插件 + 正在导入插件到 %1$s + 为 %1$s 选择插件 zip 包 + %1$s 太大,无法导入 + 正在删除 %1$s + 已删除 %1$s + 在 %1$s 中未找到插件 + + 已导入 %1$d 个插件到 %2$s + 为 %1$s 选择存档 zip 包 正在导出 %1$s @@ -3448,6 +3465,7 @@ Steam Cloud 适用于存档保存在 Steam 账户中的游戏 网页搜索 + 插件 安装 位置 文件夹 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index d1024c1c5..6df9a2425 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1177,6 +1177,13 @@ 匯出為 GameHub zip… 匯出為 Winlator zip… 開啟存檔資料夾 + 從 zip 匯入外掛… + 開啟外掛資料夾 + 刪除 %1$s? + 這會移除外掛的資料夾,其設定會保留。 + + %1$d 個外掛 + 從未玩過 運行環境繁忙 · %1$d%% 運行環境繁忙 @@ -1353,6 +1360,16 @@ 已從 %2$s 匯入 %1$d 個檔案至 %3$s。舊存檔已備份至 Download/DroidDeck/Saves/backups + 請先關閉會話,以免遊戲在匯入期間使用外掛 + 正在匯入外掛至 %1$s + 選擇 %1$s 的外掛 zip + %1$s 太大,無法匯入 + 正在刪除 %1$s + 已刪除 %1$s + 在 %1$s 中找不到外掛 + + 已匯入 %1$d 個外掛至 %2$s + 為 %1$s 選擇存檔 zip 正在匯出 %1$s @@ -3445,6 +3462,7 @@ Steam Cloud 適用於存檔儲存在 Steam 帳戶中的遊戲 網上搜尋 + 外掛 安裝 位置 資料夾 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 3065f3a45..9e32ee2b5 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1177,6 +1177,13 @@ 匯出為 GameHub 壓縮檔… 匯出為 Winlator 壓縮檔… 開啟存檔資料夾 + 從 zip 匯入外掛… + 開啟外掛資料夾 + 刪除 %1$s? + 這會移除外掛的資料夾,其設定會保留。 + + %1$d 個外掛 + 從未執行 執行環境繁忙 · %1$d%% 執行環境繁忙 @@ -1353,6 +1360,16 @@ 已從 %2$s 匯入 %1$d 個檔案到 %3$s。舊存檔已備份到 Download/DroidDeck/Saves/backups + 請先關閉工作階段,以免遊戲在匯入期間使用外掛 + 正在匯入外掛到 %1$s + 選擇 %1$s 的外掛 zip + %1$s 太大,無法匯入 + 正在刪除 %1$s + 已刪除 %1$s + 在 %1$s 中找不到外掛 + + 已匯入 %1$d 個外掛到 %2$s + 選擇 %1$s 的存檔壓縮檔 正在匯出 %1$s @@ -3445,6 +3462,7 @@ Steam Cloud 適用於存檔儲存在 Steam 帳戶中的遊戲 網路搜尋 + 外掛 安裝 位置 資料夾 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 74312a921..5ea9cf34c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1317,6 +1317,14 @@ Export as GameHub zip… Export as Winlator zip… Open save folder + Import addons from zip… + Open addons folder + Delete %1$s? + This removes the addon\'s folder. Its settings stay. + + %1$d addon + %1$d addons + Never played Runtime busy · %1$d%% Runtime busy @@ -1504,6 +1512,17 @@ Imported %1$d file from the %2$s into %3$s. Old saves backed up to Download/DroidDeck/Saves/backups Imported %1$d files from the %2$s into %3$s. Old saves backed up to Download/DroidDeck/Saves/backups + Close the session first, so the game isn\'t using its addons during the import + Importing addons into %1$s + Choose an addon zip for %1$s + %1$s is too large to import + Deleting %1$s + Deleted %1$s + No addons found in %1$s + + Imported %1$d addon into %2$s + Imported %1$d addons into %2$s + Choose a save zip for %1$s Exporting %1$s @@ -3669,6 +3688,7 @@ Web search %1$s %2$d/%3$d Launch + Addons Saves & cloud Install Steam shortcut diff --git a/app/src/test/java/com/droiddeck/launcher/runtime/WowAddonsTest.kt b/app/src/test/java/com/droiddeck/launcher/runtime/WowAddonsTest.kt new file mode 100644 index 000000000..8350ef519 --- /dev/null +++ b/app/src/test/java/com/droiddeck/launcher/runtime/WowAddonsTest.kt @@ -0,0 +1,221 @@ +package com.droiddeck.launcher.runtime + +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry +import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertThrows +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File +import java.io.IOException +import java.nio.file.Files +import java.nio.file.LinkOption +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +class WowAddonsTest { + @get:Rule val temp = TemporaryFolder() + + private fun zip(vararg files: Pair): File { + val file = temp.newFile("addon-${System.nanoTime()}.zip") + ZipOutputStream(file.outputStream()).use { out -> + for ((name, text) in files) { + out.putNextEntry(ZipEntry(name)) + out.write(text.toByteArray()) + out.closeEntry() + } + } + return file + } + + private fun target(): WowAddons.Target { + val root = temp.newFolder("World of Warcraft") + File(root, "_retail_").mkdirs() + return WowAddons.targets(BattleNet.title("wow")!!, root).single() + } + + private fun names(vararg files: String) = WowAddons.addons(files.toList()).map { it.name to it.root } + + @Test fun plainFoldersKeepTheirNames() { + assertEquals(listOf("Foo" to "Foo/", "Bar" to "Bar/"), names("Foo/Foo.toc", "Foo/a.lua", "Bar/Bar.toc")) + } + + @Test fun flavourTocsKeepTheFolderName() { + assertEquals(listOf("Foo" to "Foo/"), names("Foo/Foo_Mainline.toc", "Foo/Foo_Vanilla.toc", "Foo/Foo-Classic.toc")) + } + + @Test fun anAddonNamedLikeAFlavourStaysWhole() { + assertEquals(listOf("Foo_Classic" to "Foo_Classic/"), names("Foo_Classic/Foo_Classic.toc")) + } + + @Test fun sourceWrappersTakeTheTocName() { + assertEquals(listOf("Foo" to "Foo-main/"), names("Foo-main/Foo.toc", "Foo-main/README.md")) + assertEquals(listOf("Foo" to "Repo-1.2.3/"), names("Repo-1.2.3/Foo_Mainline.toc", "Repo-1.2.3/Foo_TBC.toc")) + } + + @Test fun packsAndGameFolderWrappersFindEveryAddon() { + assertEquals( + listOf("Foo" to "Interface/AddOns/Foo/", "Bar" to "Interface/AddOns/Bar/"), + names("Interface/AddOns/Foo/Foo.toc", "Interface/AddOns/Bar/Bar.toc"), + ) + } + + @Test fun bareFilesGetAFolder() { + assertEquals(listOf("Foo" to ""), names("Foo.toc", "core.lua")) + } + + @Test fun nestedTocsBelongToTheirAddon() { + assertEquals(listOf("Foo" to "Foo/"), names("Foo/Foo.toc", "Foo/Libs/Lib/Lib.toc")) + } + + @Test fun anAddonInTheFolderOfASimilarNameIsNotNested() { + assertEquals(listOf("Foo" to "Foo/", "FooOptions" to "FooOptions/"), names("Foo/Foo.toc", "FooOptions/FooOptions.toc")) + } + + @Test fun importsIntoInterfaceAddOns() { + val target = target() + val zip = zip("Foo-main/Foo.toc" to "toc", "Foo-main/sub/a.lua" to "lua", "__MACOSX/Foo-main/._Foo.toc" to "x") + assertEquals(listOf("Foo"), WowAddons.import(zip, target)) + assertEquals(File(target.addOns.parentFile, "AddOns"), target.addOns) + assertEquals("lua", File(target.addOns, "Foo/sub/a.lua").readText()) + assertEquals(listOf("Foo"), WowAddons.installed(target)) + assertEquals(listOf("AddOns"), target.addOns.parentFile!!.list()!!.toList()) + } + + @Test fun reimportReplacesTheFolderIgnoringCase() { + val target = target() + File(target.addOns, "foo").mkdirs() + File(target.addOns, "foo/stale.lua").writeText("old") + File(target.addOns, "Keep").mkdirs() + WowAddons.import(zip("Foo/Foo.toc" to "new"), target) + assertTrue(File(target.addOns, "Foo/Foo.toc").isFile) + assertFalse(File(target.addOns, "foo").exists()) + assertTrue(File(target.addOns, "Keep").isDirectory) + } + + @Test fun replacingALinkLeavesItsTargetAlone() { + val target = target() + val outside = temp.newFolder("outside") + File(outside, "keep.lua").writeText("keep") + target.addOns.mkdirs() + Files.createSymbolicLink(File(target.addOns, "Foo").toPath(), outside.toPath()) + WowAddons.import(zip("Foo/Foo.toc" to "new"), target) + assertEquals("keep", File(outside, "keep.lua").readText()) + assertFalse(Files.isSymbolicLink(File(target.addOns, "Foo").toPath())) + } + + @Test fun deletesOnlyTheChosenAddon() { + val target = target() + WowAddons.import(zip("Foo/Foo.toc" to "a", "Bar/Bar.toc" to "b"), target) + assertEquals(listOf("Bar", "Foo"), WowAddons.installed(target)) + WowAddons.delete(target, "Foo") + assertEquals(listOf("Bar"), WowAddons.installed(target)) + assertTrue(File(target.addOns, "Bar/Bar.toc").isFile) + } + + @Test fun deletingALinkLeavesItsTargetAlone() { + val target = target() + val outside = temp.newFolder("linked") + File(outside, "keep.lua").writeText("keep") + target.addOns.mkdirs() + Files.createSymbolicLink(File(target.addOns, "Foo").toPath(), outside.toPath()) + WowAddons.delete(target, "Foo") + assertFalse(Files.exists(File(target.addOns, "Foo").toPath(), LinkOption.NOFOLLOW_LINKS)) + assertEquals("keep", File(outside, "keep.lua").readText()) + } + + @Test fun onlyAFolderDirectlyInAddOnsCanBeDeleted() { + val target = target() + val outside = File(target.addOns.parentFile!!.parentFile, "Outside").apply { mkdirs() } + target.addOns.mkdirs() + assertThrows(IOException::class.java) { WowAddons.delete(target, "../../Outside") } + assertTrue(outside.isDirectory) + } + + @Test fun aFailedReplacementRestoresEarlierAddons() { + val base = temp.newFolder("swap") + val addOns = File(base, "AddOns").apply { mkdirs() } + File(addOns, "Foo").mkdirs() + File(addOns, "Foo/old.lua").writeText("old") + File(addOns, "Bar").mkdirs() + File(addOns, "Bar/old.lua").writeText("old") + val fresh = File(base, "fresh/Foo").apply { mkdirs() } + File(fresh, "new.lua").writeText("new") + val old = File(base, "old").apply { mkdirs() } + assertThrows(IOException::class.java) { + WowAddons.swap(listOf(fresh to "Foo", File(base, "fresh/Missing") to "Bar"), addOns, old) + } + assertEquals("old", File(addOns, "Foo/old.lua").readText()) + assertEquals("old", File(addOns, "Bar/old.lua").readText()) + assertFalse(File(addOns, "Foo/new.lua").exists()) + } + + @Test fun zipsWithoutAnAddonChangeNothing() { + val target = target() + assertEquals(emptyList(), WowAddons.import(zip("readme.txt" to "x"), target)) + assertFalse(target.addOns.exists()) + } + + @Test fun linkEntriesDoNotCountAsAddons() { + val target = target() + val file = temp.newFile("links.zip") + ZipArchiveOutputStream(file).use { out -> + val link = ZipArchiveEntry("Foo/Foo.toc").apply { unixMode = 0xA1FF } + out.putArchiveEntry(link) + out.write("/etc/passwd".toByteArray()) + out.closeArchiveEntry() + } + assertEquals(emptyList(), WowAddons.import(file, target)) + assertFalse(target.addOns.exists()) + } + + @Test fun aZipWithAnyLinkIsRefusedWhole() { + val target = target() + File(target.addOns, "Ace3").mkdirs() + File(target.addOns, "Ace3/keep.lua").writeText("keep") + val file = temp.newFile("nested.zip") + ZipArchiveOutputStream(file).use { out -> + out.putArchiveEntry(ZipArchiveEntry("Foo/Foo.toc").apply { unixMode = 0xA1FF }) + out.write("/etc/passwd".toByteArray()) + out.closeArchiveEntry() + out.putArchiveEntry(ZipArchiveEntry("Foo/Libs/Ace3/Ace3.toc")) + out.write("toc".toByteArray()) + out.closeArchiveEntry() + } + assertEquals(emptyList(), WowAddons.import(file, target)) + assertEquals("keep", File(target.addOns, "Ace3/keep.lua").readText()) + } + + @Test fun escapingEntriesAreNeverWritten() { + val target = target() + WowAddons.import(zip("Foo/Foo.toc" to "a", "../../evil.lua" to "x", "Foo/../../evil2.lua" to "x"), target) + val written = target.addOns.parentFile!!.parentFile!!.walkTopDown().map { it.name }.toList() + assertFalse("evil.lua" in written) + assertFalse("evil2.lua" in written) + assertTrue("Foo.toc" in written) + } + + @Test fun usesTheInstalledFlavourFolders() { + val root = temp.newFolder("wow") + listOf("_classic_", "_classic_era_", "_anniversary_", "_retail_").forEach { File(root, it).mkdirs() } + val classic = BattleNet.title("wow_classic")!! + assertEquals(setOf("_classic_", "_classic_era_", "_anniversary_"), WowAddons.targets(classic, root).map { it.flavor }.toSet()) + assertEquals(listOf("_retail_"), WowAddons.targets(BattleNet.title("wow")!!, File(root, "_retail_")).map { it.flavor }) + } + + @Test fun reusesAnExistingFolderSpelling() { + val root = temp.newFolder("wow2") + File(root, "_forever_/interface/addons").mkdirs() + val target = WowAddons.targets(BattleNet.title("wow_forever")!!, root).single() + assertEquals(File(root, "_forever_/interface/addons"), target.addOns) + } + + @Test fun otherGamesHaveNoTargets() { + val root = temp.newFolder("d4") + File(root, "_retail_").mkdirs() + assertEquals(emptyList(), WowAddons.targets(BattleNet.title("fenris")!!, root).map { it.flavor }) + } +}