mirror of
https://github.com/izzy2lost/Super3.git
synced 2026-07-05 15:18:38 -07:00
added some on screen buttons
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user