diff --git a/android/app/src/main/cpp/android_input_system.cpp b/android/app/src/main/cpp/android_input_system.cpp index 814c10f..d792d1d 100644 --- a/android/app/src/main/cpp/android_input_system.cpp +++ b/android/app/src/main/cpp/android_input_system.cpp @@ -12,6 +12,22 @@ AndroidInputSystem::AndroidInputSystem() std::strncpy(m_mouseDetails.name, "Touchscreen", MAX_NAME_LENGTH); m_mouseDetails.name[MAX_NAME_LENGTH] = '\0'; m_mouseDetails.isAbsolute = true; + + std::memset(&m_virtualJoyDetails, 0, sizeof(m_virtualJoyDetails)); + std::strncpy(m_virtualJoyDetails.name, "Touch Wheel", MAX_NAME_LENGTH); + m_virtualJoyDetails.name[MAX_NAME_LENGTH] = '\0'; + m_virtualJoyDetails.numAxes = 6; + m_virtualJoyDetails.numPOVs = 0; + m_virtualJoyDetails.numButtons = 0; + m_virtualJoyDetails.hasFFeedback = false; + for (int a = 0; a < NUM_JOY_AXES; a++) + { + m_virtualJoyDetails.hasAxis[a] = false; + m_virtualJoyDetails.axisHasFF[a] = false; + std::strncpy(m_virtualJoyDetails.axisName[a], GetDefaultAxisName(a), MAX_NAME_LENGTH); + m_virtualJoyDetails.axisName[a][MAX_NAME_LENGTH] = '\0'; + } + m_virtualJoyDetails.hasAxis[AXIS_X] = true; } AndroidInputSystem::~AndroidInputSystem() @@ -33,6 +49,37 @@ void AndroidInputSystem::SetGunTouchEnabled(bool enabled) std::memset(m_mouseButtonPulseUntilMs, 0, sizeof(m_mouseButtonPulseUntilMs)); } +void AndroidInputSystem::SetVirtualWheelEnabled(bool enabled) +{ + if (m_virtualWheelEnabled == enabled) + return; + m_virtualWheelEnabled = enabled; + m_wheelFingerActive = false; + m_wheelFinger = 0; + m_virtualJoyX = 0; +} + +bool AndroidInputSystem::UseVirtualWheel() const +{ + return m_virtualWheelEnabled && m_controllers.empty(); +} + +void AndroidInputSystem::SetVirtualSteerFromEncoded(float encodedX) +{ + // encodedX comes from Java as [(steer+1)/2], where steer is in [-1,1]. + const float steer = std::clamp((encodedX - 0.5f) * 2.0f, -1.0f, 1.0f); + constexpr float deadzone = 0.08f; + if (std::abs(steer) < deadzone) + { + m_virtualJoyX = 0; + return; + } + + const float scaled = (std::abs(steer) - deadzone) / (1.0f - deadzone); + const float signedScaled = (steer < 0.0f) ? -scaled : scaled; + m_virtualJoyX = (int)std::lround(std::clamp(signedScaled, -1.0f, 1.0f) * 32767.0f); +} + void AndroidInputSystem::ApplyConfig(const Util::Config::Node& config) { auto get = [&](const char* key, const char* fallback) -> std::string { @@ -81,6 +128,9 @@ bool AndroidInputSystem::InitializeSystem() std::memset(m_mouseButtonPulseUntilMs, 0, sizeof(m_mouseButtonPulseUntilMs)); m_gunFingerActive = false; m_gunFinger = 0; + m_wheelFingerActive = false; + m_wheelFinger = 0; + m_virtualJoyX = 0; SDL_GameControllerEventState(SDL_ENABLE); RefreshControllers(); @@ -215,6 +265,32 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down) const float x = tf.x; const float y = tf.y; + // Virtual steering wheel (racing games): encoded in tf.x from Java and keyed by a fixed fingerId. + if (UseVirtualWheel()) + { + constexpr SDL_FingerID kWheelFingerId = 1107; + if (down) + { + if (!m_wheelFingerActive && tf.fingerId == kWheelFingerId) + { + m_wheelFingerActive = true; + m_wheelFinger = tf.fingerId; + SetVirtualSteerFromEncoded(x); + return; + } + } + else + { + if (m_wheelFingerActive && tf.fingerId == m_wheelFinger) + { + SetVirtualSteerFromEncoded(0.5f); + m_wheelFingerActive = false; + m_wheelFinger = 0; + return; + } + } + } + // Tap zones (momentary): // - Bottom-left: Coin (KEY_5) // - Bottom-middle: Start (KEY_1) @@ -320,6 +396,15 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down) void AndroidInputSystem::HandleTouchMotion(const SDL_TouchFingerEvent& tf) { + if (UseVirtualWheel()) + { + if (m_wheelFingerActive && tf.fingerId == m_wheelFinger) + { + SetVirtualSteerFromEncoded(tf.x); + return; + } + } + if (m_gunTouchEnabled) { if (m_gunFingerActive && tf.fingerId == m_gunFinger) @@ -500,11 +585,20 @@ void AndroidInputSystem::RefreshControllers() int AndroidInputSystem::GetNumJoysticks() { + if (UseVirtualWheel()) + return 1; return static_cast(m_controllers.size()); } const JoyDetails* AndroidInputSystem::GetJoyDetails(int joyNum) { + if (UseVirtualWheel()) + { + if (joyNum == ANY_JOYSTICK || joyNum == 0) + return &m_virtualJoyDetails; + return nullptr; + } + if (joyNum < 0 || joyNum >= static_cast(m_controllers.size())) return nullptr; return &m_controllers[static_cast(joyNum)].details; @@ -573,6 +667,13 @@ bool AndroidInputSystem::ButtonPressedFor(const ControllerState& c, int butNum) int AndroidInputSystem::GetJoyAxisValue(int joyNum, int axisNum) { + if (UseVirtualWheel()) + { + if (axisNum == AXIS_X) + return m_virtualJoyX; + return 0; + } + if (m_controllers.empty()) return 0; diff --git a/android/app/src/main/cpp/android_input_system.h b/android/app/src/main/cpp/android_input_system.h index 77f528b..1b3fc81 100644 --- a/android/app/src/main/cpp/android_input_system.h +++ b/android/app/src/main/cpp/android_input_system.h @@ -21,6 +21,7 @@ public: ~AndroidInputSystem() override; void SetGunTouchEnabled(bool enabled); + void SetVirtualWheelEnabled(bool enabled); // Called from the SDL event loop (main thread). void HandleEvent(const SDL_Event& ev); @@ -73,6 +74,9 @@ private: void RefreshControllers(); void CloseControllers(); + bool UseVirtualWheel() const; + void SetVirtualSteerFromEncoded(float encodedX); + void SetKey(SDL_Scancode sc, bool down); void PulseKey(SDL_Scancode sc, uint32_t durationMs); void SetKeys(const DualScancode& sc, bool down); @@ -114,6 +118,12 @@ private: SDL_FingerID m_gunFinger = 0; bool m_gunFingerActive = false; + bool m_virtualWheelEnabled = false; + SDL_FingerID m_wheelFinger = 0; + bool m_wheelFingerActive = false; + int m_virtualJoyX = 0; + JoyDetails m_virtualJoyDetails{}; + MouseDetails m_mouseDetails{}; int m_mouseX = 0; int m_mouseY = 0; diff --git a/android/app/src/main/cpp/native-lib.cpp b/android/app/src/main/cpp/native-lib.cpp index 68f0051..b8184b2 100644 --- a/android/app/src/main/cpp/native-lib.cpp +++ b/android/app/src/main/cpp/native-lib.cpp @@ -316,6 +316,9 @@ struct Super3Host { (game.inputs & (Game::INPUT_GUN1 | Game::INPUT_GUN2 | Game::INPUT_ANALOG_GUN1 | Game::INPUT_ANALOG_GUN2)) != 0; inputSystem.SetGunTouchEnabled(gunGame); + const bool vehicleGame = (game.inputs & (Game::INPUT_VEHICLE | Game::INPUT_HARLEY)) != 0; + inputSystem.SetVirtualWheelEnabled(vehicleGame); + // Apply Supermodel.ini overrides (Global + [ game ]) after the loader has determined game->name. ApplyIniOverrides(game.name); ApplyAndroidHardOverrides(); diff --git a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt index e5d0be2..07d7286 100644 --- a/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt +++ b/android/app/src/main/java/com/izzy2lost/super3/Super3Activity.kt @@ -6,6 +6,7 @@ import android.view.LayoutInflater import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.ImageButton import android.widget.LinearLayout import android.widget.RelativeLayout import java.io.File @@ -64,6 +65,9 @@ class Super3Activity : SDLActivity() { overlay.findViewById(R.id.overlay_pedals)?.visibility = if (isRacing) View.VISIBLE else View.GONE + overlay.findViewById(R.id.overlay_wheel)?.visibility = + if (isRacing) View.VISIBLE else View.GONE + fun nativeTouch(action: Int, fingerId: Int, x: Float, y: Float, p: Float = 1.0f) { SDLActivity.onNativeTouch(0, fingerId, action, x, y, p) } @@ -126,6 +130,44 @@ class Super3Activity : SDLActivity() { // Match the native pedal zone (right-middle), independent of UI placement. bindHeld(R.id.overlay_gas, fingerId = 1103, x = 0.85f, y = 0.35f) bindHeld(R.id.overlay_brake, fingerId = 1104, x = 0.85f, y = 0.80f) + + val wheel = overlay.findViewById(R.id.overlay_wheel) + wheel?.setOnTouchListener { v, ev -> + val w = v.width.toFloat().coerceAtLeast(1f) + val cx = w / 2f + val dx = (ev.x - cx) / cx + val steer = dx.coerceIn(-1f, 1f) + + v.rotation = steer * 75f + + val encodedX = ((steer + 1f) / 2f).coerceIn(0f, 1f) + val encodedY = 0.5f + + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> { + v.alpha = 0.75f + nativeTouch(MotionEvent.ACTION_DOWN, 1107, encodedX, encodedY) + true + } + MotionEvent.ACTION_MOVE -> { + nativeTouch(MotionEvent.ACTION_MOVE, 1107, encodedX, encodedY) + true + } + MotionEvent.ACTION_UP -> { + v.alpha = 1.0f + v.rotation = 0f + nativeTouch(MotionEvent.ACTION_UP, 1107, 0.5f, encodedY) + true + } + MotionEvent.ACTION_CANCEL -> { + v.alpha = 1.0f + v.rotation = 0f + nativeTouch(MotionEvent.ACTION_UP, 1107, 0.5f, encodedY) + true + } + else -> true + } + } } } diff --git a/android/app/src/main/res/drawable/wheel.xml b/android/app/src/main/res/drawable/wheel.xml new file mode 100644 index 0000000..15a091f --- /dev/null +++ b/android/app/src/main/res/drawable/wheel.xml @@ -0,0 +1,3085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/overlay_controls.xml b/android/app/src/main/res/layout/overlay_controls.xml index fe887c1..1c25b9c 100644 --- a/android/app/src/main/res/layout/overlay_controls.xml +++ b/android/app/src/main/res/layout/overlay_controls.xml @@ -21,6 +21,20 @@ android:scaleType="fitCenter" android:src="@drawable/coin" /> + +