mirror of
https://github.com/izzy2lost/Super3.git
synced 2026-07-05 15:18:38 -07:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user