mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Android 2.6.1: fix controller remap regression, Black/OLED themes, Boot BIOS, menu scroll memory, RA file-type badge, sub-native resolution fix, achievement-sound reliability, GameDB (Tekken 4/5/Tag, Ar Tonelico 1&2, Aeon Flux, Musashi)
This commit is contained in:
@@ -2972,6 +2972,9 @@ SCCS-60002:
|
||||
SCED-50041:
|
||||
name: "Tekken Tag Tournament [Demo]"
|
||||
region: "PAL-E"
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SCED-50065:
|
||||
@@ -3282,6 +3285,9 @@ SCED-51111:
|
||||
SCED-51120:
|
||||
name: "Tekken 4 [Demo]"
|
||||
region: "PAL-E"
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SCED-51140:
|
||||
@@ -4834,6 +4840,9 @@ SCES-50001:
|
||||
name: "Tekken Tag Tournament"
|
||||
region: "PAL-M5"
|
||||
compat: 5
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SCES-50002:
|
||||
@@ -5205,6 +5214,9 @@ SCES-50878:
|
||||
name: "Tekken 4"
|
||||
region: "PAL-M5"
|
||||
compat: 5
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SCES-50885:
|
||||
@@ -7645,6 +7657,9 @@ SCKA-20045:
|
||||
name: "철권 4 [PlayStation 2 Big Hit Series]"
|
||||
name-en: "Tekken 4 [PlayStation 2 Big Hit Series]"
|
||||
region: "NTSC-K"
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SCKA-20046:
|
||||
@@ -10798,6 +10813,9 @@ SCPS-55017:
|
||||
name-en: "Tekken 4"
|
||||
region: "NTSC-J"
|
||||
compat: 3
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1
|
||||
SCPS-55018:
|
||||
@@ -11085,6 +11103,9 @@ SCPS-56002:
|
||||
name-en: "Tekken Tag Tournament"
|
||||
region: "NTSC-K"
|
||||
compat: 5
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SCPS-56003:
|
||||
@@ -11112,6 +11133,9 @@ SCPS-56006:
|
||||
name-en: "Tekken 4"
|
||||
region: "NTSC-K"
|
||||
compat: 5
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1
|
||||
SCPS-56007:
|
||||
@@ -23955,6 +23979,8 @@ SLES-53518:
|
||||
SLES-53521:
|
||||
name: "Musashi - Samurai Legend"
|
||||
region: "PAL-M4"
|
||||
gsHWFixes:
|
||||
mergeSprite: 1 # Fixes ghosting.
|
||||
SLES-53523:
|
||||
name: "Gun"
|
||||
region: "PAL-M4"
|
||||
@@ -25442,6 +25468,8 @@ SLES-53955:
|
||||
SLES-53956:
|
||||
name: "Aeon Flux"
|
||||
region: "PAL-E"
|
||||
speedHacks:
|
||||
eeCycleRate: 1 # Improves performance.
|
||||
compat: 5
|
||||
SLES-53957:
|
||||
name: "Namco Museum - 50th Anniversary"
|
||||
@@ -26089,6 +26117,8 @@ SLES-54168:
|
||||
SLES-54169:
|
||||
name: "Aeon Flux"
|
||||
region: "PAL-M4"
|
||||
speedHacks:
|
||||
eeCycleRate: 1 # Improves performance.
|
||||
SLES-54170:
|
||||
name: "Jaws Unleashed"
|
||||
region: "PAL-M5"
|
||||
@@ -27493,7 +27523,7 @@ SLES-54586:
|
||||
name: "Ar tonelico - Melody of Elemia"
|
||||
region: "PAL-E"
|
||||
gsHWFixes:
|
||||
roundSprite: 1 # Fixes vertical lines in FMVs, character portraits and other sprites.
|
||||
roundSprite: 2 # Fixes vertical lines in FMVs, character portraits and other sprites.
|
||||
SLES-54587:
|
||||
name: "Raw Danger"
|
||||
region: "PAL-E"
|
||||
@@ -55077,6 +55107,9 @@ SLPS-20015:
|
||||
name-en: "Tekken Tag Tournament"
|
||||
region: "NTSC-J"
|
||||
compat: 5
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SLPS-20016:
|
||||
@@ -58185,6 +58218,9 @@ SLPS-25100:
|
||||
name-en: "Tekken 4"
|
||||
region: "NTSC-J"
|
||||
compat: 5
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SLPS-25101:
|
||||
@@ -61251,7 +61287,7 @@ SLPS-25604:
|
||||
name-en: "Ar tonelico - Sekai no Owari de Utai Tsuzukeru Shoujo"
|
||||
region: "NTSC-J"
|
||||
gsHWFixes:
|
||||
roundSprite: 1 # Fixes vertical lines in FMVs, character portraits and other sprites.
|
||||
roundSprite: 2 # Fixes vertical lines in FMVs, character portraits and other sprites.
|
||||
SLPS-25605:
|
||||
name: "NEOGEOオンラインコレクション Vol.3 THE KING OF FIGHTERS ~オロチ編~ [通常版]"
|
||||
name-sort: "ねおじおおんらいんこれくしょん Vol. 3 ざ きんぐ おぶ ふぁいたーず おろちへん [つうじょうばん]"
|
||||
@@ -63672,6 +63708,9 @@ SLPS-71501:
|
||||
name-sort: "てっけんたっぐとーなめんと [MEGA HITS!]"
|
||||
name-en: "Tekken Tag Tournament [MEGA HITS!]"
|
||||
region: "NTSC-J"
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SLPS-71502:
|
||||
@@ -63768,6 +63807,9 @@ SLPS-73104:
|
||||
name-sort: "てっけんたっぐとーなめんと [PlayStation2 the Best]"
|
||||
name-en: "Tekken Tag Tournament [PlayStation2 the Best]"
|
||||
region: "NTSC-J"
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SLPS-73105:
|
||||
@@ -63904,6 +63946,9 @@ SLPS-73209:
|
||||
name-sort: "てっけん4 [PlayStation2 the Best]"
|
||||
name-en: "Tekken 4 [PlayStation2 the Best]"
|
||||
region: "NTSC-J"
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SLPS-73210:
|
||||
@@ -64296,7 +64341,7 @@ SLPS-73249:
|
||||
name-en: "Ar tonelico - Sekai no Owari de Utai Tsuzukeru Shoujo [PlayStation2 the Best]"
|
||||
region: "NTSC-J"
|
||||
gsHWFixes:
|
||||
roundSprite: 1 # Fixes vertical lines in FMVs, character portraits and other sprites.
|
||||
roundSprite: 2 # Fixes vertical lines in FMVs, character portraits and other sprites.
|
||||
SLPS-73250:
|
||||
name: "エースコンバット・ゼロ・ザ・ベルカン・ウォー [PlayStation2 the Best]"
|
||||
name-sort: "えーすこんばっと・ぜろ・ざ・べるかん・うぉー [PlayStation2 the Best]"
|
||||
@@ -64673,6 +64718,9 @@ SLUS-20001:
|
||||
name: "Tekken Tag Tournament"
|
||||
region: "NTSC-U"
|
||||
compat: 5
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SLUS-20002:
|
||||
@@ -66078,6 +66126,9 @@ SLUS-20328:
|
||||
name: "Tekken 4"
|
||||
region: "NTSC-U"
|
||||
compat: 5
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SLUS-20329:
|
||||
@@ -69698,6 +69749,8 @@ SLUS-20983:
|
||||
name: "Musashi Samurai Legend"
|
||||
region: "NTSC-U"
|
||||
compat: 5
|
||||
gsHWFixes:
|
||||
mergeSprite: 1 # Fixes ghosting.
|
||||
SLUS-20984:
|
||||
name: "Resident Evil Outbreak - File #2"
|
||||
region: "NTSC-U"
|
||||
@@ -71080,6 +71133,8 @@ SLUS-21204:
|
||||
SLUS-21205:
|
||||
name: "Aeon Flux"
|
||||
region: "NTSC-U"
|
||||
speedHacks:
|
||||
eeCycleRate: 1 # Improves performance.
|
||||
compat: 5
|
||||
SLUS-21206:
|
||||
name: "Shining Force Neo"
|
||||
@@ -72811,7 +72866,7 @@ SLUS-21445:
|
||||
region: "NTSC-U"
|
||||
compat: 5
|
||||
gsHWFixes:
|
||||
roundSprite: 1 # Fixes vertical lines in FMVs, character portraits and other sprites.
|
||||
roundSprite: 2 # Fixes vertical lines in FMVs, character portraits and other sprites.
|
||||
SLUS-21446:
|
||||
name: "Family Feud"
|
||||
region: "NTSC-U"
|
||||
@@ -75493,6 +75548,9 @@ SLUS-28010:
|
||||
SLUS-28012:
|
||||
name: "Tekken 4 [Trade Demo]"
|
||||
region: "NTSC-U"
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SLUS-28013:
|
||||
@@ -75787,6 +75845,9 @@ SLUS-29033:
|
||||
SLUS-29034:
|
||||
name: "Tekken 4 [Regular Demo]"
|
||||
region: "NTSC-U"
|
||||
clampModes:
|
||||
eeClampMode: 3 # Fixes camera and stops constant coin noises on Pirates Cove.
|
||||
vu1ClampMode: 2 # Fixes broken effects.
|
||||
gsHWFixes:
|
||||
alignSprite: 1 # Fixes vertical lines.
|
||||
SLUS-29035:
|
||||
|
||||
@@ -222,6 +222,8 @@ val EN: Map<String, String> = mapOf(
|
||||
"app.theme.system" to "System",
|
||||
"app.theme.light" to "Light",
|
||||
"app.theme.dark" to "Dark",
|
||||
"app.theme.black" to "Black",
|
||||
"app.theme.oled" to "OLED",
|
||||
"app.bootLogo" to "Boot animation",
|
||||
"app.bootLogo.desc" to "Play the ARMSX2 intro video when the app starts.",
|
||||
"app.toolbarPosition" to "Library toolbar",
|
||||
@@ -1043,6 +1045,7 @@ val EN: Map<String, String> = mapOf(
|
||||
"setup.step.appData.title" to "App Data Folder",
|
||||
"setup.step.bios.description" to "Pick a folder of PS2 BIOS files to start playing — every BIOS inside is added.",
|
||||
"setup.step.bios.title" to "BIOS Location",
|
||||
"bios.boot.title" to "Boot BIOS",
|
||||
"setup.step.rom.description" to "Pick one or more folders where you keep your PS2 games. Supports ISO, CHD, BIN, IMG, MDF, and GZ.",
|
||||
"setup.step.rom.title" to "ROM Location",
|
||||
"setup.storageChooser.customFolder" to "Custom Folder…",
|
||||
|
||||
@@ -752,6 +752,13 @@ object ControllerMappings {
|
||||
// consume (B = exit) — can be captured. Normal nav resumes when it clears.
|
||||
val padCapturing = mutableStateOf(false)
|
||||
|
||||
/** A pad-button capture in progress from the Controls screen (ControllerManagerScreen). While
|
||||
* set, MainActivityRuntime.dispatchKeyEvent binds the next pressed keycode via this lambda —
|
||||
* handled there, like [captureHotkey], instead of a focus-stealing AlertDialog whose separate
|
||||
* window swallowed controller keys before Compose's onPreviewKeyEvent could see them (the
|
||||
* 2.6.0 "can't remap buttons" bug). The lambda binds the key and returns true. */
|
||||
val capturePadAction = mutableStateOf<((Int) -> Boolean)?>(null)
|
||||
|
||||
// Capture bridge: the Hotkeys tab calls [beginHotkeyCapture]; the next
|
||||
// button(s) seen by MainActivityRuntime.dispatchKeyEvent are bound to it. Press one button
|
||||
// for a single bind, or two together for a combo. Observed for UI feedback.
|
||||
|
||||
@@ -64,8 +64,11 @@ private val TrophyGold = Color(0xFFFFC93C)
|
||||
private data class DrawerItem(
|
||||
val titleKey: String,
|
||||
val glyph: String,
|
||||
val destination: AppRoute,
|
||||
// A nav destination OR an action (onAction). Action rows (e.g. Boot BIOS) run onAction and
|
||||
// close the drawer instead of navigating to a screen.
|
||||
val destination: AppRoute? = null,
|
||||
val iconRes: Int? = null,
|
||||
val onAction: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -111,17 +114,20 @@ fun NavigationDrawer(
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.55f)),
|
||||
shadowElevation = 18.dp,
|
||||
) {
|
||||
DrawerContent(selected, onNavigate)
|
||||
DrawerContent(selected, onNavigate, onDismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DrawerContent(selected: AppRoute, onNavigate: (AppRoute) -> Unit) {
|
||||
private fun DrawerContent(selected: AppRoute, onNavigate: (AppRoute) -> Unit, onDismiss: () -> Unit) {
|
||||
val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
val primary = listOf(
|
||||
DrawerItem("games.section.library", "▦", AppRoute.Home),
|
||||
// Boot straight into the PS2 system BIOS with no disc — distinct from "BIOS Location"
|
||||
// below, which only points the emulator at your BIOS file.
|
||||
DrawerItem("bios.boot.title", "▶", onAction = { MainActivityRuntime.startBios(); onDismiss() }),
|
||||
DrawerItem("ra.title", "★", AppRoute.Achievements, iconRes = com.armsx2.R.drawable.ic_trophy),
|
||||
DrawerItem("action.settings", "⚙", AppRoute.Settings()),
|
||||
)
|
||||
@@ -175,12 +181,12 @@ private fun DrawerSection(
|
||||
)
|
||||
items.forEachIndexed { index, item ->
|
||||
DrawerRow(
|
||||
controllerId = "drawer.${item.destination::class.simpleName}",
|
||||
controllerId = "drawer.${item.destination?.let { it::class.simpleName } ?: item.titleKey}",
|
||||
title = str(item.titleKey),
|
||||
glyph = item.glyph,
|
||||
iconRes = item.iconRes,
|
||||
selected = sameDestination(selected, item.destination),
|
||||
onClick = { onNavigate(item.destination) },
|
||||
selected = item.destination != null && sameDestination(selected, item.destination),
|
||||
onClick = { item.onAction?.invoke() ?: item.destination?.let(onNavigate) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1515,7 +1515,9 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK !=
|
||||
Configuration.UI_MODE_NIGHT_YES
|
||||
com.armsx2.ui.theme.ThemeMode.Light -> true
|
||||
com.armsx2.ui.theme.ThemeMode.Dark -> false
|
||||
com.armsx2.ui.theme.ThemeMode.Dark,
|
||||
com.armsx2.ui.theme.ThemeMode.Black,
|
||||
com.armsx2.ui.theme.ThemeMode.Oled -> false
|
||||
}
|
||||
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
||||
controller.show(WindowInsetsCompat.Type.systemBars())
|
||||
@@ -1585,7 +1587,9 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
val darkTheme = when (com.armsx2.ui.theme.ThemePreferences.mode.value) {
|
||||
com.armsx2.ui.theme.ThemeMode.System -> androidx.compose.foundation.isSystemInDarkTheme()
|
||||
com.armsx2.ui.theme.ThemeMode.Light -> false
|
||||
com.armsx2.ui.theme.ThemeMode.Dark -> true
|
||||
com.armsx2.ui.theme.ThemeMode.Dark,
|
||||
com.armsx2.ui.theme.ThemeMode.Black,
|
||||
com.armsx2.ui.theme.ThemeMode.Oled -> true
|
||||
}
|
||||
androidx.compose.runtime.SideEffect {
|
||||
applySystemBarTheme(darkTheme = darkTheme, showSystemBars = showSystemBars)
|
||||
@@ -1901,6 +1905,18 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
}
|
||||
return true // swallow down + up while capturing
|
||||
}
|
||||
// Pad-button capture (Controls screen): bind here — like the hotkey capture above —
|
||||
// instead of via Compose's onPreviewKeyEvent, because the bind prompt is no longer a
|
||||
// focus-stealing dialog (a Dialog window would swallow controller keys before Compose or
|
||||
// this handler saw them — the 2.6.0 "can't remap buttons" bug). Bind the first real key
|
||||
// press; swallow down+up so nav (B = exit, A = confirm) can't fire mid-capture.
|
||||
val padCapture = ControllerMappings.capturePadAction.value
|
||||
if (padCapture != null) {
|
||||
if (kc != KeyEvent.KEYCODE_UNKNOWN && event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
|
||||
padCapture(kc)
|
||||
}
|
||||
return true
|
||||
}
|
||||
// Pad-button capture (Pad tab): let every key fall through to Compose's
|
||||
// onPreviewKeyEvent so ANY button binds — without this the overlay nav
|
||||
// below eats B (exit), A (confirm), Y, D-pad and L1/R1 before they reach
|
||||
|
||||
@@ -15,6 +15,11 @@ object InGameOverlay {
|
||||
val hardcoreOn = mutableStateOf(false)
|
||||
val frameLimitOn = mutableStateOf(true)
|
||||
|
||||
/** Per-tab scroll offset (px) of the in-game pause menu, retained across menu open/close so
|
||||
* reopening a tab — especially the long Fixes list — returns to where you were instead of
|
||||
* snapping back to the top. Keyed by EmulationMenuTab.name to avoid coupling to that enum. */
|
||||
val menuTabScroll = HashMap<String, Int>()
|
||||
|
||||
fun saveSettings(updated: Settings) {
|
||||
val previous = settingsState.value
|
||||
settingsState.value = updated
|
||||
|
||||
+6
-10
@@ -18,7 +18,6 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Surface
|
||||
@@ -88,14 +87,11 @@ fun ControllerManagerScreen(onBack: () -> Unit, viewModel: ControllerManagerView
|
||||
}
|
||||
}
|
||||
|
||||
state.capturingAction?.let { action ->
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::cancelCapture,
|
||||
title = { Text("${str("pad.action.bind")}: ${action.label}") },
|
||||
text = { Text(str("hotkeys.capturePrompt")) },
|
||||
confirmButton = { TextButton(onClick = viewModel::cancelCapture) { Text(str("action.cancel")) } },
|
||||
)
|
||||
}
|
||||
// No capture dialog. A Compose AlertDialog is a separate focused WINDOW that swallowed all
|
||||
// controller key events before either this Activity's dispatchKeyEvent or the main Compose
|
||||
// tree could see them — so no button ever bound (the 2.6.0 "can't remap buttons" bug). The
|
||||
// bind prompt now shows inline on the row (highlight + "press a button" text, like hotkeys),
|
||||
// and the key is captured in MainActivityRuntime.dispatchKeyEvent via capturePadAction.
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -163,7 +159,7 @@ private fun ControllerBindings(state: ControllerManagerUiState, viewModel: Contr
|
||||
label = action.label,
|
||||
binding = ControllerMappings.labelForKey(ControllerMappings.physicalFor(action, state.player)),
|
||||
capturing = state.capturingAction == action,
|
||||
onBind = { viewModel.beginCapture(action) },
|
||||
onBind = { if (state.capturingAction == action) viewModel.cancelCapture() else viewModel.beginCapture(action) },
|
||||
onClear = { viewModel.clear(action) },
|
||||
)
|
||||
}
|
||||
|
||||
+5
@@ -41,6 +41,9 @@ class ControllerManagerViewModel(application: Application) : AndroidViewModel(ap
|
||||
|
||||
fun beginCapture(action: ControllerMappings.Action) {
|
||||
ControllerMappings.padCapturing.value = true
|
||||
// Bind via the Activity's dispatchKeyEvent (see ControllerMappings.capturePadAction) so any
|
||||
// button binds even though the bind prompt is no longer a focus-stealing dialog.
|
||||
ControllerMappings.capturePadAction.value = { kc -> captureKey(kc) }
|
||||
state.value = state.value.copy(capturingAction = action)
|
||||
}
|
||||
|
||||
@@ -48,12 +51,14 @@ class ControllerManagerViewModel(application: Application) : AndroidViewModel(ap
|
||||
val action = state.value.capturingAction ?: return false
|
||||
ControllerMappings.bind(action, keyCode, state.value.player)
|
||||
ControllerMappings.padCapturing.value = false
|
||||
ControllerMappings.capturePadAction.value = null
|
||||
state.value = state.value.copy(capturingAction = null, tick = state.value.tick + 1)
|
||||
return true
|
||||
}
|
||||
|
||||
fun cancelCapture() {
|
||||
ControllerMappings.padCapturing.value = false
|
||||
ControllerMappings.capturePadAction.value = null
|
||||
state.value = state.value.copy(capturingAction = null)
|
||||
}
|
||||
|
||||
|
||||
@@ -205,7 +205,16 @@ private fun MenuPage(
|
||||
modifier: Modifier,
|
||||
) {
|
||||
val tabScrollStates = remember {
|
||||
EmulationMenuTab.entries.associateWith { ScrollState(initial = 0) }
|
||||
EmulationMenuTab.entries.associateWith {
|
||||
ScrollState(initial = InGameOverlay.menuTabScroll[it.name] ?: 0)
|
||||
}
|
||||
}
|
||||
// Remember each tab's scroll offset when the menu closes so reopening a tab (especially
|
||||
// the long Fixes list) returns to where you were instead of snapping back to the top.
|
||||
androidx.compose.runtime.DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
tabScrollStates.forEach { (tab, ss) -> InGameOverlay.menuTabScroll[tab.name] = ss.value }
|
||||
}
|
||||
}
|
||||
val scrollState = tabScrollStates.getValue(state.tab)
|
||||
// Provide the pane's scroll state to the settings widgets so the Fixes pane's
|
||||
@@ -432,6 +441,12 @@ private fun MenuHeader(compact: Boolean, hardcore: Boolean, richPresence: String
|
||||
Spacer(Modifier.width(8.dp))
|
||||
HardcoreBadge()
|
||||
}
|
||||
// File-type chip after the HC badge (ISO / CHD / …), mirroring the library
|
||||
// list view so the pause/RA header shows the same at-a-glance file info.
|
||||
game?.let { g ->
|
||||
Spacer(Modifier.width(6.dp))
|
||||
com.armsx2.ui.common.StatusChip(g.extension.ifBlank { g.platform.key.uppercase() })
|
||||
}
|
||||
}
|
||||
if (!game?.serial.isNullOrBlank()) {
|
||||
Spacer(Modifier.height(2.dp))
|
||||
|
||||
+4
-1
@@ -187,7 +187,10 @@ class EmulationMenuViewModel(application: Application) : AndroidViewModel(applic
|
||||
}
|
||||
|
||||
fun setUpscale(value: Float) {
|
||||
val normalized = value.coerceIn(1f, 8f)
|
||||
// Allow sub-native (0.25x–0.75x, issue #207) — the in-game menu offers them via
|
||||
// UPSCALE_OPTIONS, so don't clamp them up to Native like the old 1f floor did (that
|
||||
// made every below-Native pick silently apply as Native). Matches the settings tab.
|
||||
val normalized = value.coerceIn(0.25f, 8f)
|
||||
updateSettings { it.copy(upscaleFloat = normalized) }
|
||||
MainActivityRuntime.upscale.value = normalized
|
||||
NativeApp.renderUpscalemultiplier(normalized)
|
||||
|
||||
@@ -73,17 +73,24 @@ fun AppTab() {
|
||||
|
||||
SegmentedRow(
|
||||
label = str("app.theme"),
|
||||
options = listOf(str("app.theme.system"), str("app.theme.light"), str("app.theme.dark")),
|
||||
options = listOf(
|
||||
str("app.theme.system"), str("app.theme.light"), str("app.theme.dark"),
|
||||
str("app.theme.black"), str("app.theme.oled"),
|
||||
),
|
||||
selectedIndex = when (ThemePreferences.mode.value) {
|
||||
ThemeMode.System -> 0
|
||||
ThemeMode.Light -> 1
|
||||
ThemeMode.Dark -> 2
|
||||
ThemeMode.Black -> 3
|
||||
ThemeMode.Oled -> 4
|
||||
},
|
||||
onChange = { index ->
|
||||
ThemePreferences.set(
|
||||
when (index) {
|
||||
1 -> ThemeMode.Light
|
||||
2 -> ThemeMode.Dark
|
||||
3 -> ThemeMode.Black
|
||||
4 -> ThemeMode.Oled
|
||||
else -> ThemeMode.System
|
||||
},
|
||||
)
|
||||
|
||||
@@ -67,6 +67,13 @@ import com.armsx2.ui.settings.LocalSettingsScrollState
|
||||
|
||||
private data class SettingsSection(val category: SettingsCategory, val titleKey: String, val glyph: String)
|
||||
|
||||
/** Retains the settings page's scroll offset across close/reopen. The selected category already
|
||||
* persists in the view-model, so restoring this one offset returns you to where you were (esp.
|
||||
* useful in long lists like Fixes) instead of snapping to the top every time. */
|
||||
private object SettingsScrollMemory {
|
||||
var lastOffset = 0
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
initialCategory: SettingsCategory,
|
||||
@@ -91,7 +98,10 @@ fun SettingsScreen(
|
||||
pendingJump = label
|
||||
}
|
||||
}
|
||||
val screenScroll = rememberScrollState()
|
||||
val screenScroll = rememberScrollState(initial = SettingsScrollMemory.lastOffset)
|
||||
androidx.compose.runtime.DisposableEffect(Unit) {
|
||||
onDispose { SettingsScrollMemory.lastOffset = screenScroll.value }
|
||||
}
|
||||
LaunchedEffect(initialCategory, game?.uri) { viewModel.load(initialCategory, game) }
|
||||
// When the controller focus returns to the category-chip row (the top-most
|
||||
// navigable element), snap the whole page back to the top so the title bar +
|
||||
|
||||
@@ -10,7 +10,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import com.armsx2.runtime.MainActivityRuntime
|
||||
import androidx.core.content.edit
|
||||
|
||||
enum class ThemeMode { System, Dark, Light }
|
||||
enum class ThemeMode { System, Dark, Light, Black, Oled }
|
||||
|
||||
object ThemePreferences {
|
||||
private const val PreferenceKey = "ui.theme.mode"
|
||||
@@ -21,6 +21,8 @@ object ThemePreferences {
|
||||
mode.value = when (MainActivityRuntime.prefs.getString(PreferenceKey, ThemeMode.System.name)) {
|
||||
ThemeMode.System.name -> ThemeMode.System
|
||||
ThemeMode.Light.name -> ThemeMode.Light
|
||||
ThemeMode.Black.name -> ThemeMode.Black
|
||||
ThemeMode.Oled.name -> ThemeMode.Oled
|
||||
else -> ThemeMode.Dark
|
||||
}
|
||||
}
|
||||
@@ -139,15 +141,43 @@ private val DayScheme = lightColorScheme(
|
||||
surfaceTint = Color.Transparent,
|
||||
)
|
||||
|
||||
// Neutral dark schemes derived from Night — same accents, but black/neutral-grey backgrounds and
|
||||
// selection highlights instead of Night's blue-tinted ones (users asked for a black, not-blue UI).
|
||||
// Black = a slightly-lifted neutral dark; Oled = true #000000 for OLED panels (deepest black, less
|
||||
// power). Only surfaces/containers/outlines change; the primary accent stays for contrast/usability.
|
||||
private val BlackScheme = NightScheme.copy(
|
||||
background = Color(0xFF0B0B0B),
|
||||
surface = Color(0xFF111111),
|
||||
surfaceVariant = Color(0xFF1B1B1B),
|
||||
primaryContainer = Color(0xFF242424),
|
||||
onPrimaryContainer = Color(0xFFEAEAEA),
|
||||
secondaryContainer = Color(0xFF1A1A1A),
|
||||
outline = Color(0xFF333333),
|
||||
outlineVariant = Color(0xFF242424),
|
||||
)
|
||||
|
||||
private val OledScheme = NightScheme.copy(
|
||||
background = Color.Black,
|
||||
surface = Color.Black,
|
||||
surfaceVariant = Color(0xFF0E0E0E),
|
||||
primaryContainer = Color(0xFF1A1A1A),
|
||||
onPrimaryContainer = Color(0xFFEAEAEA),
|
||||
secondaryContainer = Color(0xFF121212),
|
||||
outline = Color(0xFF262626),
|
||||
outlineVariant = Color(0xFF161616),
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun Armsx2Theme(content: @Composable () -> Unit) {
|
||||
val dark = when (ThemePreferences.mode.value) {
|
||||
ThemeMode.System -> isSystemInDarkTheme()
|
||||
ThemeMode.Dark -> true
|
||||
ThemeMode.Light -> false
|
||||
val scheme = when (ThemePreferences.mode.value) {
|
||||
ThemeMode.System -> if (isSystemInDarkTheme()) NightScheme else DayScheme
|
||||
ThemeMode.Dark -> NightScheme
|
||||
ThemeMode.Light -> DayScheme
|
||||
ThemeMode.Black -> BlackScheme
|
||||
ThemeMode.Oled -> OledScheme
|
||||
}
|
||||
MaterialTheme(
|
||||
colorScheme = if (dark) NightScheme else DayScheme,
|
||||
colorScheme = scheme,
|
||||
typography = ArmsTypography,
|
||||
content = content,
|
||||
)
|
||||
|
||||
@@ -336,21 +336,35 @@ public class NativeApp {
|
||||
// Uses MediaPlayer, not SoundPool: SoundPool decoded/resampled the 44.1 kHz
|
||||
// stereo PCM oddly and it came out "weird". MediaPlayer plays the .wav straight,
|
||||
// matching desktop PCSX2. One short-lived player per shot, released on complete.
|
||||
// Strong references to the currently-playing sound players. Without this the
|
||||
// MediaPlayer below is a pure local; once start() returns and the worker thread
|
||||
// exits, nothing roots it, so the GC (very active under a running emulator) could
|
||||
// finalize and release it MID-PLAYBACK — that's why unlock sounds dropped at random
|
||||
// with no error logged. Held from before start() until the completion/error callback.
|
||||
private static final java.util.Set<android.media.MediaPlayer> sActiveSounds =
|
||||
java.util.Collections.synchronizedSet(new java.util.HashSet<>());
|
||||
|
||||
public static void playSound(String path) {
|
||||
if (path == null || path.isEmpty()) return;
|
||||
// Cap concurrent players — a burst of simultaneous unlocks (combo/milestone) could
|
||||
// otherwise exhaust the device's MediaPlayer/codec pool and make start() no-op.
|
||||
if (sActiveSounds.size() >= 4) return;
|
||||
new Thread(() -> {
|
||||
android.media.MediaPlayer mp = null;
|
||||
try {
|
||||
android.media.MediaPlayer mp = new android.media.MediaPlayer();
|
||||
mp = new android.media.MediaPlayer();
|
||||
mp.setAudioAttributes(new android.media.AudioAttributes.Builder()
|
||||
.setUsage(android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
|
||||
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.build());
|
||||
mp.setDataSource(path);
|
||||
mp.setOnCompletionListener(m -> { try { m.release(); } catch (Throwable ignore) {} });
|
||||
mp.setOnErrorListener((m, what, extra) -> { try { m.release(); } catch (Throwable ignore) {} return true; });
|
||||
mp.setOnCompletionListener(m -> { sActiveSounds.remove(m); try { m.release(); } catch (Throwable ignore) {} });
|
||||
mp.setOnErrorListener((m, what, extra) -> { sActiveSounds.remove(m); try { m.release(); } catch (Throwable ignore) {} return true; });
|
||||
sActiveSounds.add(mp);
|
||||
mp.prepare();
|
||||
mp.start();
|
||||
} catch (Throwable t) {
|
||||
if (mp != null) { sActiveSounds.remove(mp); try { mp.release(); } catch (Throwable ignore) {} }
|
||||
android.util.Log.e("ARMSX2", "playSound failed: " + path, t);
|
||||
}
|
||||
}, "armsx2-ra-sound").start();
|
||||
|
||||
Reference in New Issue
Block a user