added some on screen buttons

This commit is contained in:
izzy2lost
2025-12-18 22:18:28 -05:00
parent af3de1484e
commit f8155ce06e
13 changed files with 4621 additions and 6 deletions
@@ -54,7 +54,7 @@ InputSteeringLeft = KEY_LEFT
InputSteeringRight = KEY_RIGHT
InputSteering = JOY1_XAXIS
InputAccelerator = KEY_W,JOY1_RZAXIS_POS
InputBrake = KEY_S,JOY1_ZAXIS_POS
InputBrake = KEY_X,JOY1_ZAXIS_POS
; Manual transmission (LB/RB + 4-speed on face buttons)
InputGearShiftUp = KEY_Y,JOY1_BUTTON6
@@ -65,7 +65,7 @@ void AndroidInputSystem::ApplyConfig(const Util::Config::Node& config)
m_touchSteerRight = {steerRight, (steerRight != joyRight) ? joyRight : SDL_SCANCODE_UNKNOWN};
m_touchThrottle.a = keySc(get("InputAccelerator", "KEY_W"), SDL_SCANCODE_W);
m_touchBrake.a = keySc(get("InputBrake", "KEY_S"), SDL_SCANCODE_S);
m_touchBrake.a = keySc(get("InputBrake", "KEY_X"), SDL_SCANCODE_X);
}
bool AndroidInputSystem::InitializeSystem()
@@ -217,13 +217,13 @@ void AndroidInputSystem::HandleTouch(const SDL_TouchFingerEvent& tf, bool down)
// Tap zones (momentary):
// - Bottom-left: Coin (KEY_5)
// - Bottom-right: Start (KEY_1)
// - Bottom-middle: Start (KEY_1)
// - Top-left: Service (KEY_F1)
// - Top-right: Test (KEY_F2)
if (down)
{
if (x < 0.25f && y > 0.75f) { PulseKeys(m_touchCoin, 120); return; }
if (x > 0.75f && y > 0.75f) { PulseKeys(m_touchStart, 120); return; }
if (x > 0.40f && x < 0.60f && y > 0.75f) { PulseKeys(m_touchStart, 120); return; }
if (x < 0.25f && y < 0.25f) { PulseKeys(m_touchService, 120); return; }
if (x > 0.75f && y < 0.25f) { PulseKeys(m_touchTest, 120); return; }
}
@@ -108,7 +108,7 @@ private:
DualScancode m_touchSteerRight{SDL_SCANCODE_RIGHT, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchThrottle{SDL_SCANCODE_W, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchBrake{SDL_SCANCODE_S, SDL_SCANCODE_UNKNOWN};
DualScancode m_touchBrake{SDL_SCANCODE_X, SDL_SCANCODE_UNKNOWN};
bool m_gunTouchEnabled = false;
SDL_FingerID m_gunFinger = 0;
@@ -13,6 +13,8 @@ object AssetInstaller {
for (dir in topLevel) {
copyAssetTree(context, dir, File(internalUserRoot, dir))
}
migrateSupermodelIni(File(File(internalUserRoot, "Config"), "Supermodel.ini"))
}
private fun copyAssetTree(context: Context, assetPath: String, dest: File) {
@@ -37,5 +39,24 @@ object AssetInstaller {
copyAssetTree(context, "$assetPath/$child", File(dest, child))
}
}
}
private fun migrateSupermodelIni(ini: File) {
if (!ini.exists()) return
val lines = runCatching { ini.readLines() }.getOrNull() ?: return
var changed = false
val updated =
lines.map { line ->
val trimmed = line.trim()
if (trimmed == "InputBrake = KEY_S,JOY1_ZAXIS_POS") {
changed = true
"InputBrake = KEY_X,JOY1_ZAXIS_POS"
} else {
line
}
}
if (!changed) return
runCatching { ini.writeText(updated.joinToString(System.lineSeparator())) }
}
}
@@ -0,0 +1,120 @@
package com.izzy2lost.super3
import android.util.Xml
import org.xmlpull.v1.XmlPullParser
import java.io.File
import java.io.InputStream
private data class GameInputs(
val name: String,
val parent: String?,
val inputTypes: Set<String>,
)
object GameInputsIndex {
@Volatile
private var cachedFromPath: String? = null
@Volatile
private var cached: Map<String, GameInputs>? = null
fun hasAnyInputType(gamesXmlPath: String, gameName: String, types: Set<String>): Boolean {
val index = load(gamesXmlPath)
val visited = HashSet<String>(8)
var cur: String? = gameName
while (cur != null && visited.add(cur)) {
val def = index[cur]
if (def != null) {
if (def.inputTypes.any { it in types }) return true
cur = def.parent
} else {
break
}
}
return false
}
@Synchronized
private fun load(gamesXmlPath: String): Map<String, GameInputs> {
val existingPath = cachedFromPath
val existing = cached
if (existing != null && existingPath == gamesXmlPath) return existing
val file = File(gamesXmlPath)
val parsed =
if (file.exists()) {
file.inputStream().use { parse(it) }
} else {
emptyMap()
}
cachedFromPath = gamesXmlPath
cached = parsed
return parsed
}
private fun parse(input: InputStream): Map<String, GameInputs> {
val parser = Xml.newPullParser()
parser.setInput(input, null)
val out = HashMap<String, GameInputs>(256)
var event = parser.eventType
var currentGameName: String? = null
var currentParent: String? = null
var inInputs = false
var types: MutableSet<String>? = null
fun finishGame() {
val name = currentGameName ?: return
out[name] = GameInputs(name = name, parent = currentParent, inputTypes = types?.toSet().orEmpty())
}
while (event != XmlPullParser.END_DOCUMENT) {
when (event) {
XmlPullParser.START_TAG -> {
when (parser.name) {
"game" -> {
currentGameName = parser.getAttributeValue(null, "name")
currentParent = parser.getAttributeValue(null, "parent")
inInputs = false
types = null
}
"inputs" -> {
inInputs = true
if (types == null) types = LinkedHashSet()
}
"input" -> {
if (inInputs) {
val t = parser.getAttributeValue(null, "type")?.trim().orEmpty()
if (t.isNotEmpty()) {
if (types == null) types = LinkedHashSet()
types?.add(t)
}
}
}
}
}
XmlPullParser.END_TAG -> {
when (parser.name) {
"inputs" -> inInputs = false
"game" -> {
finishGame()
currentGameName = null
currentParent = null
inInputs = false
types = null
}
}
}
}
event = parser.next()
}
return out
}
}
@@ -1,6 +1,13 @@
package com.izzy2lost.super3
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.RelativeLayout
import java.io.File
import kotlin.concurrent.thread
import org.libsdl.app.SDLActivity
@@ -10,6 +17,8 @@ import org.libsdl.app.SDLActivity
* library specified by SDL_MAIN_LIBRARY (set to "super3" in the manifest).
*/
class Super3Activity : SDLActivity() {
private var overlayView: View? = null
override fun getLibraries(): Array<String> = arrayOf(
"SDL2",
"super3",
@@ -29,6 +38,105 @@ class Super3Activity : SDLActivity() {
return args.toTypedArray()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val root = SDLActivity.getContentView() as? RelativeLayout ?: return
if (overlayView != null) return
val overlay = LayoutInflater.from(this).inflate(R.layout.overlay_controls, root, false)
overlayView = overlay
root.addView(
overlay,
RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
),
)
val game = intent.getStringExtra("gameName").orEmpty()
val gamesXml = intent.getStringExtra("gamesXmlPath").orEmpty()
val isRacing =
game.isNotBlank() &&
gamesXml.isNotBlank() &&
GameInputsIndex.hasAnyInputType(gamesXml, game, setOf("vehicle", "harley"))
overlay.findViewById<LinearLayout>(R.id.overlay_pedals)?.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)
}
fun bindMomentary(viewId: Int, fingerId: Int, x: Float, y: Float) {
val v = overlay.findViewById<View>(viewId) ?: return
v.setOnTouchListener { _, ev ->
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
v.alpha = 0.75f
nativeTouch(MotionEvent.ACTION_DOWN, fingerId, x, y)
true
}
MotionEvent.ACTION_UP -> {
v.alpha = 1.0f
nativeTouch(MotionEvent.ACTION_UP, fingerId, x, y)
true
}
MotionEvent.ACTION_CANCEL -> {
v.alpha = 1.0f
nativeTouch(MotionEvent.ACTION_UP, fingerId, x, y)
true
}
else -> true
}
}
}
fun bindHeld(viewId: Int, fingerId: Int, x: Float, y: Float) {
val v = overlay.findViewById<View>(viewId) ?: return
v.setOnTouchListener { _, ev ->
when (ev.actionMasked) {
MotionEvent.ACTION_DOWN -> {
v.alpha = 0.75f
nativeTouch(MotionEvent.ACTION_DOWN, fingerId, x, y)
true
}
MotionEvent.ACTION_UP -> {
v.alpha = 1.0f
nativeTouch(MotionEvent.ACTION_UP, fingerId, x, y)
true
}
MotionEvent.ACTION_CANCEL -> {
v.alpha = 1.0f
nativeTouch(MotionEvent.ACTION_UP, fingerId, x, y)
true
}
else -> true
}
}
}
// Use synthetic touch IDs that won't collide with real pointer IDs.
bindMomentary(R.id.overlay_coin, fingerId = 1101, x = 0.10f, y = 0.90f)
bindMomentary(R.id.overlay_start, fingerId = 1102, x = 0.50f, y = 0.90f)
bindMomentary(R.id.overlay_service, fingerId = 1105, x = 0.10f, y = 0.10f)
bindMomentary(R.id.overlay_test, fingerId = 1106, x = 0.90f, y = 0.10f)
if (isRacing) {
// 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)
}
}
override fun onDestroy() {
overlayView?.let { v ->
(v.parent as? ViewGroup)?.removeView(v)
}
overlayView = null
super.onDestroy()
}
override fun onResume() {
super.onResume()
applyImmersiveMode()
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Light blue / silver-ish press feedback -->
<item android:color="#66BFD9FF" />
</selector>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/overlay_ripple">
<item android:id="@android:id/mask">
<shape android:shape="oval">
<solid android:color="@android:color/white" />
</shape>
</item>
</ripple>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/overlay_ripple">
<item android:id="@android:id/mask">
<shape>
<corners android:radius="18dp" />
<solid android:color="@android:color/white" />
</shape>
</item>
</ripple>
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/overlay_controls_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:clickable="false"
android:focusable="false"
android:importantForAccessibility="no">
<ImageButton
android:id="@+id/overlay_coin"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_gravity="bottom|start"
android:layout_margin="16dp"
android:background="@drawable/overlay_ripple_circle"
android:contentDescription="Coin"
android:padding="10dp"
android:scaleType="fitCenter"
android:src="@drawable/coin" />
<com.google.android.material.button.MaterialButton
android:id="@+id/overlay_service"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|start"
android:layout_margin="12dp"
android:minHeight="0dp"
android:minWidth="0dp"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp"
android:text="SERVICE"
android:textAllCaps="true"
android:textSize="12sp"
app:rippleColor="@color/overlay_ripple"
app:strokeWidth="2dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/overlay_test"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|end"
android:layout_margin="12dp"
android:minHeight="0dp"
android:minWidth="0dp"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp"
android:text="TEST"
android:textAllCaps="true"
android:textSize="12sp"
app:rippleColor="@color/overlay_ripple"
app:strokeWidth="2dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/overlay_start"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="18dp"
android:minWidth="140dp"
android:text="START"
android:textAllCaps="true"
app:rippleColor="@color/overlay_ripple"
app:strokeWidth="2dp" />
<LinearLayout
android:id="@+id/overlay_pedals"
android:layout_width="92dp"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:clickable="false"
android:focusable="false"
android:orientation="vertical"
android:visibility="gone">
<ImageButton
android:id="@+id/overlay_gas"
android:layout_width="match_parent"
android:layout_height="92dp"
android:background="@drawable/overlay_ripple_rounded"
android:contentDescription="Gas"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/gaspedal" />
<Space
android:layout_width="match_parent"
android:layout_height="10dp" />
<ImageButton
android:id="@+id/overlay_brake"
android:layout_width="match_parent"
android:layout_height="92dp"
android:background="@drawable/overlay_ripple_rounded"
android:contentDescription="Brake"
android:padding="8dp"
android:scaleType="fitCenter"
android:src="@drawable/brakepedal" />
</LinearLayout>
</FrameLayout>