diff --git a/app/src/main/feature/retro/Gen1EmbedLaunch.kt b/app/src/main/feature/retro/Gen1EmbedLaunch.kt index 6e3688f0d..e9153e714 100644 --- a/app/src/main/feature/retro/Gen1EmbedLaunch.kt +++ b/app/src/main/feature/retro/Gen1EmbedLaunch.kt @@ -151,6 +151,10 @@ object Gen1EmbedLaunch { Gen1EngineActivity.EXTRA_ARTWORK_PATH, shortcut.getExtra("customCoverArtPath"), ) + putExtra( + Gen1EngineActivity.EXTRA_ENGINE_VARS, + Gen1EngineSettings.resolve(context, shortcut), + ) } } } diff --git a/app/src/main/feature/retro/Gen1EngineActivity.kt b/app/src/main/feature/retro/Gen1EngineActivity.kt index f4a71f844..713af01c0 100644 --- a/app/src/main/feature/retro/Gen1EngineActivity.kt +++ b/app/src/main/feature/retro/Gen1EngineActivity.kt @@ -20,8 +20,12 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.setViewTreeLifecycleOwner import androidx.lifecycle.setViewTreeViewModelStoreOwner +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import androidx.savedstate.SavedStateRegistry import androidx.savedstate.SavedStateRegistryController import androidx.savedstate.SavedStateRegistryOwner @@ -45,6 +49,7 @@ class Gen1EngineActivity : private lateinit var bridge: Gen1EngineBridge private var touchControls = true + private var engineVarsApplied = false private var persistShortcut: Shortcut? = null @@ -163,6 +168,12 @@ class Gen1EngineActivity : override fun dispatchKeyEvent(event: android.view.KeyEvent): Boolean { val keyCode = event.keyCode + if (keyCode == android.view.KeyEvent.KEYCODE_BUTTON_MODE && isGamepadSource(event)) { + if (event.action == android.view.KeyEvent.ACTION_UP) { + if (menu.visible) menu.close() else openMenu() + } + return true + } if (keyCode == android.view.KeyEvent.KEYCODE_BACK) { if (event.action == android.view.KeyEvent.ACTION_UP) { if (menu.visible) { @@ -180,6 +191,24 @@ class Gen1EngineActivity : return super.dispatchKeyEvent(event) } + override fun dispatchGenericMotionEvent(event: android.view.MotionEvent): Boolean { + val joystick = + event.source and android.view.InputDevice.SOURCE_JOYSTICK == + android.view.InputDevice.SOURCE_JOYSTICK + if (menu.visible && joystick) { + val hatX = event.getAxisValue(android.view.MotionEvent.AXIS_HAT_X) + val hatY = event.getAxisValue(android.view.MotionEvent.AXIS_HAT_Y) + val x = + if (kotlin.math.abs(hatX) > 0.5f) hatX else event.getAxisValue(android.view.MotionEvent.AXIS_X) + val y = + if (kotlin.math.abs(hatY) > 0.5f) hatY else event.getAxisValue(android.view.MotionEvent.AXIS_Y) + menu.handleAxis(x, y) + return true + } + if (menu.visible) return true + return super.dispatchGenericMotionEvent(event) + } + private fun buildTabs(): List = listOf( RetroTabSpec(null, RetroDrawerIcons.Play, getString(R.string.retro_tab_menu)), @@ -225,8 +254,99 @@ class Gen1EngineActivity : else -> RetroPane.SYSTEM } + private fun isGamepadSource(event: android.view.KeyEvent): Boolean { + val source = event.device?.sources ?: return false + return source and android.view.InputDevice.SOURCE_GAMEPAD == android.view.InputDevice.SOURCE_GAMEPAD || + source and android.view.InputDevice.SOURCE_JOYSTICK == android.view.InputDevice.SOURCE_JOYSTICK + } + + private fun stadiumRomEntry(row: Gen1EngineBridge.Row): RetroMenuEntry { + val building = Gen1StadiumRom.isBuilding(row.value) || Gen1StadiumRom.hasStagedPick(this) + val installed = Gen1StadiumRom.isInstalled(this) + return RetroMenuEntry.Action( + label = row.label, + icon = RetroDrawerIcons.EditLayout, + danger = installed && !building, + subtitle = + when { + building -> getString(R.string.retro_stadium_building) + installed -> getString(R.string.retro_stadium_ready) + else -> getString(R.string.retro_stadium_import) + }, + ) { + when { + building -> Unit + installed -> promptStadiumDelete() + else -> pickStadiumRom() + } + } + } + + private fun pickStadiumRom() { + menu.close() + val intent = + android.content.Intent(android.content.Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(android.content.Intent.CATEGORY_OPENABLE) + type = "*/*" + } + runCatching { startActivityForResult(intent, REQUEST_STADIUM_ROM) } + .onFailure { toast(getString(R.string.retro_stadium_no_picker)) } + } + + private fun promptStadiumDelete() { + menu.confirmPrompt = + RetroConfirmPrompt( + title = getString(R.string.retro_stadium_row), + message = getString(R.string.retro_stadium_delete_body), + confirmLabel = getString(R.string.retro_stadium_delete), + dismissLabel = getString(R.string.retro_stadium_keep), + onConfirm = { + menu.confirmPrompt = null + val gone = Gen1StadiumRom.delete(this) + toast( + getString( + if (gone) { + R.string.retro_stadium_deleted + } else { + R.string.retro_stadium_delete_failed + }, + ), + ) + menu.rebuild() + pollFaster() + }, + onDismiss = { menu.confirmPrompt = null }, + ) + } + + override fun onActivityResult( + requestCode: Int, + resultCode: Int, + data: android.content.Intent?, + ) { + if (requestCode != REQUEST_STADIUM_ROM) { + super.onActivityResult(requestCode, resultCode, data) + return + } + val uri = data?.data + if (resultCode != RESULT_OK || uri == null) return + toast(getString(R.string.retro_stadium_importing)) + lifecycleScope.launch(Dispatchers.IO) { + val result = Gen1StadiumRom.stage(this@Gen1EngineActivity, uri) + withContext(Dispatchers.Main) { + result + .onSuccess { + toast(getString(R.string.retro_stadium_imported)) + pollFaster() + } + .onFailure { toast(it.message ?: getString(R.string.retro_stadium_import_failed)) } + } + } + } + private fun rowEntry(row: Gen1EngineBridge.Row): RetroMenuEntry = when { + row.id == Gen1StadiumRom.ROW_ID -> stadiumRomEntry(row) row.values.isNotEmpty() -> RetroMenuEntry.Choice(row.label, row.values, row.selectedIndex) { index -> bridge.setRow(row.id, index) @@ -631,6 +751,16 @@ class Gen1EngineActivity : private fun onEngineState(state: Gen1EngineBridge.State, menuChanged: Boolean) { importState = state.import if (loadingVisible && state.booted) loadingVisible = false + if (state.booted && state.rows.isNotEmpty()) { + if (!engineVarsApplied) { + engineVarsApplied = true + @Suppress("UNCHECKED_CAST") + val wanted = + intent.getSerializableExtra(EXTRA_ENGINE_VARS) as? HashMap + if (wanted != null) Gen1EngineSettings.applyTo(bridge, wanted) + } + Gen1EngineSettings.cache(this, state.rows) + } if (menuChanged && menu.visible) menu.rebuild() } @@ -852,10 +982,13 @@ class Gen1EngineActivity : companion object { private const val TAG = "WnGen1Engine" + private const val REQUEST_STADIUM_ROM = 0x5D01 + const val EXTRA_ROM_PATH = "wn.engine.rom" const val EXTRA_VERSION = "wn.engine.version" const val EXTRA_GAME_NAME = "wn.engine.game_name" const val EXTRA_SHORTCUT_PATH = "wn.engine.shortcut" + const val EXTRA_ENGINE_VARS = "wn.engine.vars" const val EXTRA_ARTWORK_PATH = "wn.engine.artwork" private const val DPAD_DEADZONE = 0.35f diff --git a/app/src/main/feature/retro/Gen1EngineSettings.kt b/app/src/main/feature/retro/Gen1EngineSettings.kt new file mode 100644 index 000000000..e1b85b7dd --- /dev/null +++ b/app/src/main/feature/retro/Gen1EngineSettings.kt @@ -0,0 +1,111 @@ +package com.winlator.cmod.feature.retro + +import android.content.Context +import androidx.preference.PreferenceManager +import com.winlator.cmod.runtime.container.Shortcut +import org.json.JSONArray +import org.json.JSONObject + +object Gen1EngineSettings { + private const val CACHE_KEY = "gen1_engine_rows" + + data class CachedRow( + val id: String, + val label: String, + val values: List, + val pane: String, + ) + + fun cache( + context: Context, + rows: List, + ) { + val usable = rows.filter { it.values.size > 1 && it.id != Gen1StadiumRom.ROW_ID } + if (usable.isEmpty()) return + val array = JSONArray() + usable.forEach { row -> + array.put( + JSONObject() + .put("id", row.id) + .put("label", row.label) + .put("pane", paneOf(row.id)) + .put("values", JSONArray(row.values)), + ) + } + PreferenceManager.getDefaultSharedPreferences(context) + .edit().putString(CACHE_KEY, array.toString()).apply() + } + + fun cached(context: Context): List = + runCatching { + val raw = + PreferenceManager.getDefaultSharedPreferences(context) + .getString(CACHE_KEY, null) ?: return emptyList() + val array = JSONArray(raw) + (0 until array.length()).mapNotNull { i -> + val o = array.optJSONObject(i) ?: return@mapNotNull null + val values = o.optJSONArray("values") ?: return@mapNotNull null + val list = (0 until values.length()).map { values.optString(it) } + if (list.size < 2) return@mapNotNull null + CachedRow( + id = o.optString("id"), + label = o.optString("label"), + values = list, + pane = o.optString("pane", PANE_SYSTEM), + ) + } + }.getOrDefault(emptyList()) + + fun selection( + shortcut: Shortcut, + row: CachedRow, + ): String? = + shortcut.getExtra(RetroShortcuts.VAR_PREFIX + row.id) + .takeIf { it.isNotEmpty() && it in row.values } + + fun resolve( + context: Context, + shortcut: Shortcut, + ): HashMap { + val out = HashMap() + cached(context).forEach { row -> + selection(shortcut, row)?.let { out[row.id] = it } + } + return out + } + + fun applyTo( + bridge: Gen1EngineBridge, + wanted: Map, + ) { + if (wanted.isEmpty()) return + bridge.state.rows.forEach { row -> + val target = wanted[row.id] ?: return@forEach + if (row.values.isEmpty()) return@forEach + val index = row.values.indexOf(target) + if (index >= 0 && index != row.selectedIndex) bridge.setRow(row.id, index) + } + } + + const val PANE_DISPLAY = "display" + const val PANE_SOUND = "sound" + const val PANE_PERFORMANCE = "performance" + const val PANE_CONTROLS = "controls" + const val PANE_SYSTEM = "system" + + private val SOUND_ROWS = setOf("musicVol", "sfxVol", "pikaVol", "musicFilter") + private val DISPLAY_ROWS = + setOf("colors", "tilt", "gbcfx", "zoom", "voidFill", "videoMode", "animations") + private val PERFORMANCE_ROWS = setOf("fpsCap", "speed") + private val CONTROL_ROWS = setOf("controls") + + private fun paneOf(id: String): String = + when { + Gen1EngineBridge.isModRow(id) -> PANE_DISPLAY + id in SOUND_ROWS -> PANE_SOUND + id in DISPLAY_ROWS -> PANE_DISPLAY + id in PERFORMANCE_ROWS -> PANE_PERFORMANCE + id in CONTROL_ROWS -> PANE_CONTROLS + else -> PANE_SYSTEM + } +} diff --git a/app/src/main/feature/retro/Gen1StadiumRom.kt b/app/src/main/feature/retro/Gen1StadiumRom.kt new file mode 100644 index 000000000..02c018783 --- /dev/null +++ b/app/src/main/feature/retro/Gen1StadiumRom.kt @@ -0,0 +1,90 @@ +package com.winlator.cmod.feature.retro + +import android.content.Context +import android.net.Uri +import java.io.File + +object Gen1StadiumRom { + const val ROW_ID = "DRAMATIC_SHAPE:stadiumRom" + + private const val SAVE_SUBDIR = "save/pokemon-love2d" + private const val PICKED = "picked_stadium.z64" + private const val PACK_SUBDIR = "dramatic_shape/stadium" + private const val ROM_SUBDIR = "baseroms" + private const val MIN_ROM_BYTES = 4L * 1024 * 1024 + + private val MAGICS = + listOf( + byteArrayOf(0x80.toByte(), 0x37, 0x12, 0x40), + byteArrayOf(0x37, 0x80.toByte(), 0x40, 0x12), + byteArrayOf(0x40, 0x12, 0x37, 0x80.toByte()), + ) + + fun saveRoot(context: Context): File = + File(context.getExternalFilesDir(null) ?: context.filesDir, SAVE_SUBDIR) + + private fun packDir(context: Context): File = File(saveRoot(context), PACK_SUBDIR) + + private fun romDir(context: Context): File = File(saveRoot(context), ROM_SUBDIR) + + fun isBuilding(value: String): Boolean = value.trim().equals("BUILDING", ignoreCase = true) + + fun isInstalled(context: Context): Boolean { + val marker = File(packDir(context), "pack.info") + if (!marker.isFile) return false + return runCatching { marker.readText().trim().isNotEmpty() }.getOrDefault(false) + } + + fun stage( + context: Context, + uri: Uri, + ): Result = + runCatching { + val root = saveRoot(context).apply { mkdirs() } + val staging = File(root, "$PICKED.part") + staging.delete() + + val copied = + context.contentResolver.openInputStream(uri)?.use { input -> + staging.outputStream().use { output -> input.copyTo(output) } + } ?: throw IllegalStateException("Could not read that file") + + if (copied < MIN_ROM_BYTES) { + staging.delete() + throw IllegalArgumentException("That file is too small to be a Nintendo 64 ROM") + } + if (!looksLikeN64(staging)) { + staging.delete() + throw IllegalArgumentException("That is not a Nintendo 64 ROM (.z64, .v64 or .n64)") + } + + val target = File(root, PICKED) + target.delete() + if (!staging.renameTo(target)) { + staging.delete() + throw IllegalStateException("Could not hand the ROM to the engine") + } + copied + } + + private fun looksLikeN64(file: File): Boolean = + runCatching { + val head = ByteArray(4) + file.inputStream().use { if (it.read(head) != 4) return false } + MAGICS.any { magic -> magic.contentEquals(head) } + }.getOrDefault(false) + + fun hasStagedPick(context: Context): Boolean = File(saveRoot(context), PICKED).isFile + + fun delete(context: Context): Boolean = + runCatching { + var removed = packDir(context).deleteRecursively() + val roms = romDir(context) + if (roms.isDirectory) { + roms.listFiles().orEmpty().forEach { if (it.isFile && it.delete()) removed = true } + } + File(saveRoot(context), PICKED).delete() + File(saveRoot(context), "$PICKED.part").delete() + removed + }.getOrDefault(false) +} diff --git a/app/src/main/feature/retro/RetroDrawerMenu.kt b/app/src/main/feature/retro/RetroDrawerMenu.kt index e951a6394..b57fa8e3f 100644 --- a/app/src/main/feature/retro/RetroDrawerMenu.kt +++ b/app/src/main/feature/retro/RetroDrawerMenu.kt @@ -92,6 +92,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.winlator.cmod.R import com.winlator.cmod.shared.theme.SessionDrawerStyle +import com.winlator.cmod.shared.theme.WinNativeDanger import com.winlator.cmod.shared.theme.WinNativeOutline import com.winlator.cmod.shared.theme.WinNativeSurface import com.winlator.cmod.shared.ui.outlinedSwitchColors @@ -128,6 +129,15 @@ class RetroRenamePrompt( val onConfirm: (String?) -> Unit, ) +class RetroConfirmPrompt( + val title: String, + val message: String, + val confirmLabel: String, + val dismissLabel: String, + val onConfirm: () -> Unit, + val onDismiss: () -> Unit, +) + class RetroConflictPrompt( val message: String, val options: List, @@ -251,6 +261,7 @@ class RetroMenuController { var bottomProvider: (() -> List)? = null var renamePrompt by mutableStateOf(null) var conflictPrompt by mutableStateOf(null) + var confirmPrompt by mutableStateOf(null) val gridColumns: Int get() = if (pane == null) 3 else 1 @@ -495,6 +506,9 @@ fun RetroDrawerMenu(controller: RetroMenuController) { controller.conflictPrompt?.let { prompt -> RetroConflictDialog(prompt) } + controller.confirmPrompt?.let { prompt -> + RetroConfirmDialog(prompt) + } } } @@ -1523,6 +1537,90 @@ internal fun RetroRenameDialog( } } +@Composable +internal fun RetroConfirmDialog(prompt: RetroConfirmPrompt) { + Box( + modifier = + Modifier + .fillMaxSize() + .background(Color(0xCC000000)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { prompt.onDismiss() }, + contentAlignment = Alignment.Center, + ) { + Column( + modifier = + Modifier + .width(340.dp) + .clip(RoundedCornerShape(16.dp)) + .background(WinNativeSurface) + .border(1.dp, WinNativeOutline, RoundedCornerShape(16.dp)) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) {} + .padding(16.dp), + ) { + Text( + text = prompt.title, + color = DrawerTextPrimary, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + ) + Spacer(Modifier.height(6.dp)) + Text( + text = prompt.message, + color = DrawerTextSecondary, + fontSize = 12.sp, + ) + Spacer(Modifier.height(14.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Row( + modifier = + Modifier + .weight(1f) + .clip(RoundedCornerShape(10.dp)) + .background(DrawerFocusFill) + .border(1.dp, DrawerAccent, RoundedCornerShape(10.dp)) + .clickable { prompt.onDismiss() } + .padding(horizontal = 12.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = prompt.dismissLabel, + color = DrawerActiveAccent, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + ) + } + Row( + modifier = + Modifier + .weight(1f) + .clip(RoundedCornerShape(10.dp)) + .background(PaneInnerResting) + .border(1.dp, RestingCardBorder, RoundedCornerShape(10.dp)) + .clickable { prompt.onConfirm() } + .padding(horizontal = 12.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.Center, + ) { + Text( + text = prompt.confirmLabel, + color = WinNativeDanger, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + ) + } + } + } + } +} + @Composable internal fun RetroConflictDialog(prompt: RetroConflictPrompt) { Box( diff --git a/app/src/main/feature/retro/RetroGameSettings.kt b/app/src/main/feature/retro/RetroGameSettings.kt index bf1bae663..a0193514c 100644 --- a/app/src/main/feature/retro/RetroGameSettings.kt +++ b/app/src/main/feature/retro/RetroGameSettings.kt @@ -77,6 +77,7 @@ import com.winlator.cmod.shared.ui.nav.paneHighlight import com.winlator.cmod.shared.ui.nav.paneNavItem import com.winlator.cmod.shared.ui.outlinedSwitchColors import com.winlator.cmod.shared.ui.widget.chasingBorder +import kotlinx.coroutines.launch private val BgDeep = GameSettingsStyle.BgDeep private val SidebarBg = GameSettingsStyle.SidebarBg @@ -134,6 +135,15 @@ class RetroSettingsState( .lowercase().let { if (it in UPSCALE_KEYS) it else "native" }, ) var engine3d by mutableStateOf(shortcut.getExtra(Gen1EmbedLaunch.KEY_ENGINE_3D) == "1") + + val engineValues: androidx.compose.runtime.snapshots.SnapshotStateMap = + androidx.compose.runtime.mutableStateMapOf().apply { + if (context != null) { + Gen1EngineSettings.cached(context).forEach { row -> + Gen1EngineSettings.selection(shortcut, row)?.let { put(row.id, it) } + } + } + } var touchControls by mutableStateOf( shortcut.getExtra(RetroShortcuts.KEY_TOUCH_CONTROLS) .ifEmpty { if (context != null && sysId != null) (if (RetroDefaults.touchControls(context, sysId)) "1" else "0") else "1" } != "0", @@ -205,6 +215,11 @@ class RetroSettingsState( shortcut.putExtra(RetroShortcuts.KEY_SGSR, if (sgsr) "1" else "0") shortcut.putExtra(RetroShortcuts.KEY_UPSCALE, upscale) shortcut.putExtra(Gen1EmbedLaunch.KEY_ENGINE_3D, if (engine3d) "1" else "0") + if (context != null) { + Gen1EngineSettings.cached(context).forEach { row -> + shortcut.putExtra(RetroShortcuts.VAR_PREFIX + row.id, engineValues[row.id]) + } + } shortcut.putExtra(RetroShortcuts.KEY_TOUCH_CONTROLS, if (touchControls) "1" else "0") shortcut.putExtra(RetroShortcuts.KEY_ADAPTIVE_STICKS, if (adaptiveSticks) "1" else "0") shortcut.putExtra(RetroShortcuts.KEY_HDD_IMAGE, hddImage) @@ -1113,6 +1128,16 @@ private fun RetroGraphicsSection(state: RetroSettingsState) { val embeddedDolphin = RetroCoreManager.usesDolphinCore(state.system) && RetroShortcuts.embeddedDolphinEnabled(embeddedDolphinContext) + val voxelEngine = state.engine3d + if (voxelEngine) { + Spacer(Modifier.height(12.dp)) + RetroSettingGroup { + RetroGroupTitle(stringResource(R.string.retro_gs_engine_3d).uppercase()) + RetroStadiumRomSetting() + } + RetroEngineRowSettings(state) + } + if (!voxelEngine) { RetroSettingGroup { RetroGroupTitle(stringResource(R.string.retro_gs_group_video)) if (embeddedDolphin) { @@ -1140,7 +1165,8 @@ private fun RetroGraphicsSection(state: RetroSettingsState) { } } } - if (state.coreOptions.isNotEmpty()) { + } + if (state.coreOptions.isNotEmpty() && !voxelEngine) { Spacer(Modifier.height(12.dp)) RetroSettingGroup { RetroGroupTitle((state.system?.shortName ?: stringResource(R.string.retro_gs_group_core)).uppercase()) @@ -1160,6 +1186,160 @@ private fun RetroGraphicsSection(state: RetroSettingsState) { } } +@Composable +private fun RetroEngineRowSettings(state: RetroSettingsState) { + val context = androidx.compose.ui.platform.LocalContext.current + val rows = remember(context) { Gen1EngineSettings.cached(context) } + if (rows.isEmpty()) { + RetroSettingGroup { + RetroSettingNote(stringResource(R.string.retro_gs_engine_3d_uncached)) + } + return + } + val panes = + listOf( + Gen1EngineSettings.PANE_DISPLAY to R.string.retro_gs_section_graphics, + Gen1EngineSettings.PANE_SOUND to R.string.retro_gs_group_audio, + Gen1EngineSettings.PANE_PERFORMANCE to R.string.retro_gs_section_performance, + Gen1EngineSettings.PANE_CONTROLS to R.string.retro_gs_group_input, + Gen1EngineSettings.PANE_SYSTEM to R.string.retro_gs_section_general, + ) + panes.forEach { (pane, titleRes) -> + val paneRows = rows.filter { it.pane == pane } + if (paneRows.isEmpty()) return@forEach + Spacer(Modifier.height(12.dp)) + RetroSettingGroup { + RetroGroupTitle(stringResource(titleRes).uppercase()) + paneRows.forEach { row -> + val current = state.engineValues[row.id] + RetroSettingDropdown( + label = row.label, + entries = listOf(stringResource(R.string.retro_gs_engine_3d_leave)) + row.values, + selectedIndex = (row.values.indexOf(current) + 1).coerceAtLeast(0), + onSelected = { index -> + if (index == 0) { + state.engineValues.remove(row.id) + } else { + state.engineValues[row.id] = row.values[index - 1] + } + }, + ) + } + } + } + RetroSettingNote(stringResource(R.string.retro_gs_engine_3d_note)) +} + +@Composable +private fun RetroSettingNote(text: String) { + Text( + text = text, + color = TextDim, + fontSize = 11.sp, + modifier = Modifier.padding(top = 6.dp), + ) +} + +@Composable +private fun RetroStadiumRomSetting() { + val context = androidx.compose.ui.platform.LocalContext.current + val scope = androidx.compose.runtime.rememberCoroutineScope() + var refresh by remember { mutableIntStateOf(0) } + var confirmDelete by remember { mutableStateOf(false) } + val installed = remember(refresh) { Gen1StadiumRom.isInstalled(context) } + val staged = remember(refresh) { Gen1StadiumRom.hasStagedPick(context) } + + val picker = + androidx.activity.compose.rememberLauncherForActivityResult( + androidx.activity.result.contract.ActivityResultContracts.OpenDocument(), + ) { uri -> + if (uri != null) { + scope.launch { + val result = + kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + Gen1StadiumRom.stage(context, uri) + } + result + .onSuccess { + android.widget.Toast.makeText( + context, + context.getString(R.string.retro_stadium_imported), + android.widget.Toast.LENGTH_LONG, + ).show() + } + .onFailure { + android.widget.Toast.makeText( + context, + it.message ?: context.getString(R.string.retro_stadium_import_failed), + android.widget.Toast.LENGTH_LONG, + ).show() + } + refresh++ + } + } + } + + val status = + when { + staged -> stringResource(R.string.retro_stadium_building) + installed -> stringResource(R.string.retro_stadium_ready) + else -> stringResource(R.string.retro_stadium_import) + } + + Row( + modifier = + Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(FieldCorner)) + .background(InputSurface) + .border(1.dp, InputBorder, RoundedCornerShape(FieldCorner)) + .clickable { + when { + staged -> Unit + installed -> confirmDelete = true + else -> picker.launch(arrayOf("*/*")) + } + } + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + stringResource(R.string.retro_stadium_row), + color = TextPrimary, + fontSize = ValueSize, + modifier = Modifier.weight(1f), + ) + Text(status, color = if (installed) TextPrimary else TextDim, fontSize = ValueSize) + } + + if (confirmDelete) { + androidx.compose.material3.AlertDialog( + onDismissRequest = { confirmDelete = false }, + title = { Text(stringResource(R.string.retro_stadium_row)) }, + text = { Text(stringResource(R.string.retro_stadium_delete_body)) }, + confirmButton = { + androidx.compose.material3.TextButton(onClick = { + confirmDelete = false + val gone = Gen1StadiumRom.delete(context) + android.widget.Toast.makeText( + context, + context.getString( + if (gone) R.string.retro_stadium_deleted else R.string.retro_stadium_delete_failed, + ), + android.widget.Toast.LENGTH_LONG, + ).show() + refresh++ + }) { Text(stringResource(R.string.retro_stadium_delete)) } + }, + dismissButton = { + androidx.compose.material3.TextButton(onClick = { confirmDelete = false }) { + Text(stringResource(R.string.retro_stadium_keep)) + } + }, + ) + } +} + @Composable private fun DolphinGpuDriverDropdown() { val context = androidx.compose.ui.platform.LocalContext.current @@ -1900,12 +2080,14 @@ private fun RetroInputSection(state: RetroSettingsState) { checked = state.touchControls, onCheckedChange = { state.touchControls = it }, ) - RetroSettingSwitch( - label = stringResource(R.string.retro_gs_adaptive_sticks), - checked = state.adaptiveSticks, - subtitle = stringResource(R.string.retro_gs_adaptive_sticks_subtitle), - onCheckedChange = { state.adaptiveSticks = it }, - ) + if (!state.engine3d) { + RetroSettingSwitch( + label = stringResource(R.string.retro_gs_adaptive_sticks), + checked = state.adaptiveSticks, + subtitle = stringResource(R.string.retro_gs_adaptive_sticks_subtitle), + onCheckedChange = { state.adaptiveSticks = it }, + ) + } val sysId = state.system?.id if (sysId == RetroSystems.PSX.id || RetroCoreManager.usesDolphinCore(state.system)) { var invVer by remember { androidx.compose.runtime.mutableIntStateOf(0) } @@ -2006,10 +2188,14 @@ private fun RetroAudioSection(state: RetroSettingsState) { } RetroSettingGroup { RetroGroupTitle(stringResource(R.string.retro_gs_group_audio)) - RetroSettingSwitch( - label = stringResource(R.string.retro_gs_sound), - checked = state.audio, - onCheckedChange = { state.audio = it }, - ) + if (state.engine3d) { + RetroSettingNote(stringResource(R.string.retro_gs_engine_3d_audio_note)) + } else { + RetroSettingSwitch( + label = stringResource(R.string.retro_gs_sound), + checked = state.audio, + onCheckedChange = { state.audio = it }, + ) + } } } 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 2cf95cefe..c5a608a85 100644 --- a/app/src/main/res/values-b+es+419/strings.xml +++ b/app/src/main/res/values-b+es+419/strings.xml @@ -2044,6 +2044,23 @@ Ruta instalada: Preparando los datos del juego por primera vez Voxel 3D Juega este juego con el motor de vóxeles 3D en lugar del núcleo estándar + Los ajustes propios del motor 3D están en el menú del juego. + Inicia el juego una vez para que sus ajustes del motor 3D aparezcan aquí. + Dejar como en el juego + El sonido lo gestiona el motor 3D. Usa la pestaña Sonido del menú del juego. + ROM de Stadium + Lista + Importar una ROM + Creando modelos de combate… + Importando… + ROM importada. Los modelos de combate se están creando. + No se pudo importar esa ROM + No hay ningún selector de archivos en este dispositivo + Conservar + Eliminar + Los modelos de combate de Stadium están instalados. ¿Eliminarlos? + Modelos de combate de Stadium eliminados. Reinicia el juego para terminar de quitarlos. + No se pudieron eliminar los modelos de Stadium Controles en pantalla Respuesta háptica AUDIO diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index d7d930f13..7bce152eb 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -2046,6 +2046,23 @@ Installeret sti: Forbereder spildata for første gang Voxel 3D Spil dette spil i 3D-voxelmotoren i stedet for den almindelige kerne + 3D-motorens egne indstillinger findes i menuen i spillet. + Start spillet én gang, så 3D-motorens indstillinger kan vises her. + Behold som i spillet + Lyden styres af 3D-motoren. Brug fanen Lyd i menuen i spillet. + Stadium-ROM + Klar + Importér en ROM + Bygger kampmodeller… + Importerer… + ROM importeret. Kampmodellerne bygges nu. + Kunne ikke importere den ROM + Der er ingen filvælger på denne enhed + Behold + Slet + Stadium-kampmodellerne er installeret. Vil du slette dem? + Stadium-kampmodeller slettet. Genstart spillet for at fjerne dem helt. + Kunne ikke slette Stadium-modellerne Skærmknapper Haptisk feedback LYD diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 965812638..b63c7a88f 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -2046,6 +2046,23 @@ Installierter Pfad: Spieldaten werden zum ersten Mal vorbereitet Voxel 3D Dieses Spiel mit der 3D-Voxel-Engine statt mit dem Standard-Core spielen + Die eigenen Einstellungen der 3D-Engine stehen im Spielmenü. + Starte das Spiel einmal, damit die Einstellungen der 3D-Engine hier erscheinen. + Wie im Spiel belassen + Der Ton wird von der 3D-Engine verwaltet. Nutze den Tab „Ton“ im Spielmenü. + Stadium-ROM + Bereit + ROM importieren + Kampfmodelle werden erstellt… + Wird importiert… + ROM importiert. Die Kampfmodelle werden jetzt erstellt. + Diese ROM konnte nicht importiert werden + Auf diesem Gerät ist keine Dateiauswahl verfügbar + Behalten + Löschen + Die Stadium-Kampfmodelle sind installiert. Löschen? + Stadium-Kampfmodelle gelöscht. Starte das Spiel neu, um sie vollständig zu entfernen. + Die Stadium-Modelle konnten nicht gelöscht werden Bildschirmsteuerung Haptisches Feedback AUDIO diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index d8b8f2dbc..891826815 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -2045,6 +2045,23 @@ Ruta instalada: Preparando los datos del juego por primera vez Voxel 3D Juega a este juego con el motor de vóxeles 3D en lugar del núcleo estándar + Los ajustes propios del motor 3D están en el menú del juego. + Inicia el juego una vez para que sus ajustes del motor 3D aparezcan aquí. + Dejar como en el juego + El sonido lo gestiona el motor 3D. Usa la pestaña Sonido del menú del juego. + ROM de Stadium + Lista + Importar una ROM + Creando modelos de combate… + Importando… + ROM importada. Los modelos de combate se están creando. + No se pudo importar esa ROM + No hay ningún selector de archivos en este dispositivo + Conservar + Eliminar + Los modelos de combate de Stadium están instalados. ¿Eliminarlos? + Modelos de combate de Stadium eliminados. Reinicia el juego para terminar de quitarlos. + No se pudieron eliminar los modelos de Stadium Controles en pantalla Respuesta háptica AUDIO diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 0234bbdb9..9259f8ab3 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -2044,6 +2044,23 @@ Asennuspolku: Valmistellaan pelitietoja ensimmäistä kertaa Voxel 3D Pelaa tätä peliä 3D-voxelmoottorilla vakioytimen sijaan + 3D-moottorin omat asetukset ovat pelin sisäisessä valikossa. + Käynnistä peli kerran, jotta 3D-moottorin asetukset näkyvät täällä. + Jätä pelin mukaiseksi + Äänestä vastaa 3D-moottori. Käytä pelin valikon Ääni-välilehteä. + Stadium-ROM + Valmis + Tuo ROM + Rakennetaan taistelumalleja… + Tuodaan… + ROM tuotu. Taistelumalleja rakennetaan nyt. + ROM-tiedostoa ei voitu tuoda + Tässä laitteessa ei ole tiedostonvalitsinta + Säilytä + Poista + Stadiumin taistelumallit on asennettu. Poistetaanko ne? + Stadiumin taistelumallit poistettu. Käynnistä peli uudelleen viimeistelläksesi poiston. + Stadium-malleja ei voitu poistaa Näytön ohjaimet Haptinen palaute ÄÄNI diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0f8daff46..aa9e2a313 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2045,6 +2045,23 @@ Chemin installé : Préparation des données du jeu pour la première fois Voxel 3D Jouer à ce jeu avec le moteur voxel 3D au lieu du cœur standard + Les réglages propres au moteur 3D se trouvent dans le menu en jeu. + Lancez le jeu une fois pour que les réglages du moteur 3D apparaissent ici. + Laisser comme dans le jeu + Le son est géré par le moteur 3D. Utilisez l\'onglet Son du menu en jeu. + ROM Stadium + Prête + Importer une ROM + Création des modèles de combat… + Importation… + ROM importée. Les modèles de combat sont en cours de création. + Impossible d\'importer cette ROM + Aucun sélecteur de fichiers sur cet appareil + Conserver + Supprimer + Les modèles de combat Stadium sont installés. Les supprimer ? + Modèles de combat Stadium supprimés. Redémarrez le jeu pour terminer la suppression. + Impossible de supprimer les modèles Stadium Commandes à l\'écran Retour haptique AUDIO diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 7ff60f316..57ee71630 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1981,6 +1981,23 @@ पहली बार गेम डेटा तैयार किया जा रहा है वॉक्सेल 3D इस गेम को मानक कोर के बजाय 3D वॉक्सेल इंजन में खेलें + 3D इंजन की अपनी सेटिंग्स इन-गेम मेन्यू में हैं। + 3D इंजन की सेटिंग्स यहाँ दिखाने के लिए गेम एक बार चलाएँ। + गेम में सेट अनुसार छोड़ें + ध्वनि 3D इंजन द्वारा नियंत्रित होती है। इन-गेम मेन्यू में ध्वनि टैब का उपयोग करें। + Stadium ROM + तैयार + ROM आयात करें + बैटल मॉडल बन रहे हैं… + आयात हो रहा है… + ROM आयात हो गया। बैटल मॉडल अब बन रहे हैं। + वह ROM आयात नहीं हो सका + इस डिवाइस पर कोई फ़ाइल पिकर उपलब्ध नहीं है + रखें + हटाएँ + Stadium बैटल मॉडल इंस्टॉल हैं। उन्हें हटाएँ? + Stadium बैटल मॉडल हटाए गए। उन्हें पूरी तरह हटाने के लिए गेम पुनः आरंभ करें। + Stadium मॉडल हटाए नहीं जा सके ऑन-स्क्रीन नियंत्रण हैप्टिक फ़ीडबैक ऑडियो diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index cdadd0c61..e5ed7beaa 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2045,6 +2045,23 @@ Percorso installato: Preparazione dei dati di gioco per la prima volta Voxel 3D Gioca a questo gioco con il motore voxel 3D invece del core standard + Le impostazioni del motore 3D si trovano nel menu di gioco. + Avvia il gioco una volta perché le impostazioni del motore 3D compaiano qui. + Lascia come nel gioco + L\'audio è gestito dal motore 3D. Usa la scheda Audio nel menu di gioco. + ROM di Stadium + Pronta + Importa una ROM + Creazione dei modelli di lotta… + Importazione… + ROM importata. I modelli di lotta si stanno creando. + Impossibile importare quella ROM + Nessun selettore di file disponibile su questo dispositivo + Mantieni + Elimina + I modelli di lotta di Stadium sono installati. Eliminarli? + Modelli di lotta di Stadium eliminati. Riavvia il gioco per completare la rimozione. + Impossibile eliminare i modelli di Stadium Comandi a schermo Feedback aptico AUDIO diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 311c03fca..5457505ed 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -2044,6 +2044,23 @@ 初回のゲームデータを準備しています ボクセル3D 標準コアの代わりに3Dボクセルエンジンでこのゲームをプレイします + 3Dエンジン独自の設定はゲーム内メニューにあります。 + 3Dエンジンの設定をここに表示するには、ゲームを一度起動してください。 + ゲーム内の設定のまま + サウンドは3Dエンジンが管理します。ゲーム内メニューのサウンドタブを使用してください。 + スタジアムROM + 準備完了 + ROMをインポート + バトルモデルを作成中… + インポート中… + ROMをインポートしました。バトルモデルを作成しています。 + そのROMをインポートできませんでした + この端末では利用できるファイル選択画面がありません + 保持 + 削除 + スタジアムのバトルモデルがインストールされています。削除しますか? + スタジアムのバトルモデルを削除しました。完全に削除するにはゲームを再起動してください。 + スタジアムのモデルを削除できませんでした 画面上のコントロール 触覚フィードバック オーディオ diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 59a829f8d..a9a07ac71 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -2046,6 +2046,23 @@ 게임 데이터를 처음으로 준비하는 중 복셀 3D 기본 코어 대신 3D 복셀 엔진으로 이 게임을 플레이합니다 + 3D 엔진의 자체 설정은 게임 내 메뉴에 있습니다. + 3D 엔진 설정을 여기에 표시하려면 게임을 한 번 실행하세요. + 게임 설정 그대로 + 사운드는 3D 엔진이 관리합니다. 게임 내 메뉴의 사운드 탭을 사용하세요. + 스타디움 ROM + 준비됨 + ROM 가져오기 + 배틀 모델 생성 중… + 가져오는 중… + ROM을 가져왔습니다. 배틀 모델을 생성하고 있습니다. + 해당 ROM을 가져올 수 없습니다 + 이 기기에서 사용할 수 있는 파일 선택기가 없습니다 + 유지 + 삭제 + 스타디움 배틀 모델이 설치되어 있습니다. 삭제할까요? + 스타디움 배틀 모델을 삭제했습니다. 완전히 제거하려면 게임을 다시 시작하세요. + 스타디움 모델을 삭제할 수 없습니다 화면 컨트롤 햅틱 피드백 오디오 diff --git a/app/src/main/res/values-no/strings.xml b/app/src/main/res/values-no/strings.xml index 75bdff9b2..821f184a6 100644 --- a/app/src/main/res/values-no/strings.xml +++ b/app/src/main/res/values-no/strings.xml @@ -2044,6 +2044,23 @@ Installert bane: Forbereder spilldata for første gang Voxel 3D Spill dette spillet i 3D-voxelmotoren i stedet for standardkjernen + 3D-motorens egne innstillinger finnes i menyen i spillet. + Start spillet én gang så innstillingene for 3D-motoren vises her. + Behold som i spillet + Lyden styres av 3D-motoren. Bruk Lyd-fanen i menyen i spillet. + Stadium-ROM + Klar + Importer en ROM + Bygger kampmodeller… + Importerer… + ROM importert. Kampmodellene bygges nå. + Kunne ikke importere den ROM-en + Ingen filvelger er tilgjengelig på denne enheten + Behold + Slett + Stadium-kampmodellene er installert. Vil du slette dem? + Stadium-kampmodeller slettet. Start spillet på nytt for å fullføre fjerningen. + Kunne ikke slette Stadium-modellene Skjermkontroller Haptisk tilbakemelding LYD diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 7124a52b2..e8d042b42 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -2051,6 +2051,23 @@ Zainstalowana ścieżka: Pierwsze przygotowywanie danych gry Voxel 3D Graj w tę grę w silniku wokselowym 3D zamiast standardowego rdzenia + Własne ustawienia silnika 3D znajdują się w menu w grze. + Uruchom grę raz, aby ustawienia silnika 3D pojawiły się tutaj. + Pozostaw jak w grze + Dźwiękiem zarządza silnik 3D. Użyj karty Dźwięk w menu w grze. + ROM Stadium + Gotowe + Importuj ROM + Tworzenie modeli walki… + Importowanie… + Zaimportowano ROM. Trwa tworzenie modeli walki. + Nie udało się zaimportować tego ROM-u + Na tym urządzeniu nie ma selektora plików + Zachowaj + Usuń + Modele walki Stadium są zainstalowane. Usunąć je? + Usunięto modele walki Stadium. Uruchom grę ponownie, aby zakończyć ich usuwanie. + Nie udało się usunąć modeli Stadium Sterowanie ekranowe Wibracje dotykowe DŹWIĘK diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2b1acbbb6..2b6df0eb6 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -2045,6 +2045,23 @@ Caminho instalado: Preparando os dados do jogo pela primeira vez Voxel 3D Jogue este jogo no motor de voxels 3D em vez do núcleo padrão + As configurações do motor 3D ficam no menu do jogo. + Inicie o jogo uma vez para que as configurações do motor 3D apareçam aqui. + Manter como no jogo + O som é gerenciado pelo motor 3D. Use a aba Som no menu do jogo. + ROM do Stadium + Pronta + Importar uma ROM + Criando modelos de batalha… + Importando… + ROM importada. Os modelos de batalha estão sendo criados. + Não foi possível importar essa ROM + Nenhum seletor de arquivos disponível neste dispositivo + Manter + Excluir + Os modelos de batalha do Stadium estão instalados. Excluí-los? + Modelos de batalha do Stadium excluídos. Reinicie o jogo para concluir a remoção. + Não foi possível excluir os modelos do Stadium Controles na tela Resposta tátil ÁUDIO diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index a12861c72..50b5cbc68 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -2044,6 +2044,23 @@ Caminho instalado: A preparar os dados do jogo pela primeira vez Voxel 3D Jogar este jogo no motor de voxels 3D em vez do núcleo padrão + As definições do motor 3D estão no menu do jogo. + Inicia o jogo uma vez para que as definições do motor 3D apareçam aqui. + Manter como no jogo + O som é gerido pelo motor 3D. Usa o separador Som no menu do jogo. + ROM do Stadium + Pronta + Importar uma ROM + A criar modelos de combate… + A importar… + ROM importada. Os modelos de combate estão a ser criados. + Não foi possível importar essa ROM + Não há nenhum seletor de ficheiros neste dispositivo + Manter + Eliminar + Os modelos de combate do Stadium estão instalados. Eliminá-los? + Modelos de combate do Stadium eliminados. Reinicie o jogo para concluir a remoção. + Não foi possível eliminar os modelos do Stadium Controlos no ecrã Retorno tátil ÁUDIO diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 9a4ce7cb2..613511f62 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -2044,6 +2044,23 @@ Cale instalata: Se pregătesc datele jocului pentru prima dată Voxel 3D Joacă acest joc în motorul voxel 3D în loc de nucleul standard + Setările proprii ale motorului 3D se află în meniul din joc. + Pornește jocul o dată pentru ca setările motorului 3D să apară aici. + Lasă ca în joc + Sunetul este gestionat de motorul 3D. Folosește fila Sunet din meniul din joc. + ROM Stadium + Pregătit + Importă un ROM + Se creează modelele de luptă… + Se importă… + ROM importat. Modelele de luptă se creează acum. + Nu s-a putut importa acel ROM + Niciun selector de fișiere disponibil pe acest dispozitiv + Păstrează + Șterge + Modelele de luptă Stadium sunt instalate. Le ștergi? + Modelele de luptă Stadium au fost șterse. Repornește jocul pentru a finaliza eliminarea. + Nu s-au putut șterge modelele Stadium Comenzi pe ecran Feedback haptic AUDIO diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b0a06075b..808fb698a 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1951,6 +1951,23 @@ Первичная подготовка данных игры Воксельный 3D Играть в эту игру в воксельном 3D-движке вместо стандартного ядра + Собственные настройки 3D-движка находятся в игровом меню. + Запустите игру один раз, чтобы настройки 3D-движка появились здесь. + Оставить как в игре + Звуком управляет 3D-движок. Используйте вкладку «Звук» в игровом меню. + ROM Stadium + Готово + Импортировать ROM + Создание боевых моделей… + Импорт… + ROM импортирован. Боевые модели создаются. + Не удалось импортировать этот ROM + На этом устройстве нет средства выбора файлов + Оставить + Удалить + Боевые модели Stadium установлены. Удалить их? + Боевые модели Stadium удалены. Перезапустите игру, чтобы завершить удаление. + Не удалось удалить модели Stadium Экранные кнопки Виброотклик ЗВУК diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 82d020fbf..4ab8e1cde 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -2044,6 +2044,23 @@ Installerad sökväg: Förbereder speldata för första gången Voxel 3D Spela det här spelet i 3D-voxelmotorn i stället för standardkärnan + 3D-motorns egna inställningar finns i menyn i spelet. + Starta spelet en gång så att 3D-motorns inställningar visas här. + Behåll som i spelet + Ljudet hanteras av 3D-motorn. Använd fliken Ljud i menyn i spelet. + Stadium-ROM + Klar + Importera en ROM + Bygger stridsmodeller… + Importerar… + ROM importerad. Stridsmodellerna byggs nu. + Det gick inte att importera den ROM-filen + Ingen filväljare är tillgänglig på den här enheten + Behåll + Ta bort + Stadium-stridsmodellerna är installerade. Vill du ta bort dem? + Stadium-stridsmodeller borttagna. Starta om spelet för att slutföra borttagningen. + Det gick inte att ta bort Stadium-modellerna Skärmkontroller Haptisk återkoppling LJUD diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index af11128d8..980740169 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -2044,6 +2044,23 @@ กำลังเตรียมข้อมูลเกมเป็นครั้งแรก วอกเซล 3D เล่นเกมนี้ด้วยเอนจินวอกเซล 3D แทนคอร์มาตรฐาน + การตั้งค่าของเอนจิน 3D อยู่ในเมนูในเกม + เปิดเกมหนึ่งครั้งเพื่อให้การตั้งค่าเอนจิน 3D แสดงที่นี่ + คงค่าตามในเกม + เสียงจัดการโดยเอนจิน 3D ใช้แท็บเสียงในเมนูในเกม + ROM ของ Stadium + พร้อมแล้ว + นำเข้า ROM + กำลังสร้างโมเดลการต่อสู้… + กำลังนำเข้า… + นำเข้า ROM แล้ว กำลังสร้างโมเดลการต่อสู้ + นำเข้า ROM นั้นไม่ได้ + ไม่มีตัวเลือกไฟล์บนอุปกรณ์นี้ + เก็บไว้ + ลบ + ติดตั้งโมเดลการต่อสู้ของ Stadium แล้ว ต้องการลบหรือไม่ + ลบโมเดลการต่อสู้ของ Stadium แล้ว รีสตาร์ทเกมเพื่อลบให้เสร็จสมบูรณ์ + ลบโมเดลของ Stadium ไม่ได้ ปุ่มควบคุมบนหน้าจอ การสั่นตอบสนอง เสียง diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a0e1385cf..445fe681e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -2044,6 +2044,23 @@ Yüklü konum: Oyun verileri ilk kez hazırlanıyor Voxel 3D Bu oyunu standart çekirdek yerine 3D voxel motorunda oynayın + 3D motorun kendi ayarları oyun içi menüdedir. + 3D motor ayarlarının burada listelenmesi için oyunu bir kez başlatın. + Oyundaki gibi bırak + Ses, 3D motor tarafından yönetilir. Oyun içi menüdeki Ses sekmesini kullanın. + Stadium ROM\'u + Hazır + ROM içe aktar + Dövüş modelleri oluşturuluyor… + İçe aktarılıyor… + ROM içe aktarıldı. Dövüş modelleri şimdi oluşturuluyor. + Bu ROM içe aktarılamadı + Bu cihazda kullanılabilir dosya seçici yok + Sakla + Sil + Stadium dövüş modelleri yüklü. Silinsin mi? + Stadium dövüş modelleri silindi. Kaldırma işlemini tamamlamak için oyunu yeniden başlatın. + Stadium modelleri silinemedi Ekran kontrolleri Dokunsal Geri Bildirim SES diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 10f837e21..e64edb25c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -2051,6 +2051,23 @@ Перша підготовка даних гри Воксельний 3D Грати в цю гру у воксельному 3D-рушії замість стандартного ядра + Власні налаштування 3D-рушія містяться в ігровому меню. + Запустіть гру один раз, щоб налаштування 3D-рушія з’явилися тут. + Залишити як у грі + Звуком керує 3D-рушій. Скористайтеся вкладкою «Звук» в ігровому меню. + ROM Stadium + Готово + Імпортувати ROM + Створення бойових моделей… + Імпортування… + ROM імпортовано. Бойові моделі створюються. + Не вдалося імпортувати цей ROM + На цьому пристрої немає засобу вибору файлів + Залишити + Видалити + Бойові моделі Stadium встановлено. Видалити їх? + Бойові моделі Stadium видалено. Перезапустіть гру, щоб завершити видалення. + Не вдалося видалити моделі Stadium Екранне керування Тактильний відгук АУДІО diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0201382f1..c91f65211 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2045,6 +2045,23 @@ 正在首次准备游戏数据 体素 3D 使用 3D 体素引擎运行此游戏,而非标准核心 + 3D 引擎自身的设置位于游戏内菜单中。 + 先启动一次游戏,其 3D 引擎设置才会显示在这里。 + 保持游戏内设置 + 声音由 3D 引擎控制,请使用游戏内菜单的“声音”标签页。 + Stadium ROM + 已就绪 + 导入 ROM + 正在生成对战模型… + 正在导入… + ROM 已导入,正在生成对战模型。 + 无法导入该 ROM + 此设备上没有可用的文件选择器 + 保留 + 删除 + Stadium 对战模型已安装。要删除吗? + 已删除 Stadium 对战模型。请重启游戏以完成移除。 + 无法删除 Stadium 模型 屏幕控制 触觉反馈 音频 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 78e12d56d..1a26b4301 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2044,6 +2044,23 @@ 正在首次準備遊戲資料 體素 3D 使用 3D 體素引擎執行此遊戲,而非標準核心 + 3D 引擎本身的設定位於遊戲內選單中。 + 先啟動一次遊戲,其 3D 引擎設定才會顯示在這裡。 + 保持遊戲內設定 + 聲音由 3D 引擎控制,請使用遊戲內選單的「聲音」分頁。 + Stadium ROM + 已就緒 + 匯入 ROM + 正在產生對戰模型… + 正在匯入… + ROM 已匯入,正在產生對戰模型。 + 無法匯入該 ROM + 此裝置上沒有可用的檔案選擇器 + 保留 + 刪除 + Stadium 對戰模型已安裝。要刪除嗎? + 已刪除 Stadium 對戰模型。請重新啟動遊戲以完成移除。 + 無法刪除 Stadium 模型 螢幕控制 觸覺回饋 音訊 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a78a56807..98e0f9f37 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2160,6 +2160,23 @@ Installed path: Preparing game data for the first time Voxel 3D Play this game in the 3D voxel engine instead of the standard core + The 3D engine\'s own settings live in the in-game menu. + Launch this game once so its 3D engine settings can be listed here. + Leave as set in game + Sound is handled by the 3D engine. Use the Sound tab in the in-game menu. + Stadium ROM + Ready + Import a ROM + Building battle models… + Importing… + ROM imported. The battle models are building now. + Could not import that ROM + No file picker is available on this device + Keep + Delete + The Stadium battle models are installed. Delete them? + Stadium battle models deleted. Restart the game to finish removing them. + Could not delete the Stadium models On-screen controls Adaptive Sticks TURNIP DRIVER FLAGS