From 0cdf8678edff6e4c27a8eec518a8ce4a04571e6e Mon Sep 17 00:00:00 2001 From: thisishuey Date: Sun, 11 Oct 2026 21:03:46 +0000 Subject: [PATCH 1/2] Calibrate Dolphin's sticks so they stop at the GameCube gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dolphin passes an uncalibrated stick straight through, so a full tilt reaches GameCube ±127, where a real pad's gate stops near ±101 on the main stick and ±92 on the C-stick. Every push lands about a quarter further than on hardware, so walking slowly or aiming finely is hard: a light push already runs, and a scope overshoots. Dolphin for Android has no Calibrate button, and nothing else sets it. Dolphin setup now writes `Calibration = 100.00`, a full circle, for each stick it binds. Dolphin then maps the stick's rim onto its default Gate Size octagon: 79.37% for the main stick and 72.21% for the C-stick, values Dolphin averaged from genuine controllers. A full push still reaches the real gate. Sources are Dolphin's GCPadEmu.h and StickGate.cpp, and Dolphin PR #8963. Setup still leaves a Pro Controller player's section to the player. It only adds the two calibration keys there, keeping the bindings they made, and only to a section that already exists. A new Dolphin section in Settings has a Calibrate sticks switch. It's on by default, for anyone who prefers the old full-range feel, and takes effect at the next Set up. The README covers the setting and adds a troubleshooting row. Tested on a Retroid Pocket Nova (Android 13) with NSO GameCube controllers, using the GameCube controller support from #30: walking and scope aiming feel like an original controller. Joy-Con 2 and Pro Controllers are untested. The hand-mapped step skips a GameCube controller player, since setup writes their whole section. Co-Authored-By: Claude Opus 5.5 --- README.md | 5 +++ .../com/joegec/joycon2android/AppContainer.kt | 4 ++ .../com/joegec/joycon2android/MainActivity.kt | 8 +++- .../joycon2android/emulator/EmulatorSetup.kt | 4 +- .../emulatorconfig/IniEditor.kt | 3 ++ docs/architecture.md | 2 +- docs/virtual-gamepad.md | 17 ++++++++ .../gamepad/emulator/DolphinGcpadConfig.kt | 34 ++++++++++++++-- .../emulator/DolphinGcpadConfigTest.kt | 39 +++++++++++++++++++ .../settings/OutputSettingsDataStore.kt | 6 +++ .../joycon2android/settings/OutputSettings.kt | 1 + .../settings/OutputSettingsRepository.kt | 1 + .../SetCalibrateDolphinSticksUseCase.kt | 5 +++ .../settings/presentation/DolphinSetting.kt | 33 ++++++++++++++++ .../settings/presentation/SettingsPanel.kt | 4 ++ .../presentation/SettingsViewModel.kt | 6 +++ .../src/main/res/values/strings.xml | 3 ++ 17 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/SetCalibrateDolphinSticksUseCase.kt create mode 100644 feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/DolphinSetting.kt diff --git a/README.md b/README.md index eca468c6..3c7188f6 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,10 @@ The gear at the top right opens the settings panel: smoother motion and quicker buttons, at a battery cost. Applies while the virtual gamepad or DSU is on. - **Ignore this device's motion in Eden** (on by default, needs Shizuku) — stops Eden mixing this device's gyro into Player 1. Lifted when the virtual gamepad and DSU are both off. +- **Dolphin → Calibrate sticks** (on by default) — Dolphin setup calibrates the sticks so they stop + where a real GameCube controller's do; without it, small movements feel twitchy. Run Set up again + after changing it. A Pro Controller you mapped by hand in Dolphin keeps its mapping and only gains + the calibration. ### 5. Set up your emulator @@ -258,6 +262,7 @@ shoulder buttons. | Motion aiming stutters | Settings (gear, top right): keep **Ignore this device's motion in Eden** and **Faster controller updates** on | | Tilting the device moves the aim in Eden | Turn on **Ignore this device's motion in Eden** | | Pointer drifts or starts off-screen | Rest the controller for ~2 s, then press Recenter | +| Sticks feel twitchy in Dolphin (hard to walk or aim) | Keep Settings → Dolphin → **Calibrate sticks** on and tap **Set up** for Dolphin again. A Pro Controller: map it in Dolphin first, then tap **Set up** once to add the calibration | | MotionPlus tutorial replays every boot (Dolphin) | Set `MPLS.MOVIE` in `Wii/shared2/sys/SYSCONF` with Dolphin closed ([why](docs/dsu-motion.md#motionplus-tutorial-replays)) | ## Roadmap and feedback diff --git a/app/src/main/java/com/joegec/joycon2android/AppContainer.kt b/app/src/main/java/com/joegec/joycon2android/AppContainer.kt index 945753f6..f84a13e9 100644 --- a/app/src/main/java/com/joegec/joycon2android/AppContainer.kt +++ b/app/src/main/java/com/joegec/joycon2android/AppContainer.kt @@ -77,6 +77,7 @@ import com.joegec.joycon2android.settings.ObserveOutputSettingsUseCase import com.joegec.joycon2android.settings.OutputSettingsDataStore import com.joegec.joycon2android.settings.OutputSettingsRepository import com.joegec.joycon2android.settings.SetBlockDeviceMotionUseCase +import com.joegec.joycon2android.settings.SetCalibrateDolphinSticksUseCase import com.joegec.joycon2android.settings.SetDeviceMotionBlockedUseCase import com.joegec.joycon2android.settings.SetFasterUpdatesUseCase import com.joegec.joycon2android.emulator.EdenDeviceMotionBlocker @@ -111,6 +112,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import java.io.File @@ -188,6 +190,7 @@ class AppContainer(context: Context) { val observeOutputSettings = ObserveOutputSettingsUseCase(outputSettings) val setFasterUpdates = SetFasterUpdatesUseCase(outputSettings) val setBlockDeviceMotion = SetBlockDeviceMotionUseCase(outputSettings) + val setCalibrateDolphinSticks = SetCalibrateDolphinSticksUseCase(outputSettings) // --- Assignment --- val assignmentRepository: AssignmentRepository = PlayerAssignmentManager() @@ -214,6 +217,7 @@ class AppContainer(context: Context) { gamepadControllerNumbers = { dolphinGamepadIds(appContext) }, getControllerMapping = getControllerMapping, getRemoteMotion = getRemoteMotion, + calibrateDolphinSticks = { observeOutputSettings().first().calibrateDolphinSticks }, ) val emulatorLauncher = EmulatorLauncher(appContext) diff --git a/app/src/main/java/com/joegec/joycon2android/MainActivity.kt b/app/src/main/java/com/joegec/joycon2android/MainActivity.kt index 42de0ab3..239ebebd 100644 --- a/app/src/main/java/com/joegec/joycon2android/MainActivity.kt +++ b/app/src/main/java/com/joegec/joycon2android/MainActivity.kt @@ -91,7 +91,12 @@ class MainActivity : ComponentActivity() { viewModelFactory { initializer { val c = (application as JoyconApplication).container - SettingsViewModel(c.observeOutputSettings, c.setFasterUpdates, c.setBlockDeviceMotion) + SettingsViewModel( + c.observeOutputSettings, + c.setFasterUpdates, + c.setBlockDeviceMotion, + c.setCalibrateDolphinSticks, + ) } } } @@ -274,6 +279,7 @@ class MainActivity : ComponentActivity() { onViewModeChange = viewModel::setViewMode, onFasterUpdatesToggle = settingsViewModel::toggleFasterUpdates, onBlockDeviceMotionToggle = settingsViewModel::toggleBlockDeviceMotion, + onCalibrateDolphinSticksToggle = settingsViewModel::toggleCalibrateDolphinSticks, ) }, containerColor = Background, diff --git a/app/src/main/java/com/joegec/joycon2android/emulator/EmulatorSetup.kt b/app/src/main/java/com/joegec/joycon2android/emulator/EmulatorSetup.kt index 19713cc4..cc11be70 100644 --- a/app/src/main/java/com/joegec/joycon2android/emulator/EmulatorSetup.kt +++ b/app/src/main/java/com/joegec/joycon2android/emulator/EmulatorSetup.kt @@ -38,6 +38,7 @@ class EmulatorSetup( private val gamepadControllerNumbers: () -> Map, private val getControllerMapping: GetEffectiveControllerMappingUseCase, private val getRemoteMotion: GetRemoteMotionUseCase, + private val calibrateDolphinSticks: suspend () -> Boolean, ) { // Read up front: the generators are synchronous. @@ -163,7 +164,8 @@ class EmulatorSetup( shell.readText(DolphinGcpadConfig.path), players, gamepadControllerNumbers(), - mappingLookup(Console.GAMECUBE, players), + calibrateSticks = calibrateDolphinSticks(), + mappingFor = mappingLookup(Console.GAMECUBE, players), ) val mappingsOk = shell.writeText(DolphinGcpadConfig.path, mappings) // Dolphin's GameCube ports default to None. diff --git a/core/emulatorconfig/src/main/kotlin/com/joegec/joycon2android/emulatorconfig/IniEditor.kt b/core/emulatorconfig/src/main/kotlin/com/joegec/joycon2android/emulatorconfig/IniEditor.kt index c5e711b6..39f70490 100644 --- a/core/emulatorconfig/src/main/kotlin/com/joegec/joycon2android/emulatorconfig/IniEditor.kt +++ b/core/emulatorconfig/src/main/kotlin/com/joegec/joycon2android/emulatorconfig/IniEditor.kt @@ -24,6 +24,9 @@ object IniEditor { return out.toString() } + fun hasSection(existing: String?, section: String): Boolean = + existing?.lines()?.any { it.trim() == section } == true + fun valueOf(existing: String?, section: String, key: String): String? { val lines = existing?.lines() ?: return null val headerIndex = lines.indexOfFirst { it.trim() == section } diff --git a/docs/architecture.md b/docs/architecture.md index 29ff90b3..91b12698 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -142,7 +142,7 @@ One **ViewModel per feature**, in its presentation module, built in `MainActivit - `GamepadViewModel` — gamepad status, Shizuku availability and emulator auto setup. - `UpdateViewModel` — the once-per-launch release check and the update prompt. - `SettingsViewModel` — the settings that apply to whichever output runs (faster updates, the Eden - motion block). The panel's layout choice stays with `Joycon2ViewModel`, which renders it. + motion block, Dolphin stick calibration). The panel's layout choice stays with `Joycon2ViewModel`, which renders it. - `ControllerMappingViewModel` (in `:core:buttonmapping:presentation`) — the button-mapping editor. - `Joycon2ViewModel` (in `:app`) — the app-level host: the coordinator's session `uiState` (genuinely cross-feature), BLE permissions, scan/assign/disconnect, and the service binding. diff --git a/docs/virtual-gamepad.md b/docs/virtual-gamepad.md index 072e069c..b7920660 100644 --- a/docs/virtual-gamepad.md +++ b/docs/virtual-gamepad.md @@ -95,6 +95,23 @@ mapping of the console each output's card is set to, while that output runs. - **Android handles `HOME` itself** and never passes it to the app in front, so a bound Home always leaves the game. +## Dolphin stick calibration + +Dolphin setup writes `Main Stick/Calibration = 100.00` and `C-Stick/Calibration = 100.00` for each +stick it binds, unless Settings → Dolphin → **Calibrate sticks** is off. Setup leaves a Pro +Controller player's section to the player, so there it only adds both calibration keys, and only +to a section that already exists. + +- **Uncalibrated, Dolphin passes the stick straight through:** a full ±1.0 tilt becomes GameCube ±127. + A real pad's gate stops near ±101 (main) and ±92 (C-stick), so every push lands about a quarter + further than on hardware, and walking or fine aiming is hard. +- **Calibrated to a full circle,** Dolphin maps the rim onto its `Gate Size` octagon: 79.37 (main) + and 72.21 (C-stick) by default, averaged from genuine controllers. +- Dolphin for Android has no Calibrate button, and nothing else sets it. Sources: Dolphin's + `GCPadEmu.h` and `StickGate.cpp`, and Dolphin PR #8963. +- Confirmed on a Retroid Pocket Nova with NSO GameCube controllers, 2026-10-11: walking and scope + aiming feel like an original controller. + ## Sideways Joy-Cons ### Why they're set up as Pro Controllers diff --git a/feature/gamepad/domain/src/main/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfig.kt b/feature/gamepad/domain/src/main/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfig.kt index afde8ce5..f67724fc 100644 --- a/feature/gamepad/domain/src/main/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfig.kt +++ b/feature/gamepad/domain/src/main/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfig.kt @@ -60,6 +60,9 @@ object DolphinGcpadConfig { ) // The D-Pad target reads the hat switch instead: Axis 15 = hat X, Axis 16 = hat Y. + // A full circle, so Dolphin shrinks the stick onto its GameCube gate: docs/virtual-gamepad.md#dolphin-stick-calibration + private const val FULL_CIRCLE_CALIBRATION = "100.00" + private val HAT_NAMES = mapOf( JoyconButton.Up to "Axis 16-", JoyconButton.Down to "Axis 16+", @@ -75,12 +78,27 @@ object DolphinGcpadConfig { "Triggers/R-Analog = `Axis 22+`", ) + private val FULL_CALIBRATION = STICK_PREFIXES.values.associate { "$it/Calibration" to FULL_CIRCLE_CALIBRATION } + fun merge( existing: String?, players: List, controllerNumbers: Map, + calibrateSticks: Boolean = true, mappingFor: (PlayerBody) -> Map, - ): String = IniEditor.mergeSections(existing, sections(players, controllerNumbers, mappingFor)) + ): String { + val configured = sections(players, controllerNumbers, mappingFor, calibrateSticks) + val merged = IniEditor.mergeSections(existing, configured) + return if (calibrateSticks) calibrateHandMapped(merged, players) else merged + } + + // Setup leaves a Pro Controller's section to the player, so it only adds the calibration, and only + // to a section the player has already made. + private fun calibrateHandMapped(ini: String, players: List): String = + players.filter { !it.isSetUpByApp && it.player.index in 1..4 } + .map { "[GCPad${it.player.index}]" } + .filter { IniEditor.hasSection(ini, it) } + .fold(ini) { result, section -> IniEditor.setKeys(result, section, FULL_CALIBRATION) } fun mergeCore(existing: String?, players: List): String { val siDevices = players @@ -97,6 +115,7 @@ object DolphinGcpadConfig { players: List, controllerNumbers: Map, mappingFor: (PlayerBody) -> Map, + calibrateSticks: Boolean, ): Map = players.filter { it.hasController } .sortedBy { it.player.index } @@ -104,7 +123,7 @@ object DolphinGcpadConfig { val index = player.player.index if (index !in 1..4) return@mapNotNull null val deviceId = controllerNumbers[index] ?: return@mapNotNull null - bodyFor(player, index, deviceId, mappingFor)?.let { "[GCPad$index]" to it } + bodyFor(player, index, deviceId, mappingFor, calibrateSticks)?.let { "[GCPad$index]" to it } }.toMap() private fun bodyFor( @@ -112,15 +131,22 @@ object DolphinGcpadConfig { index: Int, deviceId: Int, mappingFor: (PlayerBody) -> Map, + calibrateSticks: Boolean, ): String? { if (!player.isSetUpByApp) return null val side = player.joyconSide() ?: return null val device = "Device = Android/$deviceId/Joy-Con Virtual Gamepad $index" + val bindings = lines(side, mappingFor(PlayerBody(player.player, side))) + val calibration = if (calibrateSticks) calibrationLines(bindings) else emptyList() val analogTriggers = if (player.hasAnalogTriggers) ANALOG_TRIGGER_LINES else emptyList() - return (listOf(device) + lines(side, mappingFor(PlayerBody(player.player, side))) + analogTriggers) - .joinToString("\n", postfix = "\n") + return (listOf(device) + bindings + calibration + analogTriggers).joinToString("\n", postfix = "\n") } + private fun calibrationLines(bindings: List): List = + STICK_PREFIXES.values + .filter { prefix -> bindings.any { it.startsWith("$prefix/") } } + .map { prefix -> "$prefix/Calibration = $FULL_CIRCLE_CALIBRATION" } + private fun lines(side: JoyconSide, mapping: Map): List { val buttonLines = mapping.toSourceMap().mapNotNull { (target, sources) -> expressionFor(side, sources)?.let { expression -> "${DOLPHIN_KEYS.getValue(target)} = $expression" } diff --git a/feature/gamepad/domain/src/test/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfigTest.kt b/feature/gamepad/domain/src/test/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfigTest.kt index 1cf983c2..64c59eca 100644 --- a/feature/gamepad/domain/src/test/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfigTest.kt +++ b/feature/gamepad/domain/src/test/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfigTest.kt @@ -101,6 +101,45 @@ class DolphinGcpadConfigTest { assertFalse(result.contains("C-Stick/Left")) // None leaves it unbound } + @Test + fun `each bound stick is calibrated to a full circle, so Dolphin applies the GameCube gate`() { + val pair = listOf(PlayerState(PlayerNumber.P1, left = joycon(Side.LEFT), right = joycon(Side.RIGHT))) + val lone = listOf(PlayerState(PlayerNumber.P1, right = joycon(Side.RIGHT))) + + val pairResult = merge(null, pair) + assertTrue(pairResult.contains("Main Stick/Calibration = 100.00")) + assertTrue(pairResult.contains("C-Stick/Calibration = 100.00")) + assertFalse(merge(null, lone).contains("C-Stick/")) // no C-stick to calibrate + } + + @Test + fun `turning calibration off leaves the sticks uncalibrated`() { + val pair = listOf(PlayerState(PlayerNumber.P1, left = joycon(Side.LEFT), right = joycon(Side.RIGHT))) + val result = DolphinGcpadConfig.merge(null, pair, mapOf(1 to 7), calibrateSticks = false) { body -> + MappingPresets.default(Console.GAMECUBE).entries(body.side) + } + + assertFalse(result.contains("Calibration")) + } + + @Test + fun `a pro controller's hand-made section keeps its bindings and gains only the calibration`() { + val handMade = "[GCPad1]\nDevice = Android/5/Pro\nButtons/A = `Button A`\n" + val result = merge(handMade, listOf(PlayerState(PlayerNumber.P1, left = joycon(Side.PRO)))) + + assertTrue(result.contains("Device = Android/5/Pro")) + assertTrue(result.contains("Buttons/A = `Button A`")) + assertTrue(result.contains("Main Stick/Calibration = 100.00")) + assertTrue(result.contains("C-Stick/Calibration = 100.00")) + } + + @Test + fun `a pro controller with no section of its own gets none`() { + val result = merge(null, listOf(PlayerState(PlayerNumber.P1, left = joycon(Side.PRO)))) + + assertFalse(result.contains("[GCPad1]")) + } + @Test fun `pro controllers are skipped`() { val result = merge(null, listOf(PlayerState(PlayerNumber.P1, left = joycon(Side.PRO)))) diff --git a/feature/settings/data/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsDataStore.kt b/feature/settings/data/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsDataStore.kt index 162f491f..202f30bb 100644 --- a/feature/settings/data/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsDataStore.kt +++ b/feature/settings/data/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsDataStore.kt @@ -21,6 +21,7 @@ class OutputSettingsDataStore(context: Context) : OutputSettingsRepository { OutputSettings( fasterUpdates = prefs[FASTER_UPDATES] ?: defaults.fasterUpdates, blockDeviceMotion = prefs[BLOCK_DEVICE_MOTION] ?: defaults.blockDeviceMotion, + calibrateDolphinSticks = prefs[CALIBRATE_DOLPHIN_STICKS] ?: defaults.calibrateDolphinSticks, ) } @@ -32,8 +33,13 @@ class OutputSettingsDataStore(context: Context) : OutputSettingsRepository { dataStore.edit { it[BLOCK_DEVICE_MOTION] = enabled } } + override suspend fun setCalibrateDolphinSticks(enabled: Boolean) { + dataStore.edit { it[CALIBRATE_DOLPHIN_STICKS] = enabled } + } + private companion object { val FASTER_UPDATES = booleanPreferencesKey("fast_motion") val BLOCK_DEVICE_MOTION = booleanPreferencesKey("block_device_motion") + val CALIBRATE_DOLPHIN_STICKS = booleanPreferencesKey("calibrate_dolphin_sticks") } } diff --git a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettings.kt b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettings.kt index e7c78804..e71952b3 100644 --- a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettings.kt +++ b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettings.kt @@ -3,4 +3,5 @@ package com.joegec.joycon2android.settings data class OutputSettings( val fasterUpdates: Boolean = true, val blockDeviceMotion: Boolean = true, + val calibrateDolphinSticks: Boolean = true, ) diff --git a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsRepository.kt b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsRepository.kt index 822f0255..5326f672 100644 --- a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsRepository.kt +++ b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsRepository.kt @@ -6,4 +6,5 @@ interface OutputSettingsRepository { val settings: Flow suspend fun setFasterUpdates(enabled: Boolean) suspend fun setBlockDeviceMotion(enabled: Boolean) + suspend fun setCalibrateDolphinSticks(enabled: Boolean) } diff --git a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/SetCalibrateDolphinSticksUseCase.kt b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/SetCalibrateDolphinSticksUseCase.kt new file mode 100644 index 00000000..c25a3cf1 --- /dev/null +++ b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/SetCalibrateDolphinSticksUseCase.kt @@ -0,0 +1,5 @@ +package com.joegec.joycon2android.settings + +class SetCalibrateDolphinSticksUseCase(private val repository: OutputSettingsRepository) { + suspend operator fun invoke(enabled: Boolean) = repository.setCalibrateDolphinSticks(enabled) +} diff --git a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/DolphinSetting.kt b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/DolphinSetting.kt new file mode 100644 index 00000000..daa63a4b --- /dev/null +++ b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/DolphinSetting.kt @@ -0,0 +1,33 @@ +package com.joegec.joycon2android.settings.presentation + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import com.joegec.joycon2android.ui.components.SettingSwitch +import com.joegec.joycon2android.ui.theme.Dimens + +@Composable +internal fun DolphinSetting( + calibrateSticks: Boolean, + onCalibrateSticksToggle: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier, verticalArrangement = Arrangement.spacedBy(Dimens.elementSpacing)) { + Text( + stringResource(R.string.settings_dolphin_title), + color = Color.White, + style = MaterialTheme.typography.titleMedium, + ) + SettingSwitch( + title = stringResource(R.string.settings_dolphin_calibrate_sticks_title), + description = stringResource(R.string.settings_dolphin_calibrate_sticks_description), + checked = calibrateSticks, + onCheckedChange = onCalibrateSticksToggle, + ) + } +} diff --git a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsPanel.kt b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsPanel.kt index 86591763..2d39994b 100644 --- a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsPanel.kt +++ b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsPanel.kt @@ -21,6 +21,7 @@ fun SettingsPanel( onViewModeChange: (ConnectionViewMode) -> Unit, onFasterUpdatesToggle: (Boolean) -> Unit, onBlockDeviceMotionToggle: (Boolean) -> Unit, + onCalibrateDolphinSticksToggle: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -56,5 +57,8 @@ fun SettingsPanel( else stringResource(R.string.settings_block_device_motion_needs_shizuku), ) } + SettingsSection { + DolphinSetting(state.outputSettings.calibrateDolphinSticks, onCalibrateDolphinSticksToggle) + } } } diff --git a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsViewModel.kt b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsViewModel.kt index 360d3577..b3453318 100644 --- a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsViewModel.kt +++ b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.joegec.joycon2android.settings.ObserveOutputSettingsUseCase import com.joegec.joycon2android.settings.OutputSettings import com.joegec.joycon2android.settings.SetBlockDeviceMotionUseCase +import com.joegec.joycon2android.settings.SetCalibrateDolphinSticksUseCase import com.joegec.joycon2android.settings.SetFasterUpdatesUseCase import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -15,6 +16,7 @@ class SettingsViewModel( observeOutputSettings: ObserveOutputSettingsUseCase, private val setFasterUpdates: SetFasterUpdatesUseCase, private val setBlockDeviceMotion: SetBlockDeviceMotionUseCase, + private val setCalibrateDolphinSticks: SetCalibrateDolphinSticksUseCase, ) : ViewModel() { val outputSettings: StateFlow = observeOutputSettings() @@ -28,6 +30,10 @@ class SettingsViewModel( viewModelScope.launch { setBlockDeviceMotion(enabled) } } + fun toggleCalibrateDolphinSticks(enabled: Boolean) { + viewModelScope.launch { setCalibrateDolphinSticks(enabled) } + } + private companion object { const val STOP_TIMEOUT_MS = 5_000L } diff --git a/feature/settings/presentation/src/main/res/values/strings.xml b/feature/settings/presentation/src/main/res/values/strings.xml index bb84a9bb..832b63c2 100644 --- a/feature/settings/presentation/src/main/res/values/strings.xml +++ b/feature/settings/presentation/src/main/res/values/strings.xml @@ -9,4 +9,7 @@ Eden adds this device\'s gyro to Player 1, which makes controller motion stutter. Lifted when the Virtual Gamepad and DSU are both off. Needs Shizuku running. Support on Ko-fi - tips go towards controllers and devices for development and testing (and coffee!) + Dolphin + Calibrate sticks + Stops each stick where a real GameCube controller\'s does, so small movements feel like the original. Takes effect the next time you Set up Dolphin. From 03cda1a534fca6fff5bf83d5a367ea0a79e49932 Mon Sep 17 00:00:00 2001 From: thisishuey Date: Tue, 13 Oct 2026 12:00:19 +0000 Subject: [PATCH 2/2] Always calibrate Dolphin's sticks, and keep a calibration already set Review feedback: the Calibrate sticks switch only served someone who prefers the twitchier full-range feel, and turning it off never undid the keys setup had added to a Pro Controller's section. Setup now always calibrates, and the switch, its setting and its use case are gone. Setup keeps a Calibration the section already has instead of overwriting it, in a Joy-Con section it writes as well as a Pro Controller's. A player who calibrated by hand keeps it across Set up. The full-circle constant moves next to STANDARD_CONTROLLER, out from under the hat-switch comment. A duplicate Pro Controller test goes, and two tests cover a kept calibration. The README drops the settings entry and keeps the troubleshooting row. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +-- .../com/joegec/joycon2android/AppContainer.kt | 4 -- .../com/joegec/joycon2android/MainActivity.kt | 8 +--- .../joycon2android/emulator/EmulatorSetup.kt | 4 +- docs/architecture.md | 2 +- docs/virtual-gamepad.md | 6 +-- .../gamepad/emulator/DolphinGcpadConfig.kt | 41 ++++++++++--------- .../emulator/DolphinGcpadConfigTest.kt | 32 +++++++++++---- .../settings/OutputSettingsDataStore.kt | 6 --- .../joycon2android/settings/OutputSettings.kt | 1 - .../settings/OutputSettingsRepository.kt | 1 - .../SetCalibrateDolphinSticksUseCase.kt | 5 --- .../settings/presentation/DolphinSetting.kt | 33 --------------- .../settings/presentation/SettingsPanel.kt | 4 -- .../presentation/SettingsViewModel.kt | 6 --- .../src/main/res/values/strings.xml | 3 -- 16 files changed, 52 insertions(+), 110 deletions(-) delete mode 100644 feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/SetCalibrateDolphinSticksUseCase.kt delete mode 100644 feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/DolphinSetting.kt diff --git a/README.md b/README.md index 3c7188f6..e699c67b 100644 --- a/README.md +++ b/README.md @@ -93,10 +93,6 @@ The gear at the top right opens the settings panel: smoother motion and quicker buttons, at a battery cost. Applies while the virtual gamepad or DSU is on. - **Ignore this device's motion in Eden** (on by default, needs Shizuku) — stops Eden mixing this device's gyro into Player 1. Lifted when the virtual gamepad and DSU are both off. -- **Dolphin → Calibrate sticks** (on by default) — Dolphin setup calibrates the sticks so they stop - where a real GameCube controller's do; without it, small movements feel twitchy. Run Set up again - after changing it. A Pro Controller you mapped by hand in Dolphin keeps its mapping and only gains - the calibration. ### 5. Set up your emulator @@ -262,7 +258,7 @@ shoulder buttons. | Motion aiming stutters | Settings (gear, top right): keep **Ignore this device's motion in Eden** and **Faster controller updates** on | | Tilting the device moves the aim in Eden | Turn on **Ignore this device's motion in Eden** | | Pointer drifts or starts off-screen | Rest the controller for ~2 s, then press Recenter | -| Sticks feel twitchy in Dolphin (hard to walk or aim) | Keep Settings → Dolphin → **Calibrate sticks** on and tap **Set up** for Dolphin again. A Pro Controller: map it in Dolphin first, then tap **Set up** once to add the calibration | +| Sticks feel twitchy in Dolphin (hard to walk or aim) | Tap **Set up** for Dolphin again. A Pro Controller: map it in Dolphin first, then tap **Set up** once to add the calibration | | MotionPlus tutorial replays every boot (Dolphin) | Set `MPLS.MOVIE` in `Wii/shared2/sys/SYSCONF` with Dolphin closed ([why](docs/dsu-motion.md#motionplus-tutorial-replays)) | ## Roadmap and feedback diff --git a/app/src/main/java/com/joegec/joycon2android/AppContainer.kt b/app/src/main/java/com/joegec/joycon2android/AppContainer.kt index f84a13e9..945753f6 100644 --- a/app/src/main/java/com/joegec/joycon2android/AppContainer.kt +++ b/app/src/main/java/com/joegec/joycon2android/AppContainer.kt @@ -77,7 +77,6 @@ import com.joegec.joycon2android.settings.ObserveOutputSettingsUseCase import com.joegec.joycon2android.settings.OutputSettingsDataStore import com.joegec.joycon2android.settings.OutputSettingsRepository import com.joegec.joycon2android.settings.SetBlockDeviceMotionUseCase -import com.joegec.joycon2android.settings.SetCalibrateDolphinSticksUseCase import com.joegec.joycon2android.settings.SetDeviceMotionBlockedUseCase import com.joegec.joycon2android.settings.SetFasterUpdatesUseCase import com.joegec.joycon2android.emulator.EdenDeviceMotionBlocker @@ -112,7 +111,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import java.io.File @@ -190,7 +188,6 @@ class AppContainer(context: Context) { val observeOutputSettings = ObserveOutputSettingsUseCase(outputSettings) val setFasterUpdates = SetFasterUpdatesUseCase(outputSettings) val setBlockDeviceMotion = SetBlockDeviceMotionUseCase(outputSettings) - val setCalibrateDolphinSticks = SetCalibrateDolphinSticksUseCase(outputSettings) // --- Assignment --- val assignmentRepository: AssignmentRepository = PlayerAssignmentManager() @@ -217,7 +214,6 @@ class AppContainer(context: Context) { gamepadControllerNumbers = { dolphinGamepadIds(appContext) }, getControllerMapping = getControllerMapping, getRemoteMotion = getRemoteMotion, - calibrateDolphinSticks = { observeOutputSettings().first().calibrateDolphinSticks }, ) val emulatorLauncher = EmulatorLauncher(appContext) diff --git a/app/src/main/java/com/joegec/joycon2android/MainActivity.kt b/app/src/main/java/com/joegec/joycon2android/MainActivity.kt index 239ebebd..42de0ab3 100644 --- a/app/src/main/java/com/joegec/joycon2android/MainActivity.kt +++ b/app/src/main/java/com/joegec/joycon2android/MainActivity.kt @@ -91,12 +91,7 @@ class MainActivity : ComponentActivity() { viewModelFactory { initializer { val c = (application as JoyconApplication).container - SettingsViewModel( - c.observeOutputSettings, - c.setFasterUpdates, - c.setBlockDeviceMotion, - c.setCalibrateDolphinSticks, - ) + SettingsViewModel(c.observeOutputSettings, c.setFasterUpdates, c.setBlockDeviceMotion) } } } @@ -279,7 +274,6 @@ class MainActivity : ComponentActivity() { onViewModeChange = viewModel::setViewMode, onFasterUpdatesToggle = settingsViewModel::toggleFasterUpdates, onBlockDeviceMotionToggle = settingsViewModel::toggleBlockDeviceMotion, - onCalibrateDolphinSticksToggle = settingsViewModel::toggleCalibrateDolphinSticks, ) }, containerColor = Background, diff --git a/app/src/main/java/com/joegec/joycon2android/emulator/EmulatorSetup.kt b/app/src/main/java/com/joegec/joycon2android/emulator/EmulatorSetup.kt index cc11be70..19713cc4 100644 --- a/app/src/main/java/com/joegec/joycon2android/emulator/EmulatorSetup.kt +++ b/app/src/main/java/com/joegec/joycon2android/emulator/EmulatorSetup.kt @@ -38,7 +38,6 @@ class EmulatorSetup( private val gamepadControllerNumbers: () -> Map, private val getControllerMapping: GetEffectiveControllerMappingUseCase, private val getRemoteMotion: GetRemoteMotionUseCase, - private val calibrateDolphinSticks: suspend () -> Boolean, ) { // Read up front: the generators are synchronous. @@ -164,8 +163,7 @@ class EmulatorSetup( shell.readText(DolphinGcpadConfig.path), players, gamepadControllerNumbers(), - calibrateSticks = calibrateDolphinSticks(), - mappingFor = mappingLookup(Console.GAMECUBE, players), + mappingLookup(Console.GAMECUBE, players), ) val mappingsOk = shell.writeText(DolphinGcpadConfig.path, mappings) // Dolphin's GameCube ports default to None. diff --git a/docs/architecture.md b/docs/architecture.md index 91b12698..29ff90b3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -142,7 +142,7 @@ One **ViewModel per feature**, in its presentation module, built in `MainActivit - `GamepadViewModel` — gamepad status, Shizuku availability and emulator auto setup. - `UpdateViewModel` — the once-per-launch release check and the update prompt. - `SettingsViewModel` — the settings that apply to whichever output runs (faster updates, the Eden - motion block, Dolphin stick calibration). The panel's layout choice stays with `Joycon2ViewModel`, which renders it. + motion block). The panel's layout choice stays with `Joycon2ViewModel`, which renders it. - `ControllerMappingViewModel` (in `:core:buttonmapping:presentation`) — the button-mapping editor. - `Joycon2ViewModel` (in `:app`) — the app-level host: the coordinator's session `uiState` (genuinely cross-feature), BLE permissions, scan/assign/disconnect, and the service binding. diff --git a/docs/virtual-gamepad.md b/docs/virtual-gamepad.md index b7920660..fcee9fe1 100644 --- a/docs/virtual-gamepad.md +++ b/docs/virtual-gamepad.md @@ -98,9 +98,9 @@ mapping of the console each output's card is set to, while that output runs. ## Dolphin stick calibration Dolphin setup writes `Main Stick/Calibration = 100.00` and `C-Stick/Calibration = 100.00` for each -stick it binds, unless Settings → Dolphin → **Calibrate sticks** is off. Setup leaves a Pro -Controller player's section to the player, so there it only adds both calibration keys, and only -to a section that already exists. +stick it binds, keeping any `Calibration` the section already has. Setup leaves a Pro Controller +player's section to the player, so there it only adds both calibration keys, and only to a section +that already exists. - **Uncalibrated, Dolphin passes the stick straight through:** a full ±1.0 tilt becomes GameCube ±127. A real pad's gate stops near ±101 (main) and ±92 (C-stick), so every push lands about a quarter diff --git a/feature/gamepad/domain/src/main/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfig.kt b/feature/gamepad/domain/src/main/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfig.kt index f67724fc..365f3897 100644 --- a/feature/gamepad/domain/src/main/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfig.kt +++ b/feature/gamepad/domain/src/main/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfig.kt @@ -25,6 +25,9 @@ object DolphinGcpadConfig { private const val STANDARD_CONTROLLER = "6" // Dolphin SIDevice: Standard Controller + // Dolphin maps a full circle onto its GameCube gate: docs/virtual-gamepad.md#dolphin-stick-calibration + private const val FULL_CIRCLE_CALIBRATION = "100.00" + private val DOLPHIN_KEYS = mapOf( GameCubeButton.A to "Buttons/A", GameCubeButton.B to "Buttons/B", @@ -60,9 +63,6 @@ object DolphinGcpadConfig { ) // The D-Pad target reads the hat switch instead: Axis 15 = hat X, Axis 16 = hat Y. - // A full circle, so Dolphin shrinks the stick onto its GameCube gate: docs/virtual-gamepad.md#dolphin-stick-calibration - private const val FULL_CIRCLE_CALIBRATION = "100.00" - private val HAT_NAMES = mapOf( JoyconButton.Up to "Axis 16-", JoyconButton.Down to "Axis 16+", @@ -78,27 +78,29 @@ object DolphinGcpadConfig { "Triggers/R-Analog = `Axis 22+`", ) - private val FULL_CALIBRATION = STICK_PREFIXES.values.associate { "$it/Calibration" to FULL_CIRCLE_CALIBRATION } - fun merge( existing: String?, players: List, controllerNumbers: Map, - calibrateSticks: Boolean = true, mappingFor: (PlayerBody) -> Map, ): String { - val configured = sections(players, controllerNumbers, mappingFor, calibrateSticks) - val merged = IniEditor.mergeSections(existing, configured) - return if (calibrateSticks) calibrateHandMapped(merged, players) else merged + val merged = IniEditor.mergeSections(existing, sections(existing, players, controllerNumbers, mappingFor)) + return calibrateHandMapped(merged, players) } // Setup leaves a Pro Controller's section to the player, so it only adds the calibration, and only // to a section the player has already made. private fun calibrateHandMapped(ini: String, players: List): String = players.filter { !it.isSetUpByApp && it.player.index in 1..4 } - .map { "[GCPad${it.player.index}]" } + .map { sectionFor(it.player.index) } .filter { IniEditor.hasSection(ini, it) } - .fold(ini) { result, section -> IniEditor.setKeys(result, section, FULL_CALIBRATION) } + .fold(ini) { result, section -> + IniEditor.setKeys(result, section, calibrationFor(result, section, STICK_PREFIXES.values)) + } + + private fun calibrationFor(existing: String?, section: String, sticks: Collection): Map = + sticks.map { "$it/Calibration" } + .associateWith { IniEditor.valueOf(existing, section, it) ?: FULL_CIRCLE_CALIBRATION } fun mergeCore(existing: String?, players: List): String { val siDevices = players @@ -112,10 +114,10 @@ object DolphinGcpadConfig { // A player whose pad isn't enumerated yet is skipped: docs/virtual-gamepad.md#device-identity private fun sections( + existing: String?, players: List, controllerNumbers: Map, mappingFor: (PlayerBody) -> Map, - calibrateSticks: Boolean, ): Map = players.filter { it.hasController } .sortedBy { it.player.index } @@ -123,30 +125,29 @@ object DolphinGcpadConfig { val index = player.player.index if (index !in 1..4) return@mapNotNull null val deviceId = controllerNumbers[index] ?: return@mapNotNull null - bodyFor(player, index, deviceId, mappingFor, calibrateSticks)?.let { "[GCPad$index]" to it } + bodyFor(existing, player, index, deviceId, mappingFor)?.let { sectionFor(index) to it } }.toMap() + private fun sectionFor(index: Int) = "[GCPad$index]" + private fun bodyFor( + existing: String?, player: PlayerState, index: Int, deviceId: Int, mappingFor: (PlayerBody) -> Map, - calibrateSticks: Boolean, ): String? { if (!player.isSetUpByApp) return null val side = player.joyconSide() ?: return null val device = "Device = Android/$deviceId/Joy-Con Virtual Gamepad $index" val bindings = lines(side, mappingFor(PlayerBody(player.player, side))) - val calibration = if (calibrateSticks) calibrationLines(bindings) else emptyList() + val boundSticks = STICK_PREFIXES.values.filter { prefix -> bindings.any { it.startsWith("$prefix/") } } + val calibration = calibrationFor(existing, sectionFor(index), boundSticks) + .map { (key, value) -> "$key = $value" } val analogTriggers = if (player.hasAnalogTriggers) ANALOG_TRIGGER_LINES else emptyList() return (listOf(device) + bindings + calibration + analogTriggers).joinToString("\n", postfix = "\n") } - private fun calibrationLines(bindings: List): List = - STICK_PREFIXES.values - .filter { prefix -> bindings.any { it.startsWith("$prefix/") } } - .map { prefix -> "$prefix/Calibration = $FULL_CIRCLE_CALIBRATION" } - private fun lines(side: JoyconSide, mapping: Map): List { val buttonLines = mapping.toSourceMap().mapNotNull { (target, sources) -> expressionFor(side, sources)?.let { expression -> "${DOLPHIN_KEYS.getValue(target)} = $expression" } diff --git a/feature/gamepad/domain/src/test/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfigTest.kt b/feature/gamepad/domain/src/test/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfigTest.kt index 64c59eca..e8a39880 100644 --- a/feature/gamepad/domain/src/test/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfigTest.kt +++ b/feature/gamepad/domain/src/test/kotlin/com/joegec/joycon2android/gamepad/emulator/DolphinGcpadConfigTest.kt @@ -9,6 +9,7 @@ import com.joegec.joycon2android.model.ControllerModel import com.joegec.joycon2android.model.PlayerNumber import com.joegec.joycon2android.model.PlayerState import com.joegec.joycon2android.model.Side +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test @@ -113,13 +114,14 @@ class DolphinGcpadConfigTest { } @Test - fun `turning calibration off leaves the sticks uncalibrated`() { + fun `a stick calibration already in the section is kept`() { + val existing = "[GCPad1]\nMain Stick/Calibration = 85.00\n" val pair = listOf(PlayerState(PlayerNumber.P1, left = joycon(Side.LEFT), right = joycon(Side.RIGHT))) - val result = DolphinGcpadConfig.merge(null, pair, mapOf(1 to 7), calibrateSticks = false) { body -> - MappingPresets.default(Console.GAMECUBE).entries(body.side) - } + val result = merge(existing, pair) - assertFalse(result.contains("Calibration")) + assertTrue(result.contains("Main Stick/Calibration = 85.00")) + assertFalse(result.contains("Main Stick/Calibration = 100.00")) + assertTrue(result.contains("C-Stick/Calibration = 100.00")) } @Test @@ -134,10 +136,13 @@ class DolphinGcpadConfigTest { } @Test - fun `a pro controller with no section of its own gets none`() { - val result = merge(null, listOf(PlayerState(PlayerNumber.P1, left = joycon(Side.PRO)))) + fun `a pro controller's own stick calibration is kept`() { + val handMade = "[GCPad1]\nDevice = Android/5/Pro\nMain Stick/Calibration = 85.00\n" + val result = merge(handMade, listOf(PlayerState(PlayerNumber.P1, left = joycon(Side.PRO)))) - assertFalse(result.contains("[GCPad1]")) + assertTrue(result.contains("Main Stick/Calibration = 85.00")) + assertFalse(result.contains("Main Stick/Calibration = 100.00")) + assertTrue(result.contains("C-Stick/Calibration = 100.00")) } @Test @@ -158,6 +163,17 @@ class DolphinGcpadConfigTest { assertTrue(result.contains("C-Stick/Up = `Axis 14-`")) } + @Test + fun `a gamecube controller's section is written by setup, with each stick calibrated once`() { + val gameCube = joycon(Side.PRO).copy(model = ControllerModel.GAMECUBE) + val stale = "[GCPad1]\nDevice = Android/5/Old\n" + val result = merge(stale, listOf(PlayerState(PlayerNumber.P1, left = gameCube, right = gameCube))) + + assertFalse(result.contains("Device = Android/5/Old")) + assertEquals(1, Regex("Main Stick/Calibration = 100.00").findAll(result).count()) + assertEquals(1, Regex("C-Stick/Calibration = 100.00").findAll(result).count()) + } + @Test fun `a gamecube controller's analog triggers bind to the brake and gas axes`() { val gameCube = joycon(Side.PRO).copy(model = ControllerModel.GAMECUBE) diff --git a/feature/settings/data/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsDataStore.kt b/feature/settings/data/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsDataStore.kt index 202f30bb..162f491f 100644 --- a/feature/settings/data/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsDataStore.kt +++ b/feature/settings/data/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsDataStore.kt @@ -21,7 +21,6 @@ class OutputSettingsDataStore(context: Context) : OutputSettingsRepository { OutputSettings( fasterUpdates = prefs[FASTER_UPDATES] ?: defaults.fasterUpdates, blockDeviceMotion = prefs[BLOCK_DEVICE_MOTION] ?: defaults.blockDeviceMotion, - calibrateDolphinSticks = prefs[CALIBRATE_DOLPHIN_STICKS] ?: defaults.calibrateDolphinSticks, ) } @@ -33,13 +32,8 @@ class OutputSettingsDataStore(context: Context) : OutputSettingsRepository { dataStore.edit { it[BLOCK_DEVICE_MOTION] = enabled } } - override suspend fun setCalibrateDolphinSticks(enabled: Boolean) { - dataStore.edit { it[CALIBRATE_DOLPHIN_STICKS] = enabled } - } - private companion object { val FASTER_UPDATES = booleanPreferencesKey("fast_motion") val BLOCK_DEVICE_MOTION = booleanPreferencesKey("block_device_motion") - val CALIBRATE_DOLPHIN_STICKS = booleanPreferencesKey("calibrate_dolphin_sticks") } } diff --git a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettings.kt b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettings.kt index e71952b3..e7c78804 100644 --- a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettings.kt +++ b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettings.kt @@ -3,5 +3,4 @@ package com.joegec.joycon2android.settings data class OutputSettings( val fasterUpdates: Boolean = true, val blockDeviceMotion: Boolean = true, - val calibrateDolphinSticks: Boolean = true, ) diff --git a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsRepository.kt b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsRepository.kt index 5326f672..822f0255 100644 --- a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsRepository.kt +++ b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/OutputSettingsRepository.kt @@ -6,5 +6,4 @@ interface OutputSettingsRepository { val settings: Flow suspend fun setFasterUpdates(enabled: Boolean) suspend fun setBlockDeviceMotion(enabled: Boolean) - suspend fun setCalibrateDolphinSticks(enabled: Boolean) } diff --git a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/SetCalibrateDolphinSticksUseCase.kt b/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/SetCalibrateDolphinSticksUseCase.kt deleted file mode 100644 index c25a3cf1..00000000 --- a/feature/settings/domain/src/main/kotlin/com/joegec/joycon2android/settings/SetCalibrateDolphinSticksUseCase.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.joegec.joycon2android.settings - -class SetCalibrateDolphinSticksUseCase(private val repository: OutputSettingsRepository) { - suspend operator fun invoke(enabled: Boolean) = repository.setCalibrateDolphinSticks(enabled) -} diff --git a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/DolphinSetting.kt b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/DolphinSetting.kt deleted file mode 100644 index daa63a4b..00000000 --- a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/DolphinSetting.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.joegec.joycon2android.settings.presentation - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import com.joegec.joycon2android.ui.components.SettingSwitch -import com.joegec.joycon2android.ui.theme.Dimens - -@Composable -internal fun DolphinSetting( - calibrateSticks: Boolean, - onCalibrateSticksToggle: (Boolean) -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier, verticalArrangement = Arrangement.spacedBy(Dimens.elementSpacing)) { - Text( - stringResource(R.string.settings_dolphin_title), - color = Color.White, - style = MaterialTheme.typography.titleMedium, - ) - SettingSwitch( - title = stringResource(R.string.settings_dolphin_calibrate_sticks_title), - description = stringResource(R.string.settings_dolphin_calibrate_sticks_description), - checked = calibrateSticks, - onCheckedChange = onCalibrateSticksToggle, - ) - } -} diff --git a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsPanel.kt b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsPanel.kt index 2d39994b..86591763 100644 --- a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsPanel.kt +++ b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsPanel.kt @@ -21,7 +21,6 @@ fun SettingsPanel( onViewModeChange: (ConnectionViewMode) -> Unit, onFasterUpdatesToggle: (Boolean) -> Unit, onBlockDeviceMotionToggle: (Boolean) -> Unit, - onCalibrateDolphinSticksToggle: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -57,8 +56,5 @@ fun SettingsPanel( else stringResource(R.string.settings_block_device_motion_needs_shizuku), ) } - SettingsSection { - DolphinSetting(state.outputSettings.calibrateDolphinSticks, onCalibrateDolphinSticksToggle) - } } } diff --git a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsViewModel.kt b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsViewModel.kt index b3453318..360d3577 100644 --- a/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsViewModel.kt +++ b/feature/settings/presentation/src/main/kotlin/com/joegec/joycon2android/settings/presentation/SettingsViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope import com.joegec.joycon2android.settings.ObserveOutputSettingsUseCase import com.joegec.joycon2android.settings.OutputSettings import com.joegec.joycon2android.settings.SetBlockDeviceMotionUseCase -import com.joegec.joycon2android.settings.SetCalibrateDolphinSticksUseCase import com.joegec.joycon2android.settings.SetFasterUpdatesUseCase import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -16,7 +15,6 @@ class SettingsViewModel( observeOutputSettings: ObserveOutputSettingsUseCase, private val setFasterUpdates: SetFasterUpdatesUseCase, private val setBlockDeviceMotion: SetBlockDeviceMotionUseCase, - private val setCalibrateDolphinSticks: SetCalibrateDolphinSticksUseCase, ) : ViewModel() { val outputSettings: StateFlow = observeOutputSettings() @@ -30,10 +28,6 @@ class SettingsViewModel( viewModelScope.launch { setBlockDeviceMotion(enabled) } } - fun toggleCalibrateDolphinSticks(enabled: Boolean) { - viewModelScope.launch { setCalibrateDolphinSticks(enabled) } - } - private companion object { const val STOP_TIMEOUT_MS = 5_000L } diff --git a/feature/settings/presentation/src/main/res/values/strings.xml b/feature/settings/presentation/src/main/res/values/strings.xml index 832b63c2..bb84a9bb 100644 --- a/feature/settings/presentation/src/main/res/values/strings.xml +++ b/feature/settings/presentation/src/main/res/values/strings.xml @@ -9,7 +9,4 @@ Eden adds this device\'s gyro to Player 1, which makes controller motion stutter. Lifted when the Virtual Gamepad and DSU are both off. Needs Shizuku running. Support on Ko-fi - tips go towards controllers and devices for development and testing (and coffee!) - Dolphin - Calibrate sticks - Stops each stick where a real GameCube controller\'s does, so small movements feel like the original. Takes effect the next time you Set up Dolphin.