mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Send rumble to the controller, not the phone
Rumble always went to the phone's own motor: the vibrator lookup asked the system service and never considered the connected pad. On a handheld that is the wrong motor outright, and on a phone-plus-controller setup it buzzes the device sitting in a dock while the pad in hand stays still. Prefer the first connected gamepad or joystick reporting a working motor, falling back to the phone. The target is resolved per state change rather than cached for the pump's lifetime, so connecting or disconnecting a pad mid-session moves rumble with it; the motor that was last started is tracked separately so unplugging mid-rumble cannot leave one buzzing. Add a 'Vibrate the phone' toggle gating only that fallback, so playing on a pad need not mean the phone rumbles along with it. Touch haptics stay on the phone deliberately -- the finger is on the phone's screen. Closes #89
This commit is contained in:
Vendored
+1
-1
Submodule 3rdparty/discord-rpc/discord-rpc updated: fb04b1766b...3dc2c326cb
Vendored
+1
-1
Submodule 3rdparty/libsdl-org/SDL updated: 147a8ee32d...f87239e71e
@@ -72,7 +72,13 @@
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "Savers", __VA_ARGS__)
|
||||
|
||||
#define STACK_DEPTH 32
|
||||
#define GL1_MAX_LISTS 16
|
||||
/* Sixteen was not enough for two of the savers, and the failure was silent-ish: glGenLists
|
||||
* returned 0, every subsequent glNewList/glCallList logged "bad list", and the geometry those
|
||||
* lists held simply never drew. Lattice asks for 20 in one range (NUMOBJECTS), and Skyrocket
|
||||
* accumulates 16 across separate calls -- flare 4, smoke 5, and seven singles in world.cpp.
|
||||
* Names are 1-based, so the old ceiling allowed a longest run of 15 and neither could ever
|
||||
* succeed. Sixty-four is a slot table of a pointer and two ints apiece: about a kilobyte. */
|
||||
#define GL1_MAX_LISTS 64
|
||||
|
||||
typedef struct {
|
||||
float pos[3];
|
||||
|
||||
@@ -906,6 +906,9 @@ val EN: Map<String, String> = mapOf(
|
||||
"pad.players.help" to "The PS3 has seven controller ports and no multitap, so up to seven pads work with no setup. Connect them before launching — the order they first press a button in is the order they are assigned.",
|
||||
"pad.rumble.description" to "Master switch for controller rumble and the device's built-in vibration. Turn off to silence all haptics.",
|
||||
"pad.rumble.label" to "Rumble / Vibration",
|
||||
"pad.rumblePhone.label" to "Vibrate the phone",
|
||||
"pad.rumblePhone.description" to
|
||||
"Use the phone's own motor when no controller has one. Turn this off to keep rumble on the controller only.",
|
||||
"pad.hapticStrength.description" to "Scales all vibration — controller rumble and on-screen touch haptics alike. Below 100% tames a strong motor; above 100% boosts a weak one.",
|
||||
"pad.hapticStrength.label" to "Vibration Strength",
|
||||
"pad.scopeHint.global" to "○ Editing GLOBAL controls (all games).",
|
||||
|
||||
@@ -393,6 +393,17 @@ object ControllerMappings {
|
||||
com.armsx3.NativeApp.sRumbleEnabled = on
|
||||
}
|
||||
|
||||
// Whether the PHONE's motor may be used. Rumble prefers a connected controller's motor and
|
||||
// falls back to the phone; this gates only that fallback, so playing on a pad need not mean
|
||||
// the phone buzzes too. Mirrored into NativeApp.sPhoneRumbleEnabled the same way KEY_RUMBLE
|
||||
// is — live on change and at app start. Default on, so a phone-only player is unaffected.
|
||||
private const val KEY_RUMBLE_PHONE = "pad.rumble.phone"
|
||||
fun phoneRumbleEnabled(): Boolean = MainActivityRuntime.prefs.getBoolean(KEY_RUMBLE_PHONE, true)
|
||||
fun setPhoneRumbleEnabled(on: Boolean) {
|
||||
MainActivityRuntime.prefs.edit { putBoolean(KEY_RUMBLE_PHONE, on) }
|
||||
com.armsx3.NativeApp.sPhoneRumbleEnabled = on
|
||||
}
|
||||
|
||||
// Haptic strength: one multiplier scaling ALL vibration — controller rumble AND on-screen
|
||||
// touch ticks both funnel through NativeApp.rumbleOne. 0..200 % (100 = as the game/UI
|
||||
// authored it), so it tames a too-strong motor or boosts a weak one. Persisted and mirrored
|
||||
|
||||
@@ -2157,6 +2157,7 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
startAutosaveIntervalJob()
|
||||
// Restore the saved rumble master toggle into the native gate (NativeApp.onPadRumble).
|
||||
NativeApp.sRumbleEnabled = ControllerMappings.rumbleEnabled()
|
||||
NativeApp.sPhoneRumbleEnabled = ControllerMappings.phoneRumbleEnabled()
|
||||
// Push the saved haptic strength + achievement-sound volume into their native gates before
|
||||
// any rumble or unlock sound can fire (both default to 1.0 = as authored until set here).
|
||||
ControllerMappings.syncHapticIntensity()
|
||||
|
||||
@@ -260,6 +260,16 @@ fun PadTab(state: MutableState<Settings>) {
|
||||
ControllerMappings.setRumbleEnabled(it)
|
||||
refreshToken.intValue++
|
||||
}
|
||||
// Rumble prefers a connected controller's motor; this gates only the phone fallback,
|
||||
// so playing on a pad need not mean the phone buzzes along with it.
|
||||
ToggleRow(
|
||||
str("pad.rumblePhone.label"),
|
||||
ControllerMappings.phoneRumbleEnabled(),
|
||||
description = str("pad.rumblePhone.description"),
|
||||
) {
|
||||
ControllerMappings.setPhoneRumbleEnabled(it)
|
||||
refreshToken.intValue++
|
||||
}
|
||||
// Vibration strength: one multiplier over BOTH controller rumble and on-screen touch
|
||||
// haptics (they share the motor path), so an over-eager motor can be tamed or a weak
|
||||
// one boosted. 100% = as authored; 0% = off.
|
||||
|
||||
@@ -101,6 +101,7 @@ internal val SETTINGS_SEARCH_INDEX: List<SettingsSearchEntry> = listOf(
|
||||
SettingsSearchEntry("pad.section.playerRumble", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.editing.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.rumble.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.rumblePhone.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.hapticStrength.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.pressureAmount.label", true, SettingsCategory.Controls),
|
||||
SettingsSearchEntry("pad.section.analogSticks", true, SettingsCategory.Controls),
|
||||
|
||||
@@ -247,6 +247,13 @@ public final class NativeApp {
|
||||
/** Master rumble toggle. */
|
||||
public static volatile boolean sRumbleEnabled = true;
|
||||
|
||||
/**
|
||||
* Whether the PHONE's own motor may be used. A controller's motor is always allowed; this
|
||||
* only gates the fallback, so a user playing on a pad can stop the phone buzzing in their
|
||||
* pocket or dock without giving up rumble entirely (issue #89). Default on.
|
||||
*/
|
||||
public static volatile boolean sPhoneRumbleEnabled = true;
|
||||
|
||||
/** Volume applied to UI sounds played through NativeApp.playSound. */
|
||||
public static volatile float sSoundVolume = 1.0f;
|
||||
|
||||
|
||||
@@ -1390,7 +1390,8 @@ object Rpcs3Bridge {
|
||||
// called. Two stores, neither connected to the other.
|
||||
private val rumbleEnabled: Boolean get() = NativeApp.sRumbleEnabled
|
||||
|
||||
private fun vibrator(): Vibrator? {
|
||||
/** The phone's own motor. */
|
||||
private fun deviceVibrator(): Vibrator? {
|
||||
val ctx = appContext ?: return null
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
(ctx.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager)?.defaultVibrator
|
||||
@@ -1400,6 +1401,50 @@ object Rpcs3Bridge {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The motor in a connected controller, if one has it.
|
||||
*
|
||||
* Rumble went to the PHONE even with a controller attached, because this only ever asked the
|
||||
* system service. The pad is what the game is addressing; the phone buzzing in its place is
|
||||
* wrong, and on a handheld it is the wrong motor entirely (issue #89).
|
||||
*
|
||||
* The first gamepad or joystick with a working motor wins, in InputDevice id order, so a
|
||||
* single connected pad is unambiguous.
|
||||
*/
|
||||
private fun controllerVibrator(): Vibrator? = runCatching {
|
||||
for (id in android.view.InputDevice.getDeviceIds()) {
|
||||
val dev = android.view.InputDevice.getDevice(id) ?: continue
|
||||
|
||||
val isPad = (dev.sources and android.view.InputDevice.SOURCE_GAMEPAD) ==
|
||||
android.view.InputDevice.SOURCE_GAMEPAD ||
|
||||
(dev.sources and android.view.InputDevice.SOURCE_JOYSTICK) ==
|
||||
android.view.InputDevice.SOURCE_JOYSTICK
|
||||
|
||||
if (!isPad) continue
|
||||
|
||||
val v = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
dev.vibratorManager?.defaultVibrator
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
dev.vibrator
|
||||
}
|
||||
|
||||
if (v != null && v.hasVibrator()) return@runCatching v
|
||||
}
|
||||
|
||||
null
|
||||
}.getOrNull()
|
||||
|
||||
/**
|
||||
* Where rumble goes: the controller if one can take it, otherwise the phone.
|
||||
*
|
||||
* Resolved on demand rather than cached, because a pad can be connected or disconnected
|
||||
* mid-session. Callers only ask when the motor state changes, so enumerating devices is not a
|
||||
* per-frame cost.
|
||||
*/
|
||||
private fun vibrator(): Vibrator? =
|
||||
controllerVibrator() ?: deviceVibrator().takeIf { NativeApp.sPhoneRumbleEnabled }
|
||||
|
||||
@JvmStatic
|
||||
fun setPadVibration(on: Boolean) {
|
||||
NativeApp.sRumbleEnabled = on
|
||||
@@ -1410,12 +1455,13 @@ object Rpcs3Bridge {
|
||||
@JvmStatic
|
||||
fun startRumblePump() {
|
||||
if (rumbleRunning) return
|
||||
val vib = vibrator() ?: return
|
||||
if (!vib.hasVibrator()) return
|
||||
|
||||
rumbleRunning = true
|
||||
rumbleThread = Thread {
|
||||
var lastAmplitude = 0
|
||||
// The motor we last started, so it can be stopped even if the target has since
|
||||
// changed underneath us -- otherwise unplugging a pad mid-rumble leaves it buzzing.
|
||||
var active: Vibrator? = null
|
||||
while (rumbleRunning) {
|
||||
val packed = runCatching { RPCSX.instance.getPadRumble(0) }.getOrDefault(0)
|
||||
val large = (packed shr 8) and 0xFF
|
||||
@@ -1428,8 +1474,15 @@ object Rpcs3Bridge {
|
||||
|
||||
if (want != lastAmplitude) {
|
||||
runCatching {
|
||||
if (want <= 0) {
|
||||
vib.cancel()
|
||||
// Resolved per change, not once at startup: a pad connected mid-session
|
||||
// has to take over from the phone, and vice versa on disconnect.
|
||||
val vib = vibrator()
|
||||
|
||||
active?.takeIf { it !== vib }?.cancel()
|
||||
active = null
|
||||
|
||||
if (want <= 0 || vib == null) {
|
||||
vib?.cancel()
|
||||
} else {
|
||||
// Repeating one-shot rather than a fixed duration: the guest
|
||||
// decides when rumble stops, and a timed effect would either cut
|
||||
@@ -1439,6 +1492,7 @@ object Rpcs3Bridge {
|
||||
longArrayOf(0, 60), intArrayOf(0, want.coerceIn(1, 255)), 0
|
||||
)
|
||||
)
|
||||
active = vib
|
||||
}
|
||||
}
|
||||
lastAmplitude = want
|
||||
@@ -1446,7 +1500,7 @@ object Rpcs3Bridge {
|
||||
|
||||
try { Thread.sleep(30) } catch (_: InterruptedException) { break }
|
||||
}
|
||||
runCatching { vib.cancel() }
|
||||
runCatching { active?.cancel() }
|
||||
}.apply { isDaemon = true; name = "rumble-pump"; start() }
|
||||
}
|
||||
|
||||
@@ -1500,13 +1554,9 @@ object Rpcs3Bridge {
|
||||
|
||||
@JvmStatic
|
||||
fun touchHaptic() {
|
||||
val ctx = appContext ?: return
|
||||
val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
(ctx.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as? VibratorManager)?.defaultVibrator
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
ctx.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
||||
} ?: return
|
||||
// The phone's motor deliberately, not vibrator(): this is feedback for a finger on the
|
||||
// phone's own screen, so it belongs there even when a controller is holding the rumble.
|
||||
val vibrator = deviceVibrator() ?: return
|
||||
|
||||
runCatching {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
|
||||
Reference in New Issue
Block a user