Added timing toggle quick menu on back press

Timing toggle set to NEW can fix some games like eca export, and USA. Fix flickering in sega rally 2. Breaks some other games. Default set to legacy. Quick options menu via back button press.
This commit is contained in:
izzy2lost
2026-01-18 20:39:58 -05:00
parent 62e6efd4f7
commit a281cb2422
14 changed files with 437 additions and 61 deletions
+2
View File
@@ -270,10 +270,12 @@ InputFishingTension = "KEY_T,JOY1_ZAXIS_NEG"
[ dayto2pe ]
PowerPCFrequency = 90
EmulateDSB = 1
PingPongFlipLine = 2
;daytona 2: battle to the edge
[ daytona2 ]
PowerPCFrequency = 90
EmulateDSB = 1
PingPongFlipLine = 2
;Dirt Devils (Export, Revision A)
[ dirtdvls ]
PowerPCFrequency = 60
+4
View File
@@ -40,9 +40,13 @@ namespace Util { namespace Config { class Node; } }
class CRender2D
{
public:
using AndroidTileBlitFn = void (*)(const uint32_t* pixelsARGB, int width, int height, bool alphaBlend);
explicit CRender2D(const Util::Config::Node &config);
~CRender2D() = default;
static void SetAndroidTileBlit(AndroidTileBlitFn fn);
void BeginFrame(void);
void PreRenderFrame(void);
void RenderFrameBottom(void);
+45 -15
View File
@@ -2054,6 +2054,10 @@ void CModel3::RunMainBoardFrame(void)
unsigned lineCycles = frameCycles / 424;
unsigned vBlankCycles = lineCycles * 40;
unsigned dispCycles = lineCycles * 384;
unsigned statusCycles = (unsigned)((float)frameCycles * (0.005f));
const bool legacyTiming =
m_config["LegacyReal3DTiming"].ValueAsDefault<bool>(
m_config["LegacyStatusBit"].ValueAsDefault<bool>(false));
// Scale PPC timer ratio according to speed at which the PowerPC is being emulated so that the observed running frequency of the PPC timer
// registers is more or less correct. This is needed to get the Virtua Striker 2 series of games running at the right speed (they are
@@ -2065,8 +2069,21 @@ void CModel3::RunMainBoardFrame(void)
if (gpusReady)
{
TileGen.BeginVBlank();
GPU.BeginVBlank();
ppc_execute(vBlankCycles);
GPU.BeginVBlank((int)statusCycles);
if (legacyTiming)
{
unsigned gapCycles = (unsigned)((float)frameCycles * 2.5f / 100.0f); // gap between IRQ2 & IRQ 0x40
unsigned offsetCycles = (unsigned)((float)frameCycles * 33.f / 100.0f);
dispCycles = frameCycles - gapCycles - offsetCycles;
ppc_execute(offsetCycles);
IRQ.Assert(0x02); // start at 33% of the frame
ppc_execute(gapCycles); // need a gap between asserting irqs
}
else
{
dispCycles = lineCycles * 384;
ppc_execute(vBlankCycles);
}
/*
* Sound:
@@ -2110,24 +2127,37 @@ void CModel3::RunMainBoardFrame(void)
}
// Run the PowerPC for the active display part of the frame
unsigned pingPongFlipLine = TileGen.ReadRegister(0x08);
for (unsigned i = 0; i < 384; i++)
if (legacyTiming)
{
if (i == pingPongFlipLine)
GPU.FlipPingPongBit();
ppc_execute(dispCycles);
}
else
{
// TileGen reg 0x08 is a line counter; clamp to the visible 0-383 range.
unsigned pingPongFlipLine = TileGen.ReadRegister(0x08) & 0x1FFu;
const int overrideLine = m_config["PingPongFlipLine"].ValueAsDefault<int>(-1);
if (overrideLine >= 0)
pingPongFlipLine = static_cast<unsigned>(overrideLine);
if (pingPongFlipLine >= 384u)
pingPongFlipLine = 383u;
for (unsigned i = 0; i < 384; i++)
{
if (i == pingPongFlipLine)
GPU.FlipPingPongBit();
if (i == 383)
IRQ.Assert(0x02);
if (i == 383)
IRQ.Assert(0x02);
unsigned cycles = lineCycles;
if (dispCycles < cycles)
cycles = dispCycles;
unsigned cycles = lineCycles;
if (dispCycles < cycles)
cycles = dispCycles;
if (cycles > 0)
ppc_execute(cycles);
if (cycles > 0)
ppc_execute(cycles);
if (dispCycles >= cycles)
dispCycles -= cycles;
if (dispCycles >= cycles)
dispCycles -= cycles;
}
}
timings.ppcTicks = CThread::GetTicks() - start;
+30 -3
View File
@@ -164,8 +164,18 @@ static void UpdateRenderConfig(IRender3D *Render3D, uint64_t internalRenderConfi
Render3D->SetSignedShade(shadeIsSigned);
}
void CReal3D::BeginVBlank(void)
void CReal3D::BeginVBlank(int statusCycles)
{
const bool legacyTiming =
m_config["LegacyReal3DTiming"].ValueAsDefault<bool>(
m_config["LegacyStatusBit"].ValueAsDefault<bool>(false));
m_useLegacyStatusBit = legacyTiming;
if (m_useLegacyStatusBit)
{
statusChange = ppc_total_cycles() + statusCycles;
m_evenFrame = !m_evenFrame;
}
m_pingPongCopy = m_pingPong;
if (commandPortWritten)
@@ -828,8 +838,19 @@ uint32_t CReal3D::ReadRegister(unsigned reg)
DebugLog("Real3D: Read reg %X\n", reg);
if (reg == 0)
{
uint32_t ping_pong = (m_pingPong ? 0x02000000 : 0x0);
return 0xfdffffff | ping_pong;
if (m_useLegacyStatusBit)
{
uint32_t ping_pong;
if (m_evenFrame) {
ping_pong = (ppc_total_cycles() >= statusChange ? 0x0 : 0x02000000);
} else {
ping_pong = (ppc_total_cycles() >= statusChange ? 0x02000000 : 0x0);
}
return 0xfdffffff | ping_pong;
}
uint32_t ping_pong = (m_pingPong ? 0x02000000 : 0x0);
return 0xfdffffff | ping_pong;
}
else if (reg >= 20 && reg<=32) { // line of sight registers
@@ -899,6 +920,9 @@ void CReal3D::Reset(void)
m_pingPong = 0;
m_pingPongCopy = 0;
statusChange = 0;
m_evenFrame = false;
m_useLegacyStatusBit = false;
commandPortWritten = false;
m_tilegenDrawFrame = false;
@@ -1065,6 +1089,9 @@ CReal3D::CReal3D(const Util::Config::Node &config)
m_tilegenDrawFrame = false;
m_pingPong = 0;
m_pingPongCopy = 0;
statusChange = 0;
m_evenFrame = false;
m_useLegacyStatusBit = false;
m_vromTextureFIFO[0] = 0;
m_vromTextureFIFO[1] = 0;
m_vromTextureFIFOIdx = 0;
+5 -2
View File
@@ -117,11 +117,11 @@ public:
void LoadState(CBlockFile *SaveState);
/*
* BeginVBlank(void):
* BeginVBlank(statusCycles):
*
* Must be called before the VBlank starts.
*/
void BeginVBlank(void);
void BeginVBlank(int statusCycles);
/*
* EndVBlank(void)
@@ -520,6 +520,9 @@ private:
// Status and command registers
uint32_t m_pingPong;
uint32_t m_pingPongCopy;
uint64_t statusChange = 0;
bool m_evenFrame = false;
bool m_useLegacyStatusBit = false;
// Internal ASIC state
std::unordered_map<ASIC, uint32_t> m_asicID;
@@ -21,6 +21,7 @@ ForceFeedback = 0
Network = 0
SimulateNet = 0
Outputs = none
LegacyReal3DTiming = 1
; Rendering (Android GLES path)
New3DEngine = 1
+11
View File
@@ -148,6 +148,15 @@ protected:
// Emulator host --------------------------------------------------------------
static struct Super3Host* g_host = nullptr;
static GlesPresenter* g_presenter = nullptr;
static void AndroidTileBlit(const uint32_t* pixelsARGB, int width, int height, bool alphaBlend)
{
if (!g_presenter || !pixelsARGB || width <= 0 || height <= 0)
return;
g_presenter->UpdateFrameARGB(pixelsARGB, width, height);
g_presenter->Render(alphaBlend);
}
struct Super3Host {
static constexpr int32_t STATE_FILE_VERSION = 3;
@@ -830,6 +839,8 @@ extern "C" int SDL_main(int argc, char* argv[]) {
}
Super3Host host;
g_presenter = &presenter;
CRender2D::SetAndroidTileBlit(&AndroidTileBlit);
g_host = &host;
// Initialize renderer backends up-front. The core will attach VRAM/palette/register
// pointers later (after it has initialized the tile generator).
@@ -24,6 +24,8 @@ static inline uint32_t ARGB(uint8_t a, uint8_t r, uint8_t g, uint8_t b)
return (uint32_t(a) << 24) | (uint32_t(r) << 16) | (uint32_t(g) << 8) | uint32_t(b);
}
static CRender2D::AndroidTileBlitFn g_tileBlit = nullptr;
template <int bits, bool alphaTest, bool clip>
static inline void DrawTileLine(uint32_t *line,
int pixelOffset,
@@ -143,6 +145,11 @@ static void DrawLayer(uint32_t *pixels, int layerNum, const uint32_t *vram, cons
CRender2D::CRender2D(const Util::Config::Node &config) : m_config(config) {}
void CRender2D::SetAndroidTileBlit(AndroidTileBlitFn fn)
{
g_tileBlit = fn;
}
bool CRender2D::Init(unsigned /*xOffset*/, unsigned /*yOffset*/, unsigned xRes, unsigned yRes, unsigned /*totalXRes*/, unsigned /*totalYRes*/)
{
// Use the core's nominal resolution if present, but fall back to known TG size.
@@ -252,6 +259,10 @@ void CRender2D::RenderFrameBottom(void)
std::memcpy(m_frame.data(), m_bottomSurface.data(), m_frame.size() * sizeof(uint32_t));
else
std::fill(m_frame.begin(), m_frame.end(), ARGB(0xFF, 0, 0, 0));
if (g_tileBlit && m_surfacesPresent.second) {
g_tileBlit(m_bottomSurface.data(), (int)m_xPixels, (int)m_yPixels, false);
}
}
void CRender2D::CompositeTopOntoFrame()
@@ -78,8 +78,16 @@ object AssetInstaller {
"InputAnalogTriggerRight2 = NONE" to "InputAnalogTriggerRight2 = JOY2_ZAXIS_POS,JOY2_BUTTON2",
)
val updated =
lines.map { line ->
lines.mapNotNull { line ->
val trimmed = line.trim()
if (trimmed.startsWith("PingPongFlipLine", ignoreCase = true)) {
changed = true
return@mapNotNull null
}
if (trimmed.startsWith("LegacyStatusBit", ignoreCase = true)) {
changed = true
return@mapNotNull null
}
val repl = replacements[trimmed]
if (repl != null) {
changed = true
@@ -89,7 +97,32 @@ object AssetInstaller {
}
}
val out = ArrayList<String>(updated)
fun ensureKey(section: String, key: String, value: String) {
val sectionIdx = out.indexOfFirst { it.trim().equals(section, ignoreCase = true) }
if (sectionIdx < 0) {
if (out.isNotEmpty() && out.last().isNotBlank()) out.add("")
out.add(section)
out.add("$key = $value")
changed = true
return
}
val endIdx = (sectionIdx + 1 + out.drop(sectionIdx + 1).indexOfFirst { it.trim().startsWith("[") })
.let { if (it <= sectionIdx) out.size else it }
val hasKey = out.subList(sectionIdx + 1, endIdx).any {
it.trim().startsWith("$key", ignoreCase = true)
}
if (hasKey) return
out.add(endIdx, "$key = $value")
changed = true
}
ensureKey("[ Global ]", "LegacyReal3DTiming", "1")
if (!changed) return
runCatching { ini.writeText(updated.joinToString(System.lineSeparator())) }
runCatching { ini.writeText(out.joinToString(System.lineSeparator())) }
}
}
@@ -62,6 +62,7 @@ class MainActivity : AppCompatActivity() {
private lateinit var btnWidescreen: MaterialButton
private lateinit var btnWideBackground: MaterialButton
private lateinit var btnReal3dRenderer: MaterialButton
private lateinit var btnDaytonaTiming: MaterialButton
private lateinit var gamesAdapter: GamesAdapter
@@ -167,6 +168,7 @@ class MainActivity : AppCompatActivity() {
btnWidescreen = headerView.findViewById(R.id.btn_widescreen)
btnWideBackground = headerView.findViewById(R.id.btn_wide_background)
btnReal3dRenderer = headerView.findViewById(R.id.btn_real3d_renderer)
btnDaytonaTiming = headerView.findViewById(R.id.btn_daytona_timing)
val btnShowTouchControls: MaterialButton = headerView.findViewById(R.id.btn_show_touch_controls)
val btnShowShifterOverlay: MaterialButton = headerView.findViewById(R.id.btn_show_shifter_overlay)
val btnGyroSteering: MaterialButton = headerView.findViewById(R.id.btn_gyro_steering)
@@ -196,6 +198,7 @@ class MainActivity : AppCompatActivity() {
btnRescan.setOnClickListener { refreshUi() }
bindVideoSettingsUi()
bindTimingUi()
btnShowTouchControls.isChecked = prefs.getBoolean("overlay_controls_enabled", true)
btnShowTouchControls.setOnClickListener {
@@ -296,6 +299,7 @@ class MainActivity : AppCompatActivity() {
AssetInstaller.ensureInstalled(this, internalUserRoot())
applyVideoSettingsToIni(internalUserRoot(), loadVideoSettings())
applyTimingDefaultToIni(internalUserRoot(), loadTimingPref())
applyViewMode(viewMode)
refreshUi()
@@ -687,6 +691,45 @@ class MainActivity : AppCompatActivity() {
)
}
private fun loadTimingPref(): Boolean {
if (prefs.contains("timing_legacy_default")) {
return prefs.getBoolean("timing_legacy_default", true)
}
return prefs.getBoolean("daytona_timing_legacy", true)
}
private fun updateTimingLabel(legacy: Boolean) {
btnDaytonaTiming.text =
if (legacy) {
getString(R.string.timing_legacy)
} else {
getString(R.string.timing_new)
}
}
private fun bindTimingUi() {
val legacy = loadTimingPref()
btnDaytonaTiming.isChecked = legacy
updateTimingLabel(legacy)
btnDaytonaTiming.setOnClickListener {
val enabled = btnDaytonaTiming.isChecked
prefs.edit().putBoolean("timing_legacy_default", enabled).remove("daytona_timing_legacy").apply()
applyTimingDefaultToIni(internalUserRoot(), enabled)
val tree = userTreeUri
if (tree != null) {
thread(name = "Super3SyncSettings") {
UserDataSync.syncInternalIntoTree(this, internalUserRoot(), tree, UserDataSync.DIRS_SETTINGS_ONLY)
}
}
updateTimingLabel(enabled)
Toast.makeText(
this,
"Timing set to ${if (enabled) "Legacy" else "New"} (restart game to apply)",
Toast.LENGTH_SHORT,
).show()
}
}
private fun exactDeviceResolution(widthPx: Int, heightPx: Int): ResolutionOption {
val x = max(1, max(widthPx, heightPx))
val y = max(1, min(widthPx, heightPx))
@@ -790,6 +833,75 @@ class MainActivity : AppCompatActivity() {
file.writeText(out.joinToString("\n"))
}
private fun updateIniSection(file: File, section: String, updates: Map<String, String>) {
val lines = if (file.exists()) file.readLines() else emptyList()
val out = ArrayList<String>(lines.size + updates.size + 8)
fun isSectionHeader(s: String): Boolean {
val t = s.trim()
return t.startsWith("[") && t.endsWith("]")
}
fun sectionName(s: String): String {
return s.trim().removePrefix("[").removeSuffix("]").trim()
}
val targetStart = lines.indexOfFirst { isSectionHeader(it) && sectionName(it).equals(section, ignoreCase = true) }
if (targetStart < 0) {
out.addAll(lines)
if (out.isNotEmpty() && out.last().isNotBlank()) out.add("")
out.add("[ $section ]")
for ((k, v) in updates) {
out.add("$k = $v")
}
file.parentFile?.mkdirs()
file.writeText(out.joinToString("\n"))
return
}
val targetEnd =
(targetStart + 1 + lines.drop(targetStart + 1).indexOfFirst { isSectionHeader(it) })
.let { if (it <= targetStart) lines.size else it }
out.addAll(lines.take(targetStart + 1))
val existing = HashMap<String, Int>(updates.size)
for (i in (targetStart + 1) until targetEnd) {
val line = lines[i]
val trimmed = line.trim()
if (trimmed.startsWith(";") || trimmed.isBlank()) {
out.add(line)
continue
}
var replaced = false
for ((k, v) in updates) {
val rx = Regex("^\\s*${Regex.escape(k)}\\s*=", RegexOption.IGNORE_CASE)
if (rx.containsMatchIn(line)) {
out.add("$k = $v")
existing[k.lowercase()] = 1
replaced = true
break
}
}
if (!replaced) out.add(line)
}
for ((k, v) in updates) {
if (existing.containsKey(k.lowercase())) continue
out.add("$k = $v")
}
out.addAll(lines.drop(targetEnd))
file.parentFile?.mkdirs()
file.writeText(out.joinToString("\n"))
}
private fun applyTimingDefaultToIni(internalRoot: File, legacy: Boolean) {
val ini = supermodelIniFile(internalRoot)
val value = if (legacy) "1" else "0"
updateIniKeys(ini, mapOf("LegacyReal3DTiming" to value))
}
private fun persistTreePermission(uri: Uri) {
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
try {
@@ -25,6 +25,7 @@ import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.view.SurfaceView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -707,26 +708,8 @@ class Super3Activity : SDLActivity() {
}
private fun handleBackPress() {
if (exitDialogOpen) return
exitDialogOpen = true
updatePauseState()
MaterialAlertDialogBuilder(
this,
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog,
)
.setTitle(R.string.exit_game_title)
.setMessage(R.string.exit_game_message)
.setPositiveButton(R.string.exit_game_confirm) { _, _ ->
finish()
}
.setNegativeButton(R.string.exit_game_cancel) { dialog, _ ->
dialog.dismiss()
}
.setOnDismissListener {
exitDialogOpen = false
updatePauseState()
}
.show()
if (quickMenuOpen || saveDialogOpen || exitDialogOpen || capturingThumbnail) return
showQuickOptionsMenu()
}
private fun updatePauseState() {
@@ -743,16 +726,23 @@ class Super3Activity : SDLActivity() {
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_quick_menu, null)
val btnPauseResume = dialogView.findViewById<MaterialButton>(R.id.btn_pause_resume)
val btnSaveStates = dialogView.findViewById<MaterialButton>(R.id.btn_save_states)
val btnTouchControls = dialogView.findViewById<MaterialButton>(R.id.btn_touch_controls)
val btnGyroSteering = dialogView.findViewById<MaterialButton>(R.id.btn_gyro_steering)
val btnDaytonaTiming = dialogView.findViewById<MaterialButton>(R.id.btn_daytona_timing)
val btnExitGame = dialogView.findViewById<MaterialButton>(R.id.btn_exit_game)
// Set initial text states
btnPauseResume.text = getString(if (userPaused) R.string.quick_menu_resume else R.string.quick_menu_pause)
btnTouchControls.text = getString(if (overlayControlsEnabled) R.string.quick_menu_hide_touch_controls else R.string.quick_menu_show_touch_controls)
btnGyroSteering.text = getString(if (gyroSteeringEnabled) R.string.quick_menu_disable_gyro else R.string.quick_menu_enable_gyro)
val game = gameName
val legacy = if (game.isNotBlank()) {
loadTimingFromIni(game)
} else {
prefs.getBoolean("timing_legacy_default", true)
}
btnDaytonaTiming.text =
getString(if (legacy) R.string.timing_legacy_game else R.string.timing_new_game)
val dialog = MaterialAlertDialogBuilder(
this,
@@ -767,12 +757,6 @@ class Super3Activity : SDLActivity() {
}
.create()
btnPauseResume.setOnClickListener {
userPaused = !userPaused
updatePauseState()
dialog.dismiss()
}
btnSaveStates.setOnClickListener {
dialog.dismiss()
mainHandler.post { showSaveStateDialog() }
@@ -788,6 +772,24 @@ class Super3Activity : SDLActivity() {
dialog.dismiss()
}
btnDaytonaTiming.setOnClickListener {
val current = if (game.isNotBlank()) loadTimingFromIni(game) else prefs.getBoolean("timing_legacy_default", true)
val legacy = !current
if (game.isNotBlank()) {
userDataRoot?.let { root ->
applyGameTimingToIni(root, game, legacy)
}
}
btnDaytonaTiming.text =
getString(if (legacy) R.string.timing_legacy_game else R.string.timing_new_game)
Toast.makeText(
this,
"Timing set to ${if (legacy) "Legacy" else "New"} (restart game to apply)",
Toast.LENGTH_SHORT,
).show()
dialog.dismiss()
}
btnExitGame.setOnClickListener {
dialog.dismiss()
finish()
@@ -818,6 +820,112 @@ class Super3Activity : SDLActivity() {
}
}
private fun applyGameTimingToIni(internalRoot: File, game: String, legacy: Boolean) {
val ini = File(File(internalRoot, "Config"), "Supermodel.ini")
val value = if (legacy) "1" else "0"
updateIniSection(ini, game, mapOf("LegacyReal3DTiming" to value))
}
private fun loadTimingFromIni(game: String): Boolean {
val ini = File(File(userDataRoot ?: return true, "Config"), "Supermodel.ini")
val perGame = readIniSectionValue(ini, game, "LegacyReal3DTiming")?.let { parseBool(it) }
if (perGame != null) return perGame
val global = readIniSectionValue(ini, "global", "LegacyReal3DTiming")?.let { parseBool(it) }
if (global != null) return global
return prefs.getBoolean("timing_legacy_default", true)
}
private fun parseBool(v: String): Boolean? {
return when (v.trim().lowercase()) {
"1", "true", "yes", "on" -> true
"0", "false", "no", "off" -> false
else -> null
}
}
private fun readIniSectionValue(file: File, section: String, key: String): String? {
if (!file.exists()) return null
val lines = file.readLines()
val keyRegex = Regex("^\\s*${Regex.escape(key)}\\s*=\\s*(.*?)\\s*$", RegexOption.IGNORE_CASE)
var inSection = false
for (line in lines) {
val trimmed = line.trim()
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
val name = trimmed.removePrefix("[").removeSuffix("]").trim()
inSection = name.equals(section, ignoreCase = true)
continue
}
if (!inSection) continue
if (trimmed.startsWith(";")) continue
val m = keyRegex.find(line) ?: continue
return m.groupValues[1]
}
return null
}
private fun updateIniSection(file: File, section: String, updates: Map<String, String>) {
val lines = if (file.exists()) file.readLines() else emptyList()
val out = ArrayList<String>(lines.size + updates.size + 8)
fun isSectionHeader(s: String): Boolean {
val t = s.trim()
return t.startsWith("[") && t.endsWith("]")
}
fun sectionName(s: String): String {
return s.trim().removePrefix("[").removeSuffix("]").trim()
}
val targetStart = lines.indexOfFirst { isSectionHeader(it) && sectionName(it).equals(section, ignoreCase = true) }
if (targetStart < 0) {
out.addAll(lines)
if (out.isNotEmpty() && out.last().isNotBlank()) out.add("")
out.add("[ $section ]")
for ((k, v) in updates) {
out.add("$k = $v")
}
file.parentFile?.mkdirs()
file.writeText(out.joinToString("\n"))
return
}
val targetEnd =
(targetStart + 1 + lines.drop(targetStart + 1).indexOfFirst { isSectionHeader(it) })
.let { if (it <= targetStart) lines.size else it }
out.addAll(lines.take(targetStart + 1))
val existing = HashMap<String, Int>(updates.size)
for (i in (targetStart + 1) until targetEnd) {
val line = lines[i]
val trimmed = line.trim()
if (trimmed.startsWith(";") || trimmed.isBlank()) {
out.add(line)
continue
}
var replaced = false
for ((k, v) in updates) {
val rx = Regex("^\\s*${Regex.escape(k)}\\s*=", RegexOption.IGNORE_CASE)
if (rx.containsMatchIn(line)) {
out.add("$k = $v")
existing[k.lowercase()] = 1
replaced = true
break
}
}
if (!replaced) out.add(line)
}
for ((k, v) in updates) {
if (existing.containsKey(k.lowercase())) continue
out.add("$k = $v")
}
out.addAll(lines.drop(targetEnd))
file.parentFile?.mkdirs()
file.writeText(out.joinToString("\n"))
}
private fun handleStartSelectCombo(event: KeyEvent): Boolean {
if (!event.isFromSource(InputDevice.SOURCE_GAMEPAD) && !event.isFromSource(InputDevice.SOURCE_JOYSTICK)) {
return false
@@ -7,22 +7,22 @@
android:paddingTop="8dp"
android:paddingBottom="8dp">
<!-- Pause/Resume -->
<!-- Exit Game -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_pause_resume"
android:id="@+id/btn_exit_game"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="start|center_vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/quick_menu_pause"
android:text="@string/quick_menu_exit_game"
android:textAlignment="viewStart"
app:icon="@drawable/play_pause_24px"
app:icon="@drawable/exit_to_app_24px"
app:iconGravity="start"
app:iconPadding="32dp"
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface" />
app:iconTint="?attr/colorError" />
<!-- Save States -->
<com.google.android.material.button.MaterialButton
@@ -75,21 +75,32 @@
app:iconSize="24dp"
app:iconTint="?attr/colorOnSurface" />
<!-- Exit Game -->
<!-- Daytona Timing -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_exit_game"
android:id="@+id/btn_daytona_timing"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="start|center_vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:text="@string/quick_menu_exit_game"
android:text="@string/timing_legacy"
android:textAlignment="viewStart"
app:icon="@drawable/exit_to_app_24px"
app:icon="@drawable/refresh_24px"
app:iconGravity="start"
app:iconPadding="32dp"
app:iconSize="24dp"
app:iconTint="?attr/colorError" />
app:iconTint="?attr/colorOnSurface" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/timing_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="24dp"
android:paddingEnd="24dp"
android:layout_marginTop="6dp"
android:text="@string/timing_note"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnSurfaceVariant" />
</LinearLayout>
@@ -110,6 +110,24 @@
android:text="3D renderer (Real3D)"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_daytona_timing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:checkable="true"
android:text="@string/timing_legacy"
style="?attr/materialButtonElevatedStyle" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/timing_note"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/timing_note"
android:textAppearance="?attr/textAppearanceBodySmall"
android:textColor="?attr/colorOnPrimary" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/controls_title"
android:layout_width="match_parent"
+6 -1
View File
@@ -18,13 +18,18 @@
<string name="quick_menu_title">Quick options</string>
<string name="quick_menu_pause">Pause</string>
<string name="quick_menu_resume">Resume</string>
<string name="quick_menu_save_states">Save states</string>
<string name="quick_menu_save_states">Save states</string>
<string name="quick_menu_quick_save_format">Quick save (slot %1$d)</string>
<string name="quick_menu_quick_load_format">Quick load (slot %1$d)</string>
<string name="quick_menu_show_touch_controls">Show touch controls</string>
<string name="quick_menu_hide_touch_controls">Hide touch controls</string>
<string name="quick_menu_enable_gyro">Enable gyro steering</string>
<string name="quick_menu_disable_gyro">Disable gyro steering</string>
<string name="timing_legacy">Timing: Legacy</string>
<string name="timing_new">Timing: New</string>
<string name="timing_legacy_game">Timing: Legacy (this game only)</string>
<string name="timing_new_game">Timing: New (this game only)</string>
<string name="timing_note">Note: Changing timing can fix some games and break others. Restart the game after switching.</string>
<string name="quick_menu_exit_game">Exit game</string>
<string name="setup_title">Welcome to SUPER3</string>
<string name="setup_subtitle">Quick setup for your folders.</string>