mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-09 10:01:50 -04:00
f5235858e9
- On Android, allow game audio to mix with external audio by default instead of demanding exclusive audio focus and freezing when focus changes. - Add an 'AUDIO MODE' option across Gen 1, Gen 2, and Gen 3 options menus and Launcher Settings: - BOTH: Game BGM and SFX mix with external media. - EXT ONLY: In-game BGM is muted while SFX remains active, mixing with external media. - GAME ONLY: Requests exclusive audio focus (AUDIOFOCUS_GAIN), pausing background media. - Guard Gen 3 waitse/waitfanfare audio script waits with timeouts and Audio._suspended checks so script execution never blocks indefinitely when audio is paused or interrupted. - Expose setAudioMixWithSystem / love.audio.setMixWithSystem via JNI / C++ to dynamically toggle Android audio focus behavior based on user setting. - Add unit test suite in tests/engine/audio_focus_mode_test.lua.
537 lines
18 KiB
Lua
537 lines
18 KiB
Lua
-- pokefirered/src/option_menu.c
|
|
|
|
local Options = require("src.core.game3.options")
|
|
local Strings = require("src.core.Strings")
|
|
local RomText = require("src.core.game3.rom_text")
|
|
|
|
local Rows = {}
|
|
|
|
local function cart(ctx)
|
|
return Options.block(ctx.options)
|
|
end
|
|
|
|
local function cartCycle(ctx, key, n, dir)
|
|
local o = cart(ctx)
|
|
local cur = tonumber(o[key]) or 0
|
|
dir = (dir and dir < 0) and -1 or 1
|
|
Options.set({ options = o }, key, ((cur + dir) % n + n) % n)
|
|
return true
|
|
end
|
|
|
|
-- src/option_menu.c:478
|
|
local function cartLabel(ctx, key, tbl)
|
|
local cur = tonumber(cart(ctx)[key]) or 0
|
|
if cur < 0 or cur >= RomText.count(tbl) then return "?" end
|
|
return RomText.at(tbl, cur)
|
|
end
|
|
|
|
-- src/option_menu.c:136
|
|
local function cartName(item)
|
|
return RomText.at("sOptionMenuItemsNames", item)
|
|
end
|
|
|
|
local function volLabel(v)
|
|
v = tonumber(v) or 7
|
|
return v == 0 and Strings("OFF") or tostring(v)
|
|
end
|
|
|
|
local function stepVolume(v, dir)
|
|
return math.max(0, math.min(7, (tonumber(v) or 7) + dir))
|
|
end
|
|
|
|
local FILTERS = { "OFF", "1X", "2X", "3X" }
|
|
|
|
local function gameSpeedLabel(v)
|
|
local GameSpeed = require("src.core.GameSpeed")
|
|
local speed = GameSpeed.clamp(v)
|
|
if speed == 1 then return Strings("NORMAL") end
|
|
return Strings("%dX", speed)
|
|
end
|
|
|
|
local function speedRow(id, label, key)
|
|
return {
|
|
id = id, label = Strings(label),
|
|
value = function(ctx) return gameSpeedLabel(ctx.options[key]) end,
|
|
step = function(ctx, dir)
|
|
local GameSpeed = require("src.core.GameSpeed")
|
|
ctx.options[key] = GameSpeed.cycle(ctx.options[key], dir)
|
|
return true
|
|
end,
|
|
}
|
|
end
|
|
|
|
function Rows.build(ctx, skip)
|
|
local rows = {}
|
|
local function add(row) rows[#rows + 1] = row end
|
|
local function addCart(id, make)
|
|
local swap = skip and skip[id]
|
|
if type(swap) == "table" then add(swap)
|
|
elseif not swap then add(make()) end
|
|
end
|
|
|
|
addCart("textSpeed", function() return {
|
|
id = "textSpeed", label = cartName(0),
|
|
value = function(c) return cartLabel(c, "textSpeed", "sTextSpeedOptions") end,
|
|
step = function(c, dir) return cartCycle(c, "textSpeed", 3, dir) end,
|
|
} end)
|
|
add(speedRow("speedOverworld", "OVERWORLD SPEED", "speedOverworld"))
|
|
add(speedRow("speedBattle", "BATTLE SPEED", "speedBattle"))
|
|
add(speedRow("speedMenu", "MENU SPEED", "speedMenu"))
|
|
|
|
addCart("battleScene", function() return {
|
|
id = "battleScene", label = cartName(1),
|
|
value = function(c) return cartLabel(c, "battleScene", "sBattleSceneOptions") end,
|
|
step = function(c, dir) return cartCycle(c, "battleScene", 2, dir) end,
|
|
} end)
|
|
addCart("battleStyle", function() return {
|
|
id = "battleStyle", label = cartName(2),
|
|
value = function(c) return cartLabel(c, "battleStyle", "sBattleStyleOptions") end,
|
|
step = function(c, dir) return cartCycle(c, "battleStyle", 2, dir) end,
|
|
} end)
|
|
|
|
addCart("sound", function() return {
|
|
id = "sound", label = cartName(3),
|
|
value = function(c) return cartLabel(c, "sound", "sSoundOptions") end,
|
|
step = function(c, dir)
|
|
cartCycle(c, "sound", 2, dir)
|
|
local Audio = require("src.core.game3.audio")
|
|
Audio.applyOptions({ options = cart(c) })
|
|
return true
|
|
end,
|
|
} end)
|
|
add({
|
|
id = "musicVol", label = Strings("MUSIC VOL"),
|
|
value = function(c) return volLabel(c.options.musicVol) end,
|
|
step = function(c, dir)
|
|
c.options.musicVol = stepVolume(c.options.musicVol, dir)
|
|
require("src.core.game3.audio").applyEngineOptions(c.options)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "sfxVol", label = Strings("SFX VOL"),
|
|
value = function(c) return volLabel(c.options.sfxVol) end,
|
|
step = function(c, dir)
|
|
c.options.sfxVol = stepVolume(c.options.sfxVol, dir)
|
|
require("src.core.game3.audio").applyEngineOptions(c.options)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "musicFilter", label = Strings("MUSIC FILTER"),
|
|
value = function(c)
|
|
return Strings(FILTERS[(tonumber(c.options.musicFilter) or 0) + 1] or "OFF")
|
|
end,
|
|
step = function(c, dir)
|
|
local v = (tonumber(c.options.musicFilter) or 0) + (dir < 0 and -1 or 1)
|
|
c.options.musicFilter = (v % 4 + 4) % 4
|
|
require("src.core.game3.audio").applyEngineOptions(c.options)
|
|
return true
|
|
end,
|
|
})
|
|
local AUDIO_MODES = {
|
|
{ "both", "BOTH" },
|
|
{ "external_only", "EXT. ONLY" },
|
|
{ "game_only", "GAME ONLY" },
|
|
}
|
|
add({
|
|
id = "audioMode", label = Strings("AUDIO MODE"),
|
|
value = function(c)
|
|
local cur = c.options.audioMode or "both"
|
|
for _, m in ipairs(AUDIO_MODES) do
|
|
if m[1] == cur then return Strings(m[2]) end
|
|
end
|
|
return Strings("BOTH")
|
|
end,
|
|
step = function(c, dir)
|
|
local cur = c.options.audioMode or "both"
|
|
local idx = 1
|
|
for i, m in ipairs(AUDIO_MODES) do
|
|
if m[1] == cur then idx = i break end
|
|
end
|
|
idx = ((idx - 1 + (dir < 0 and -1 or 1)) % #AUDIO_MODES) + 1
|
|
c.options.audioMode = AUDIO_MODES[idx][1]
|
|
require("src.core.game3.audio").applyEngineOptions(c.options)
|
|
return true
|
|
end,
|
|
})
|
|
|
|
addCart("buttonMode", function() return {
|
|
id = "buttonMode", label = cartName(4),
|
|
value = function(c) return cartLabel(c, "buttonMode", "sButtonTypeOptions") end,
|
|
step = function(c, dir) return cartCycle(c, "buttonMode", 3, dir) end,
|
|
} end)
|
|
add({
|
|
id = "controls", label = Strings("CONTROLS"),
|
|
activate = function(c)
|
|
require("src.ui.game3.screens").get("controls", c.session)
|
|
.show({ game = c.game, session = c.session, options = c.options })
|
|
end,
|
|
})
|
|
addCart("frameType", function() return {
|
|
id = "frameType", label = cartName(5),
|
|
value = function(c)
|
|
return RomText.plain("gText_FrameType") .. string.format("%2d", (tonumber(cart(c).frameType) or 0) + 1) -- src/option_menu.c:496
|
|
end,
|
|
step = function(c, dir)
|
|
local okC, Chrome = pcall(require, "src.ui.game3.chrome")
|
|
-- pokeemerald/src/option_menu.c:518
|
|
cartCycle(c, "frameType", (okC and Chrome and Chrome.userFrameCount) and Chrome.userFrameCount() or 10, dir)
|
|
if okC and Chrome and Chrome.setFrameType then
|
|
Chrome.setFrameType(cart(c).frameType)
|
|
end
|
|
return true
|
|
end,
|
|
} end)
|
|
|
|
add({
|
|
id = "uiLayout", label = Strings("UI LAYOUT"),
|
|
value = function(c)
|
|
return Strings(c.options.uiLayout == "dynamic" and "DYNAMIC" or "CENTERED")
|
|
end,
|
|
step = function(c)
|
|
c.options.uiLayout = (c.options.uiLayout == "dynamic") and "centered" or "dynamic"
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "uiLetterbox", label = Strings("UI LETTERBOX"),
|
|
value = function(c)
|
|
local Letterbox = require("src.render.Letterbox")
|
|
return Strings(Letterbox.label(c.options.uiLetterbox))
|
|
end,
|
|
step = function(c, dir)
|
|
local Letterbox = require("src.render.Letterbox")
|
|
c.options.uiLetterbox = Letterbox.cycle(c.options.uiLetterbox, dir)
|
|
Letterbox.setMode(c.options.uiLetterbox)
|
|
return true
|
|
end,
|
|
})
|
|
for _, slot in ipairs({ "main", "secondary" }) do
|
|
add({
|
|
id = slot == "main" and "shaderfx" or "shaderfx2",
|
|
label = Strings(slot == "main" and "SHADER FX" or "SHADER FX 2"),
|
|
value = function()
|
|
return require("src.ui.game3.shaderfx_menu").slotLabel(slot, 64)
|
|
end,
|
|
activate = function(c)
|
|
require("src.ui.game3.shaderfx_menu").show({ game = c.game, slot = slot })
|
|
end,
|
|
})
|
|
end
|
|
local Pipelines = require("src.render.Pipelines")
|
|
for _, entry in ipairs(Pipelines.list()) do
|
|
local id = entry.id
|
|
if entry.def.present then
|
|
add({
|
|
id = "pipeline:" .. id,
|
|
label = Strings(entry.def.label or id:upper()),
|
|
value = function() return Strings(Pipelines.levelLabel(id)) end,
|
|
step = function(c, dir)
|
|
Pipelines.cycle(id, dir)
|
|
Pipelines.syncOptions(c.options)
|
|
require("src.render.Tilt").setLevel(tonumber(c.options.tilt) or 0)
|
|
return true
|
|
end,
|
|
})
|
|
end
|
|
end
|
|
add({
|
|
id = "videoMode", label = Strings("VIDEO MODE"),
|
|
value = function(c)
|
|
local VideoMode = require("src.core.VideoMode")
|
|
return Strings(VideoMode.modeLabel(c.options.videoMode))
|
|
end,
|
|
step = function(c, dir)
|
|
local VideoMode = require("src.core.VideoMode")
|
|
c.options.videoMode = VideoMode.cycle(c.options.videoMode, dir)
|
|
VideoMode.apply(c.options.videoMode)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "orientation", label = Strings("ORIENTATION"),
|
|
value = function(c)
|
|
local Orientation = require("src.core.Orientation")
|
|
return Strings(Orientation.modeLabel(c.options.orientation))
|
|
end,
|
|
step = function(c, dir)
|
|
local Orientation = require("src.core.Orientation")
|
|
c.options.orientation = Orientation.cycle(c.options.orientation, dir)
|
|
Orientation.apply(c.options.orientation)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "faithfulRes", label = Strings("FAITHFUL RATIO"),
|
|
value = function(c)
|
|
local FaithfulRes = require("src.core.FaithfulRes")
|
|
return Strings(FaithfulRes.label(c.options.faithfulRes))
|
|
end,
|
|
step = function(c, dir)
|
|
local FaithfulRes = require("src.core.FaithfulRes")
|
|
c.options.faithfulRes = FaithfulRes.cycle(c.options.faithfulRes, dir)
|
|
FaithfulRes.apply(c.options.faithfulRes)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "screenPos", label = Strings("SCREEN POS"),
|
|
value = function(c)
|
|
local ScreenPosition = require("src.core.ScreenPosition")
|
|
return Strings(ScreenPosition.label(c.options.screenPos))
|
|
end,
|
|
step = function(c, dir)
|
|
local ScreenPosition = require("src.core.ScreenPosition")
|
|
local nextMode = ScreenPosition.cycle(c.options.screenPos, dir)
|
|
if nextMode == c.options.screenPos then return false end
|
|
c.options.screenPos = nextMode
|
|
ScreenPosition.setMode(nextMode)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "fpsCap", label = Strings("MAX FPS"),
|
|
value = function(c)
|
|
local FrameCap = require("src.core.FrameCap")
|
|
return Strings(FrameCap.label(c.options.fpsCap))
|
|
end,
|
|
step = function(c, dir)
|
|
local FrameCap = require("src.core.FrameCap")
|
|
c.options.fpsCap = FrameCap.cycle(c.options.fpsCap, dir)
|
|
FrameCap.apply(c.options.fpsCap)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "vsync", label = Strings("VSYNC"),
|
|
value = function(c)
|
|
local VSync = require("src.core.VSync")
|
|
return Strings(VSync.label(c.options.vsync))
|
|
end,
|
|
step = function(c, dir)
|
|
local VSync = require("src.core.VSync")
|
|
c.options.vsync = VSync.cycle(c.options.vsync, dir)
|
|
VSync.apply(c.options.vsync)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "logicClock", label = Strings("LOGIC CLOCK"),
|
|
value = function(c)
|
|
local LogicClock = require("src.core.LogicClock")
|
|
return Strings(LogicClock.label(c.options.logicClock))
|
|
end,
|
|
step = function(c, dir)
|
|
local LogicClock = require("src.core.LogicClock")
|
|
c.options.logicClock = LogicClock.cycle(c.options.logicClock, dir)
|
|
LogicClock.apply(c.options.logicClock)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "performance", label = Strings("PERFORMANCE"),
|
|
value = function(c)
|
|
local Performance = require("src.core.Performance")
|
|
return Strings(Performance.label(c.options.performance))
|
|
end,
|
|
step = function(c, dir)
|
|
local Performance = require("src.core.Performance")
|
|
c.options.performance = Performance.cycle(c.options.performance, dir)
|
|
if c.game and c.game.applyOptions then c.game:applyOptions(c.options) end
|
|
return true
|
|
end,
|
|
})
|
|
|
|
add({
|
|
id = "tilt", label = Strings("TILT"),
|
|
value = function(c)
|
|
local Tilt = require("src.render.Tilt")
|
|
return Strings(Tilt.levelLabel(tonumber(c.options.tilt) or 0))
|
|
end,
|
|
step = function(c, dir)
|
|
local Tilt = require("src.render.Tilt")
|
|
local n = #Tilt.ANGLE_LABELS
|
|
local v = ((tonumber(c.options.tilt) or 0) + (dir < 0 and -1 or 1)) % n
|
|
c.options.tilt = (v + n) % n
|
|
Tilt.setLevel(c.options.tilt)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "zoom", label = Strings("ZOOM"),
|
|
value = function(c)
|
|
local Zoom = require("src.render.Zoom")
|
|
return Strings(Zoom.offsetLabel(tonumber(c.options.zoom) or 0))
|
|
end,
|
|
step = function(c, dir)
|
|
local Zoom = require("src.render.Zoom")
|
|
local Renderer = require("src.render.Renderer")
|
|
Zoom.nudgeOptions(c.options, dir, Renderer:fitScale())
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "voidFill", label = Strings("VOID FILL"),
|
|
value = function(c)
|
|
local VoidFill = require("src.core.game3.void_fill")
|
|
return Strings(VoidFill.label(cart(c).voidFill))
|
|
end,
|
|
step = function(c, dir)
|
|
local VoidFill = require("src.core.game3.void_fill")
|
|
local o = cart(c)
|
|
o.voidFill = VoidFill.cycle(o.voidFill, dir)
|
|
VoidFill.setMode(o.voidFill)
|
|
return true
|
|
end,
|
|
})
|
|
local EventIslands = require("src.core.game3.rse.event_islands")
|
|
if EventIslands.available() then add(EventIslands.optionRow()) end
|
|
|
|
add({
|
|
id = "touchControls", label = Strings("TOUCH PAD"),
|
|
value = function(c)
|
|
local t = c.options.touchControls
|
|
local on = not (type(t) == "table" and t.enabled == false)
|
|
return Strings(on and "ON" or "OFF")
|
|
end,
|
|
step = function(c)
|
|
local TouchControls = require("src.core.TouchControls")
|
|
local t = c.options.touchControls
|
|
if type(t) ~= "table" then t = { enabled = true } end
|
|
t.enabled = not (t.enabled ~= false)
|
|
c.options.touchControls = t
|
|
TouchControls:applyOptions(c.options)
|
|
return true
|
|
end,
|
|
})
|
|
add({
|
|
id = "haptics", label = Strings("VIBRATION"),
|
|
value = function(c)
|
|
local TouchControls = require("src.core.TouchControls")
|
|
return Strings(TouchControls.hapticLabel(c.options.haptics))
|
|
end,
|
|
step = function(c, dir)
|
|
local TouchControls = require("src.core.TouchControls")
|
|
c.options.haptics = TouchControls.cycleHaptics(c.options.haptics, dir)
|
|
TouchControls:applyOptions(c.options)
|
|
TouchControls.buzz(c.options.haptics)
|
|
return true
|
|
end,
|
|
})
|
|
-- Manager discoverable home (18-mod-manager-ux), same contract as Gen 1
|
|
-- OptionsMenu: always listed with an installed count, activate opens the
|
|
-- manager. Inert until A; costs a vanilla install a single row.
|
|
add({
|
|
id = "mods", label = Strings("MODS"),
|
|
value = function(c)
|
|
local status = (c.game and c.game.modStatus) or {}
|
|
return Strings("%d INSTALLED", #(status.available or {}))
|
|
end,
|
|
activate = function(c)
|
|
local ModManager = require("src.ui.game3.mod_manager")
|
|
ModManager.show({ game = c.game, session = c.session })
|
|
end,
|
|
})
|
|
add({
|
|
id = "hotbar", label = Strings("KEY BAR"),
|
|
value = function(c) return Strings(c.options.hotbar == false and "OFF" or "ON") end,
|
|
step = function(c)
|
|
local TouchControls = require("src.core.TouchControls")
|
|
c.options.hotbar = (c.options.hotbar == false)
|
|
TouchControls:applyOptions(c.options)
|
|
return true
|
|
end,
|
|
})
|
|
|
|
local Orientation = require("src.core.Orientation")
|
|
local VideoMode = require("src.core.VideoMode")
|
|
local touchEnv = os.getenv("POKEPORT_TOUCH")
|
|
local mobile = Orientation.isAndroid() or Orientation.isIOS()
|
|
local showTouch = touchEnv == "1" or (mobile and touchEnv ~= "0")
|
|
local keep = {}
|
|
for _, row in ipairs(rows) do
|
|
local drop = false
|
|
if row.id == "orientation" and not mobile then drop = true end
|
|
if row.id == "videoMode" and VideoMode.fixedDisplay and VideoMode.fixedDisplay() then
|
|
drop = true
|
|
end
|
|
if (row.id == "touchControls" or row.id == "haptics" or row.id == "hotbar")
|
|
and not showTouch then
|
|
drop = true
|
|
end
|
|
if not drop then keep[#keep + 1] = row end
|
|
end
|
|
return keep
|
|
end
|
|
|
|
Rows.GROUPS = {
|
|
{ id = "group.speed", label = "SPEED",
|
|
members = { "textSpeed", "speedOverworld", "speedBattle", "speedMenu" } },
|
|
{ id = "group.video", label = "VIDEO",
|
|
members = { "uiLayout", "videoMode", "orientation", "faithfulRes",
|
|
"screenPos", "fpsCap", "vsync", "logicClock" } },
|
|
{ id = "group.graphics", label = "GRAPHICS",
|
|
members = { "uiLetterbox", "frameType", "shaderfx", "shaderfx2" } },
|
|
{ id = "group.audio", label = "AUDIO",
|
|
members = { "sound", "musicVol", "sfxVol", "musicFilter" } },
|
|
{ id = "group.battle", label = "BATTLE OPTIONS",
|
|
members = { "battleScene", "battleStyle" } },
|
|
{ id = "group.extras", label = "EXTRAS",
|
|
members = { "tilt", "zoom", "voidFill", "eventTickets" } },
|
|
}
|
|
|
|
Rows.ORDER = {
|
|
"group.speed", "group.video", "group.graphics", "group.audio",
|
|
"performance", "group.battle", "group.extras", "buttonMode", "controls", "mods",
|
|
}
|
|
|
|
function Rows.group(rows, openPage)
|
|
local owner, picked = {}, {}
|
|
for _, g in ipairs(Rows.GROUPS) do
|
|
for _, id in ipairs(g.members) do owner[id] = g end
|
|
picked[g.id] = {}
|
|
end
|
|
for _, row in ipairs(rows) do
|
|
local g = row.id and owner[row.id]
|
|
if g then picked[g.id][#picked[g.id] + 1] = row end
|
|
end
|
|
local made = {}
|
|
for _, g in ipairs(Rows.GROUPS) do
|
|
local members = picked[g.id]
|
|
if #members > 0 then
|
|
made[g.id] = {
|
|
id = g.id, label = Strings(g.label), group = true,
|
|
value = function() return Strings("%d OPTIONS", #members) end,
|
|
activate = function(ctx) openPage(Strings(g.label), members) end,
|
|
}
|
|
end
|
|
end
|
|
local byId, view, taken = {}, {}, {}
|
|
for _, row in ipairs(rows) do
|
|
if row.id and not owner[row.id] then byId[row.id] = row end
|
|
end
|
|
for _, id in ipairs(Rows.ORDER) do
|
|
local row = made[id] or byId[id]
|
|
if row then
|
|
view[#view + 1] = row
|
|
taken[id] = true
|
|
end
|
|
end
|
|
for _, row in ipairs(rows) do
|
|
local id = row.id
|
|
if not (id and (owner[id] or taken[id])) then view[#view + 1] = row end
|
|
end
|
|
return view
|
|
end
|
|
|
|
function Rows.withCart(ctx, cartRows, openPage, exclude)
|
|
local rows = {}
|
|
for _, r in ipairs(Rows.build(ctx, cartRows)) do
|
|
if not (exclude and exclude[r.id]) then rows[#rows + 1] = r end
|
|
end
|
|
return Rows.group(rows, openPage)
|
|
end
|
|
|
|
return Rows
|