diff --git a/app/build.gradle b/app/build.gradle index 749b433..ddef139 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -29,8 +29,8 @@ android { applicationId = "com.izzy2lost.psx2" minSdk = 26 targetSdk = 36 - versionCode 20 - versionName "1.1.8" + versionCode 21 + versionName "1.1.9" // APK base.archivesName = "PSX2_${versionCode}_${new Date().format('yyyyMMddHHmm')}" diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 6c49df3..ff768bc 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -28,6 +28,7 @@ #include "SIO/Pad/PadDualshock2.h" #include "MTGS.h" #include "SDL3/SDL.h" +#include #include #ifdef __ANDROID__ #include "SDL3/SDL.h" @@ -400,7 +401,12 @@ Java_com_izzy2lost_psx2_NativeApp_setPadButton(JNIEnv *env, jclass clazz, default: _key = PadDualshock2::Inputs::PAD_CROSS ; break; } - Pad::SetControllerState(0, static_cast(_key), p_keyPressed ? 1.0 : 0.0); + float value = p_keyPressed ? 1.0f : 0.0f; + if (p_keyPressed && p_range > 0) { + const float denom = (p_range > 255) ? 32766.0f : 255.0f; + value = std::clamp(static_cast(p_range) / denom, 0.0f, 1.0f); + } + Pad::SetControllerState(0, static_cast(_key), value); } extern "C" JNIEXPORT void JNICALL diff --git a/app/src/main/java/com/izzy2lost/psx2/JoystickView.java b/app/src/main/java/com/izzy2lost/psx2/JoystickView.java index 78c6dc3..eb2a38a 100644 --- a/app/src/main/java/com/izzy2lost/psx2/JoystickView.java +++ b/app/src/main/java/com/izzy2lost/psx2/JoystickView.java @@ -19,6 +19,7 @@ public class JoystickView extends View { private final Paint knobPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private float centerX, centerY, radius, knobX, knobY, knobRadius; private boolean isDragging = false; + private int activePointerId = MotionEvent.INVALID_POINTER_ID; private OnMoveListener listener; public JoystickView(Context ctx) { super(ctx); init(); } @@ -72,36 +73,44 @@ public class JoystickView extends View { @Override public boolean onTouchEvent(MotionEvent event) { final int action = event.getActionMasked(); + final int actionIndex = event.getActionIndex(); switch (action) { case MotionEvent.ACTION_DOWN: - case MotionEvent.ACTION_POINTER_DOWN: isDragging = true; + activePointerId = event.getPointerId(0); // fallthrough to move case MotionEvent.ACTION_MOVE: if (isDragging) { - float dx = event.getX() - centerX; - float dy = event.getY() - centerY; - // Clamp to circle - float dist = (float)Math.hypot(dx, dy); - if (dist > radius) { - float scale = radius / dist; - dx *= scale; - dy *= scale; - } - knobX = centerX + dx; - knobY = centerY + dy; - invalidate(); - if (listener != null) { - // Normalize to [-1,1], invert Y so up is negative value (screen y grows down) - float nx = dx / radius; - float ny = dy / radius; - listener.onMove(clamp(nx), clamp(ny), MotionEvent.ACTION_MOVE); + int pointerIndex = 0; + if (activePointerId != MotionEvent.INVALID_POINTER_ID) { + int idx = event.findPointerIndex(activePointerId); + if (idx >= 0) pointerIndex = idx; } + updateFromPointer(event, pointerIndex); + } + return true; + case MotionEvent.ACTION_POINTER_DOWN: + // Ignore additional pointers while dragging; if we aren't dragging, adopt the new pointer. + if (!isDragging) { + isDragging = true; + activePointerId = event.getPointerId(actionIndex); + updateFromPointer(event, actionIndex); + return true; + } + return true; + case MotionEvent.ACTION_POINTER_UP: + // If the active pointer goes up, release. + if (event.getPointerId(actionIndex) == activePointerId) { + activePointerId = MotionEvent.INVALID_POINTER_ID; + isDragging = false; + resetKnob(); + if (listener != null) listener.onMove(0f, 0f, MotionEvent.ACTION_UP); } return true; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: isDragging = false; + activePointerId = MotionEvent.INVALID_POINTER_ID; resetKnob(); if (listener != null) listener.onMove(0f, 0f, MotionEvent.ACTION_UP); return true; @@ -109,6 +118,26 @@ public class JoystickView extends View { return super.onTouchEvent(event); } + private void updateFromPointer(MotionEvent event, int pointerIndex) { + float dx = event.getX(pointerIndex) - centerX; + float dy = event.getY(pointerIndex) - centerY; + // Clamp to circle + float dist = (float) Math.hypot(dx, dy); + if (dist > radius) { + float scale = radius / dist; + dx *= scale; + dy *= scale; + } + knobX = centerX + dx; + knobY = centerY + dy; + invalidate(); + if (listener != null) { + float nx = dx / radius; + float ny = dy / radius; + listener.onMove(clamp(nx), clamp(ny), MotionEvent.ACTION_MOVE); + } + } + private static float clamp(float v) { return Math.max(-1f, Math.min(1f, v)); } private float dp(float d) { diff --git a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java index 8fffab1..07ee4db 100644 --- a/app/src/main/java/com/izzy2lost/psx2/MainActivity.java +++ b/app/src/main/java/com/izzy2lost/psx2/MainActivity.java @@ -76,6 +76,8 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF public static final int ORIENTATION_LANDSCAPE = 1; public static final int ORIENTATION_PORTRAIT = 2; + private static final String PREF_TOUCH_RIGHT_STICK = "touch_right_stick"; + private String m_szGamefile = ""; private boolean mRaLoginPromptScheduled = false; @@ -91,6 +93,10 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF private boolean joyDownPressed = false; private boolean joyLeftPressed = false; private boolean joyRightPressed = false; + private boolean joyRUpPressed = false; + private boolean joyRDownPressed = false; + private boolean joyRLeftPressed = false; + private boolean joyRRightPressed = false; private boolean controllerUiApplied = false; private AlertDialog mBiosPromptDialog = null; private boolean mControllerHintShowing = false; @@ -135,6 +141,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF View btnSettings = findViewById(R.id.btn_settings); // Visibility toggle removed; no dependency on it for constraints View llJoy = findViewById(R.id.ll_pad_joy); + View llRJoy = findViewById(R.id.ll_pad_rjoy); View llDpad = findViewById(R.id.ll_pad_dpad); View llRight = findViewById(R.id.ll_pad_right_buttons); View llSelectStart = findViewById(R.id.ll_pad_select_start); @@ -182,6 +189,31 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF llJoy.setTranslationX(-dp(28)); } + if (llRJoy != null) { + ConstraintLayout.LayoutParams lp = safeCLP(llRJoy); + lp.startToStart = ConstraintLayout.LayoutParams.UNSET; + lp.endToEnd = ConstraintLayout.LayoutParams.UNSET; + lp.endToStart = ConstraintLayout.LayoutParams.UNSET; + lp.topToTop = ConstraintLayout.LayoutParams.UNSET; + lp.topToBottom = ConstraintLayout.LayoutParams.UNSET; + lp.bottomToBottom = ConstraintLayout.LayoutParams.UNSET; + lp.bottomToTop = ConstraintLayout.LayoutParams.UNSET; + + if (orientation == Configuration.ORIENTATION_LANDSCAPE) { + lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; + if (llRight != null) lp.endToStart = llRight.getId(); + else lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID; + lp.setMargins(dp(6), dp(6), dp(6), dp(6)); + } else { + lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID; + if (llRight != null) lp.bottomToTop = llRight.getId(); + else if (llSelectStart != null) lp.bottomToTop = llSelectStart.getId(); + else lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID; + lp.setMargins(dp(0), dp(0), dp(12), dp(6)); + } + llRJoy.setLayoutParams(lp); + } + if (llDpad != null && llJoy != null) { ConstraintLayout.LayoutParams lp = safeCLP(llDpad); // Above-left of joystick for both, with slight spacing @@ -886,6 +918,44 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF }); } + // Optional Right JoystickView (right analog stick) + View joystickRight = findViewById(R.id.joystick_view_right); + if (joystickRight instanceof JoystickView) { + JoystickView jv = (JoystickView) joystickRight; + jv.setOnMoveListener((nx, ny, action) -> { + final float T = 0.3f; + boolean up = ny < -T; + boolean down = ny > T; + boolean left = nx < -T; + boolean right = nx > T; + + if (up != joyRUpPressed) { + sendKeyAction(jv, up ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_R_UP); + joyRUpPressed = up; + } + if (down != joyRDownPressed) { + sendKeyAction(jv, down ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_R_DOWN); + joyRDownPressed = down; + } + if (left != joyRLeftPressed) { + sendKeyAction(jv, left ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_R_LEFT); + joyRLeftPressed = left; + } + if (right != joyRRightPressed) { + sendKeyAction(jv, right ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_R_RIGHT); + joyRRightPressed = right; + } + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + if (joyRUpPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_R_UP); + if (joyRDownPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_R_DOWN); + if (joyRLeftPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_R_LEFT); + if (joyRRightPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_R_RIGHT); + joyRUpPressed = joyRDownPressed = joyRLeftPressed = joyRRightPressed = false; + } + }); + } + //// D-Pad buttons MaterialButton btn_pad_dir_top = findViewById(R.id.btn_pad_dir_top); if(btn_pad_dir_top != null) { @@ -920,16 +990,48 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF // Visibility toggle removed; no global hidden state flag private void setControlsVisible(boolean visible) { + if (!visible) { + releaseVirtualStickInputs(); + } int vis = visible ? View.VISIBLE : View.GONE; View llDpad = findViewById(R.id.ll_pad_dpad); View llRight = findViewById(R.id.ll_pad_right_buttons); View llSelectStart = findViewById(R.id.ll_pad_select_start); View llJoy = findViewById(R.id.ll_pad_joy); + View llRJoy = findViewById(R.id.ll_pad_rjoy); if (llDpad != null) llDpad.setVisibility(vis); if (llRight != null) llRight.setVisibility(vis); if (llSelectStart != null) llSelectStart.setVisibility(vis); if (llJoy != null) llJoy.setVisibility(vis); + if (llRJoy != null) { + boolean show = visible && getSharedPreferences("app_prefs", MODE_PRIVATE).getBoolean(PREF_TOUCH_RIGHT_STICK, false); + llRJoy.setVisibility(show ? View.VISIBLE : View.GONE); + } + } + + private void releaseVirtualStickInputs() { + // Left stick + if (joyUpPressed || joyDownPressed || joyLeftPressed || joyRightPressed) { + try { NativeApp.setPadButton(ControllerInputHandler.PAD_L_UP, 0, false); } catch (Throwable ignored) {} + try { NativeApp.setPadButton(ControllerInputHandler.PAD_L_DOWN, 0, false); } catch (Throwable ignored) {} + try { NativeApp.setPadButton(ControllerInputHandler.PAD_L_LEFT, 0, false); } catch (Throwable ignored) {} + try { NativeApp.setPadButton(ControllerInputHandler.PAD_L_RIGHT, 0, false); } catch (Throwable ignored) {} + joyUpPressed = joyDownPressed = joyLeftPressed = joyRightPressed = false; + } + + // Right stick + releaseVirtualRightStickInputs(); + } + + private void releaseVirtualRightStickInputs() { + if (joyRUpPressed || joyRDownPressed || joyRLeftPressed || joyRRightPressed) { + try { NativeApp.setPadButton(ControllerInputHandler.PAD_R_UP, 0, false); } catch (Throwable ignored) {} + try { NativeApp.setPadButton(ControllerInputHandler.PAD_R_DOWN, 0, false); } catch (Throwable ignored) {} + try { NativeApp.setPadButton(ControllerInputHandler.PAD_R_LEFT, 0, false); } catch (Throwable ignored) {} + try { NativeApp.setPadButton(ControllerInputHandler.PAD_R_RIGHT, 0, false); } catch (Throwable ignored) {} + joyRUpPressed = joyRDownPressed = joyRLeftPressed = joyRRightPressed = false; + } } // Removed visibility toggle function @@ -2183,6 +2285,17 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF try { NativeApp.setHudVisible(isChecked); } catch (Throwable ignored) {} }); } + + // Touch right stick joystick (optional on-screen control) + com.google.android.material.materialswitch.MaterialSwitch swTouchRightStick = header.findViewById(R.id.drawer_sw_touch_right_stick); + if (swTouchRightStick != null && swTouchRightStick.getTag() == null) { + swTouchRightStick.setTag("setup"); + swTouchRightStick.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean(PREF_TOUCH_RIGHT_STICK, isChecked).apply(); + if (!isChecked) releaseVirtualRightStickInputs(); + try { updateUiForControllerPresence(); } catch (Throwable ignored) {} + }); + } } private void refreshDrawerSettings() { @@ -2326,6 +2439,15 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF } catch (Exception e) { android.util.Log.e("MainActivity", "Error refreshing dev HUD switch: " + e.getMessage()); } + + try { + com.google.android.material.materialswitch.MaterialSwitch swTouchRightStick = header.findViewById(R.id.drawer_sw_touch_right_stick); + if (swTouchRightStick != null) { + swTouchRightStick.setChecked(prefs.getBoolean(PREF_TOUCH_RIGHT_STICK, false)); + } + } catch (Exception e) { + android.util.Log.e("MainActivity", "Error refreshing touch right stick switch: " + e.getMessage()); + } } catch (Throwable t) { android.util.Log.e("MainActivity", "Unexpected error in refreshDrawerSettings: " + t.getMessage()); @@ -2826,6 +2948,9 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF private void hideGamepadControls() { View joy = findViewById(R.id.ll_pad_joy); if (joy != null) joy.setVisibility(View.GONE); + + View rjoy = findViewById(R.id.ll_pad_rjoy); + if (rjoy != null) rjoy.setVisibility(View.GONE); View dpad = findViewById(R.id.ll_pad_dpad); if (dpad != null) dpad.setVisibility(View.GONE); diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 5c52a97..b9a25ce 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -9,6 +9,7 @@ @@ -119,29 +120,48 @@ - - + + + + + - - + + - - + + + + @@ -293,6 +294,26 @@ android:layout_margin="2dp" /> + + + + + + @@ -293,6 +294,26 @@ android:layout_margin="2dp" /> + + + + + + + +