mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-09 01:59:15 -04:00
CLOSES #2726, CLOSES #2727, CLOSES #2729, CLOSES #2730, CLOSES #2731, CLOSES #2733, CLOSES #2734, CLOSES #2735, CLOSES #2736, CLOSES #2737, CLOSES #2740
This commit is contained in:
@@ -23,6 +23,18 @@ function Transaction.assetValid(path, body)
|
||||
return be(17) == 1136 and be(21) == 432
|
||||
end
|
||||
|
||||
local function forgetOptions(journal)
|
||||
for _, job in ipairs(journal.jobs) do
|
||||
if job.path == "options.lua" then
|
||||
local SaveData = package.loaded["src.core.SaveData"]
|
||||
if type(SaveData) == "table" and SaveData.invalidateOptionsCache then
|
||||
SaveData.invalidateOptionsCache()
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function write(fs, path, body)
|
||||
local dir = path:match("^(.*)/[^/]+$")
|
||||
if dir and fs.createDirectory then
|
||||
@@ -105,6 +117,7 @@ local function abort(fs, journal, body)
|
||||
local engine = type(sync) == "table" and sync._shared
|
||||
if type(engine) == "table" then engine:noteSaveWritten() end
|
||||
end
|
||||
if wrote then forgetOptions(journal) end
|
||||
local kept, err = quarantine(fs, body)
|
||||
if not kept then return nil, err end
|
||||
return true, false, "A save changed while a Box transfer was pending. The transfer was undone and its record kept at " .. kept .. "."
|
||||
@@ -188,6 +201,7 @@ function Transaction.recover(fs)
|
||||
if not ok or fs.getInfo(job.path .. ".tmp") then return nil, err or "Could not clear a stale save recovery file." end
|
||||
end
|
||||
end
|
||||
forgetOptions(journal)
|
||||
local ok, err = fs.remove(Transaction.PATH)
|
||||
if not ok or fs.getInfo(Transaction.PATH) then
|
||||
return nil, err or "The completed Box transfer journal could not be cleared."
|
||||
|
||||
+175
-25
@@ -75,6 +75,9 @@ local statFrames, statUnderruns, statRestarts = 0, 0, 0
|
||||
local statDepthMin, statDepthSum, statDepthFrames = nil, 0, 0
|
||||
local statWorkerJit, statWorkerXrt = nil, nil
|
||||
local statXrtSum, statXrtCount, statXrtMax = 0, 0, nil
|
||||
local statSyncRenders, statSyncMs, statSyncMsMax = 0, 0, 0
|
||||
local statSyncLast
|
||||
local statPrewarmHits = 0
|
||||
local lastCosted
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -91,8 +94,18 @@ local fxEpoch = 0 -- bumped by invalidate: drops results from a stale def
|
||||
local fxReady = {} -- key -> SoundData | false (renders to nothing)
|
||||
local fxPending = {} -- key -> true while a request is in flight
|
||||
local fxReadyCount = 0
|
||||
local fxReadyOrder = {}
|
||||
local fxOrderCount = 0
|
||||
local FX_READY_MAX = 64
|
||||
local drainEffects, dropEffects
|
||||
local fxPinned = {}
|
||||
local fxPinnedCount = 0
|
||||
local FX_PINNED_MAX = 24
|
||||
local pinRequests = false
|
||||
local fxQueue = {}
|
||||
local fxInFlight = {}
|
||||
local fxInFlightCount = 0
|
||||
local FX_IN_FLIGHT_MAX = 2
|
||||
local drainEffects, dropEffects, pumpEffects
|
||||
|
||||
local function ensureWorker()
|
||||
if workerReady ~= nil then return workerReady end
|
||||
@@ -397,6 +410,14 @@ function ChipAudio.stats()
|
||||
xrtAvg = statXrtCount > 0 and (statXrtSum / statXrtCount) or nil,
|
||||
xrtMax = statXrtMax,
|
||||
buffers = statXrtCount,
|
||||
syncRenders = statSyncRenders,
|
||||
syncRenderMs = statSyncMs,
|
||||
syncRenderMsMax = statSyncMsMax,
|
||||
syncRenderLast = statSyncLast,
|
||||
prewarmHits = statPrewarmHits,
|
||||
prewarmQueued = #fxQueue,
|
||||
prewarmInFlight = fxInFlightCount,
|
||||
prewarmReady = fxReadyCount,
|
||||
}
|
||||
local function num(value, places)
|
||||
if type(value) ~= "number" then return "-" end
|
||||
@@ -404,18 +425,25 @@ function ChipAudio.stats()
|
||||
end
|
||||
stats.line = string.format(
|
||||
"rate=%d worker=%s depth=%s/%d min=%s avg=%s underruns=%d restarts=%d "
|
||||
.. "xrt=%s/%s/%s n=%d",
|
||||
.. "xrt=%s/%s/%s n=%d sync=%d/%sms max=%sms last=%s prewarm=%d q=%d",
|
||||
stats.rate, worker, depth and tostring(depth) or "-", MUSIC_BUFFER_COUNT,
|
||||
statDepthMin and tostring(statDepthMin) or "-", num(average, 1),
|
||||
statUnderruns, statRestarts,
|
||||
num(statWorkerXrt, 3), num(stats.xrtAvg, 3), num(statXrtMax, 3),
|
||||
statXrtCount)
|
||||
statXrtCount, statSyncRenders, num(statSyncMs, 1), num(statSyncMsMax, 1),
|
||||
tostring(statSyncLast or "-"), statPrewarmHits, #fxQueue + fxInFlightCount)
|
||||
return stats
|
||||
end
|
||||
|
||||
function ChipAudio.resetSyncStats()
|
||||
statSyncRenders, statSyncMs, statSyncMsMax = 0, 0, 0
|
||||
statSyncLast = nil
|
||||
statPrewarmHits = 0
|
||||
end
|
||||
|
||||
function ChipAudio.update()
|
||||
if suspended then return end
|
||||
if fxCh then drainEffects() end
|
||||
if fxCh then pumpEffects() end
|
||||
local m = currentMusic
|
||||
if not m then return end
|
||||
if m.threaded then
|
||||
@@ -708,19 +736,59 @@ local function effectKey(data, header, options)
|
||||
}, "|")
|
||||
end
|
||||
|
||||
local function storeReady(key, sd)
|
||||
if fxReady[key] ~= nil then return end
|
||||
if fxPinned[key] then
|
||||
fxReady[key] = sd
|
||||
fxReadyCount = fxReadyCount + 1
|
||||
return
|
||||
end
|
||||
while fxOrderCount >= FX_READY_MAX and #fxReadyOrder > 0 do
|
||||
local oldest = table.remove(fxReadyOrder, 1)
|
||||
if fxReady[oldest] ~= nil and not fxPinned[oldest] then
|
||||
fxReady[oldest] = nil
|
||||
fxReadyCount = fxReadyCount - 1
|
||||
fxOrderCount = fxOrderCount - 1
|
||||
end
|
||||
end
|
||||
if #fxReadyOrder > FX_READY_MAX * 2 then
|
||||
local live = {}
|
||||
for _, k in ipairs(fxReadyOrder) do
|
||||
if fxReady[k] ~= nil and not fxPinned[k] then live[#live + 1] = k end
|
||||
end
|
||||
fxReadyOrder = live
|
||||
end
|
||||
fxReady[key] = sd
|
||||
fxReadyCount = fxReadyCount + 1
|
||||
fxOrderCount = fxOrderCount + 1
|
||||
fxReadyOrder[#fxReadyOrder + 1] = key
|
||||
end
|
||||
|
||||
local function pinEffect(key)
|
||||
if fxPinned[key] or fxPinnedCount >= FX_PINNED_MAX then return end
|
||||
fxPinned[key] = true
|
||||
fxPinnedCount = fxPinnedCount + 1
|
||||
if fxReady[key] ~= nil then fxOrderCount = fxOrderCount - 1 end
|
||||
end
|
||||
|
||||
function drainEffects()
|
||||
if not fxCh then return end
|
||||
while true do
|
||||
local result = fxCh:pop()
|
||||
if not result then return end
|
||||
if result.epoch == fxEpoch and fxPending[result.key] then
|
||||
fxPending[result.key] = nil
|
||||
if result.error then
|
||||
require("src.core.Logger").warn("chip audio prewarm: %s",
|
||||
tostring(result.error))
|
||||
elseif fxReadyCount < FX_READY_MAX then
|
||||
fxReady[result.key] = result.sd or false
|
||||
fxReadyCount = fxReadyCount + 1
|
||||
if result.epoch == fxEpoch then
|
||||
if fxInFlight[result.key] then
|
||||
fxInFlight[result.key] = nil
|
||||
fxInFlightCount = fxInFlightCount - 1
|
||||
end
|
||||
if fxPending[result.key] then
|
||||
fxPending[result.key] = nil
|
||||
if result.error then
|
||||
require("src.core.Logger").warn("chip audio prewarm: %s",
|
||||
tostring(result.error))
|
||||
else
|
||||
storeReady(result.key, result.sd or false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -729,17 +797,53 @@ end
|
||||
function dropEffects()
|
||||
fxEpoch = fxEpoch + 1
|
||||
fxReady, fxPending, fxReadyCount = {}, {}, 0
|
||||
fxReadyOrder, fxQueue, fxInFlight, fxInFlightCount = {}, {}, {}, 0
|
||||
fxOrderCount, fxPinned, fxPinnedCount = 0, {}, 0
|
||||
if fxCh then fxCh:clear() end
|
||||
end
|
||||
|
||||
local function musicHasHeadroom()
|
||||
local m = currentMusic
|
||||
if not (m and m.threaded and m.started) or m.finished or musicHeld then
|
||||
return true
|
||||
end
|
||||
local depth = queuedBuffers(m.source)
|
||||
if not depth then return true end
|
||||
if outCh then depth = depth + outCh:getCount() end
|
||||
return depth >= MUSIC_PREROLL * 2
|
||||
end
|
||||
|
||||
function pumpEffects()
|
||||
drainEffects()
|
||||
while #fxQueue > 0 and fxInFlightCount < FX_IN_FLIGHT_MAX do
|
||||
if not workerAlive() then
|
||||
fxQueue = {}
|
||||
return
|
||||
end
|
||||
if fxInFlightCount > 0 and not musicHasHeadroom() then return end
|
||||
local entry = table.remove(fxQueue, 1)
|
||||
if fxPending[entry.key] then
|
||||
local pushed = pcall(cmdCh.push, cmdCh, entry.request)
|
||||
if pushed then
|
||||
fxInFlight[entry.key] = true
|
||||
fxInFlightCount = fxInFlightCount + 1
|
||||
else
|
||||
fxPending[entry.key] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function requestEffect(data, header, options)
|
||||
if not ensureWorker() or not workerAlive() then return false end
|
||||
local key = effectKey(data, header, options)
|
||||
if not key then return false end
|
||||
drainEffects()
|
||||
if fxReady[key] ~= nil or fxPending[key] then return true end
|
||||
fxCh = fxCh or love.thread.getChannel("chipaudio_fx")
|
||||
local pushed = pcall(cmdCh.push, cmdCh, {
|
||||
drainEffects()
|
||||
if pinRequests then pinEffect(key) end
|
||||
if fxReady[key] ~= nil or fxPending[key] then return true end
|
||||
fxPending[key] = true
|
||||
fxQueue[#fxQueue + 1] = { key = key, request = {
|
||||
cmd = "effect", key = key, epoch = fxEpoch, header = header,
|
||||
options = {
|
||||
frequencyOffset = options.frequencyOffset,
|
||||
@@ -753,9 +857,8 @@ local function requestEffect(data, header, options)
|
||||
channelPitches = ChipSynth.getChannelPitches(),
|
||||
stereo = ChipSynth.getStereo(),
|
||||
sampleRate = sampleRate(),
|
||||
})
|
||||
if not pushed then return false end
|
||||
fxPending[key] = true
|
||||
} }
|
||||
pumpEffects()
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -769,16 +872,46 @@ local function takeEffect(data, header, options)
|
||||
if sd ~= nil then
|
||||
fxReady[key] = nil
|
||||
fxReadyCount = fxReadyCount - 1
|
||||
if not fxPinned[key] then fxOrderCount = fxOrderCount - 1 end
|
||||
return sd
|
||||
end
|
||||
-- about to render it here; the worker's copy would arrive unused
|
||||
fxPending[key] = nil
|
||||
if fxPending[key] then
|
||||
fxPending[key] = nil
|
||||
for i, entry in ipairs(fxQueue) do
|
||||
if entry.key == key then
|
||||
table.remove(fxQueue, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function renderEffect(data, header, options)
|
||||
local function nowMs()
|
||||
local timer = love and love.timer
|
||||
if timer and timer.getTime then return timer.getTime() * 1000 end
|
||||
return os.clock() * 1000
|
||||
end
|
||||
|
||||
local function renderEffect(data, header, options, label)
|
||||
local sd = takeEffect(data, header, options)
|
||||
if sd == nil then sd = ChipSynth.renderEffectData(data, header, options) end
|
||||
if sd == nil then
|
||||
local began = nowMs()
|
||||
sd = ChipSynth.renderEffectData(data, header, options)
|
||||
local ms = nowMs() - began
|
||||
statSyncRenders = statSyncRenders + 1
|
||||
statSyncMs = statSyncMs + ms
|
||||
if ms > statSyncMsMax then statSyncMsMax = ms end
|
||||
statSyncLast = label
|
||||
if STATS then
|
||||
require("src.core.Logger").info("chipaudio: sync render %s %.1fms",
|
||||
tostring(label), ms)
|
||||
end
|
||||
local FixedStep = require("src.core.FixedStep")
|
||||
if ms >= FixedStep.STEP * 1000 then FixedStep:discardCatchup() end
|
||||
else
|
||||
statPrewarmHits = statPrewarmHits + 1
|
||||
end
|
||||
if not sd then return nil end
|
||||
return love.audio.newSource(sd, "static")
|
||||
end
|
||||
@@ -801,7 +934,8 @@ end
|
||||
|
||||
function ChipAudio.newSfx(data, name, pitch, tempo, header, plainFrames)
|
||||
header = header or data.audio.sfx[name]
|
||||
return renderEffect(data, header, sfxOptions(pitch, tempo, plainFrames))
|
||||
return renderEffect(data, header, sfxOptions(pitch, tempo, plainFrames),
|
||||
name)
|
||||
end
|
||||
|
||||
-- `resolved` is a {header|chip, pitch, length} def the caller already worked
|
||||
@@ -810,7 +944,8 @@ end
|
||||
function ChipAudio.newCry(data, species, resolved)
|
||||
local cry = cryDef(data, species, resolved)
|
||||
if not cry then return nil end
|
||||
return renderEffect(data, cry.chip and cry or cry.header, cryOptions(cry))
|
||||
return renderEffect(data, cry.chip and cry or cry.header, cryOptions(cry),
|
||||
"cry:" .. tostring(species))
|
||||
end
|
||||
|
||||
-- Same arguments as newSfx / newCry; returns true when a render was queued
|
||||
@@ -830,11 +965,26 @@ function ChipAudio.prewarmCry(data, species, resolved)
|
||||
return requestEffect(data, header, cryOptions(cry))
|
||||
end
|
||||
|
||||
function ChipAudio.pumpEffects()
|
||||
if suspended or not fxCh then return end
|
||||
pumpEffects()
|
||||
end
|
||||
|
||||
function ChipAudio.prewarmPinned(fn, ...)
|
||||
local was = pinRequests
|
||||
pinRequests = true
|
||||
local ok, err = pcall(fn, ...)
|
||||
pinRequests = was
|
||||
if not ok then error(err, 0) end
|
||||
end
|
||||
|
||||
-- test hook: prewarm bookkeeping
|
||||
function ChipAudio._effectStateForTest()
|
||||
local pending = 0
|
||||
for _ in pairs(fxPending) do pending = pending + 1 end
|
||||
return { ready = fxReadyCount, pending = pending, epoch = fxEpoch }
|
||||
return { ready = fxReadyCount, pending = pending, epoch = fxEpoch,
|
||||
queued = #fxQueue, inFlight = fxInFlightCount,
|
||||
pinned = fxPinnedCount, unpinnedReady = fxOrderCount }
|
||||
end
|
||||
|
||||
-- Two channels for the same reason ChipSynth.renderEffectData renders stereo:
|
||||
|
||||
@@ -1234,6 +1234,7 @@ function Game2:load(opts)
|
||||
pcall(Font.load, self.data)
|
||||
end
|
||||
Strings.load(self.data)
|
||||
self:prewarmSessionSfx()
|
||||
|
||||
-- The boot skeleton (Game2.new built it, before any bus existed) announced
|
||||
-- here rather than at its construction, which is the same spot in the boot
|
||||
@@ -1341,6 +1342,25 @@ function Game2:load(opts)
|
||||
end
|
||||
end
|
||||
|
||||
Game2.SESSION_SFX = {
|
||||
-- engine/overworld/scripting.asm:467
|
||||
"Sfx_Item", "Sfx_KeyItem", "Sfx_GetTm",
|
||||
"Sfx_ReadText2", "Sfx_Menu", "Sfx_Bump", "Sfx_EnterDoor",
|
||||
"Sfx_ExitBuilding", "Sfx_JumpOverLedge", "Sfx_WarpTo", "Sfx_WarpFrom",
|
||||
"Sfx_Save", "Sfx_Transaction", "Sfx_Wrong", "Sfx_SwitchPokemon",
|
||||
"Sfx_GetBadge", "Sfx_Strength",
|
||||
}
|
||||
|
||||
function Game2:prewarmSessionSfx()
|
||||
local Sound = require("src.core.Sound")
|
||||
local ChipAudio = require("src.core.ChipAudio")
|
||||
ChipAudio.prewarmPinned(function()
|
||||
for _, name in ipairs(Game2.SESSION_SFX) do
|
||||
pcall(Sound.prewarmSfx, self.data, name)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function Game2:inFillBoot()
|
||||
-- Entire pre-world cinema (copyright / title / Oak / name / nested NamingScreen)
|
||||
-- draws in GB letterbox space.
|
||||
@@ -1376,6 +1396,8 @@ end
|
||||
|
||||
function Game2:update(dt)
|
||||
require("src.core.DeferredWrite").tick()
|
||||
local ChipAudio = package.loaded["src.core.ChipAudio"]
|
||||
if ChipAudio then ChipAudio.pumpEffects() end
|
||||
-- _UpdateSound is a VBlank job, so it runs at 60Hz off real time whatever the
|
||||
-- logic multiplier is (audio/engine.asm:84, home/vblank.asm:141-143).
|
||||
local step = FixedStep.STEP
|
||||
|
||||
+2
-1
@@ -368,7 +368,8 @@ end
|
||||
|
||||
function Game3:writeOptions()
|
||||
if type(self.options) ~= "table" then return end
|
||||
if SaveData.saveOptions then pcall(SaveData.saveOptions, self.options) end
|
||||
local write = SaveData.saveSessionOptions or SaveData.saveOptions
|
||||
if write then pcall(write, self.options) end
|
||||
end
|
||||
Game3.persistOptions = Game3.writeOptions
|
||||
|
||||
|
||||
+19
-10
@@ -236,21 +236,30 @@ local function windowsModulePath()
|
||||
return api and api.modulePath() or nil
|
||||
end
|
||||
|
||||
-- Restart the whole app. The obvious love.event.quit("restart") re-runs LÖVE's
|
||||
-- boot in-process, which calls love.filesystem.init a second time -- and inside
|
||||
-- an AppImage physfs is already initialized, so that second init throws
|
||||
-- ("Failed to initialize filesystem: already initialized") and the relaunch
|
||||
-- crashes. So on an AppImage we relaunch the executable; the fresh process's
|
||||
-- Boot step mounts any downloaded update exactly as a manual relaunch would.
|
||||
-- Android (#575) and iOS restart in-process: love.quit joins every worker
|
||||
-- first, so no physfs handle outlives lua_close. iOS turns every quit into
|
||||
-- DONE_RESTART in love.cpp, so a bare quit() is the restart there.
|
||||
-- quit("restart") re-inits physfs, which an AppImage refuses ("already initialized"), so Linux execs the binary.
|
||||
function HostShell.loopRestarts()
|
||||
return rawget(_G, "POKEPORT_LOOP_RESTART") == true
|
||||
end
|
||||
|
||||
function HostShell.canRestart()
|
||||
local osName = love and love.system and love.system.getOS and love.system.getOS()
|
||||
return osName ~= "Android" or HostShell.loopRestarts()
|
||||
end
|
||||
|
||||
HostShell.restarting = false
|
||||
|
||||
function HostShell.restart()
|
||||
if not (love and love.event and love.event.quit) then return end
|
||||
HostShell.restarting = true
|
||||
|
||||
local osName = love.system and love.system.getOS and love.system.getOS()
|
||||
if osName == "Android" then
|
||||
love.event.quit("restart")
|
||||
if HostShell.loopRestarts() then
|
||||
love.event.quit("restart")
|
||||
return
|
||||
end
|
||||
if love.system.restartApp and love.system.restartApp() then return end
|
||||
love.event.quit()
|
||||
return
|
||||
end
|
||||
if osName == "iOS" then
|
||||
|
||||
+36
-2
@@ -871,7 +871,36 @@ end
|
||||
-- Both take an optional fs (write/getInfo/read) defaulting to
|
||||
-- love.filesystem, so the mod loader's injected filesystem can carry the
|
||||
-- options round-trip headless (no love global).
|
||||
SaveData.LAUNCHER_KEYS = {
|
||||
"autoReimport", "splashVideo", "splashMute", "themeVideoBg", "reduceMotion",
|
||||
"boxMusicVol", "boxCryVol", "romSources", "lastVersion", "activeCart", "modSort",
|
||||
}
|
||||
|
||||
function SaveData.launcherKeys()
|
||||
local out = {}
|
||||
for _, key in ipairs(SaveData.LAUNCHER_KEYS) do out[#out + 1] = key end
|
||||
local ok, SecretGames = pcall(require, "src.import.SecretGames")
|
||||
if ok and type(SecretGames) == "table" and type(SecretGames.GAMES) == "table" then
|
||||
for _, key in pairs(SecretGames.GAMES) do out[#out + 1] = key end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function SaveData.invalidateOptionsCache()
|
||||
for _, slot in pairs(optionsCache) do slot.rev = slot.rev + 1 end
|
||||
end
|
||||
|
||||
local writeOptions
|
||||
|
||||
function SaveData.saveOptions(opts, fs)
|
||||
return writeOptions(opts, fs, false)
|
||||
end
|
||||
|
||||
function SaveData.saveSessionOptions(opts, fs)
|
||||
return writeOptions(opts, fs, true)
|
||||
end
|
||||
|
||||
writeOptions = function(opts, fs, launcherFromDisk)
|
||||
fs = persistFs(fs)
|
||||
-- #932: options.lua is a WHOLE-FILE rewrite, so a caller that hands over a
|
||||
-- PARTIAL table (just the keys it changed) would silently drop every key it
|
||||
@@ -893,6 +922,11 @@ function SaveData.saveOptions(opts, fs)
|
||||
-- a partial write would push the global value into the cart's bucket.
|
||||
local cartGlobalValues = cartGlobals(onDisk)
|
||||
applyCartOverlay(onDisk)
|
||||
if launcherFromDisk and type(opts) == "table" and type(onDisk) == "table" then
|
||||
for _, key in ipairs(SaveData.launcherKeys()) do
|
||||
opts[key] = deepCopy(onDisk[key])
|
||||
end
|
||||
end
|
||||
local isFull = type(opts) == "table"
|
||||
if isFull then
|
||||
for k in pairs(SaveData.defaultOptions()) do
|
||||
@@ -2720,7 +2754,7 @@ end)
|
||||
function SaveData.saveLiveOptions(data)
|
||||
if type(data) ~= "table" or type(data.options) ~= "table" then return nil end
|
||||
data.options = rememberPlaythroughId(data, data.options)
|
||||
return SaveData.saveOptions(data.options)
|
||||
return SaveData.saveSessionOptions(data.options)
|
||||
end
|
||||
|
||||
-- Game progress only; options are written separately via saveOptions.
|
||||
@@ -2740,7 +2774,7 @@ function SaveData.save(data, mods)
|
||||
SaveData.saveLiveOptions(data)
|
||||
else
|
||||
local opts, changed = rememberPlaythroughId(data)
|
||||
if changed then SaveData.saveOptions(opts) end
|
||||
if changed then SaveData.saveSessionOptions(opts) end
|
||||
end
|
||||
if activeCart and type(data.meta) == "table" then
|
||||
data.meta.cartId = activeCart
|
||||
|
||||
@@ -39,5 +39,17 @@ return function()
|
||||
end
|
||||
K.destroy(t)
|
||||
end),
|
||||
-- pokeruby/src/battle/anim/sfx.c:194
|
||||
sub_812B2B8 = K.wrap(function(t, vm)
|
||||
local isContest = Context.isContest(vm)
|
||||
local pan = isContest and 63 or (P.atkId(vm) % 2 == 0 and -64 or 63)
|
||||
local species
|
||||
if isContest then species = tonumber(P.species(vm, P.atk(vm))) or 0
|
||||
else species = partySpecies(K.battleState(), P.atkId(vm)) end
|
||||
if species ~= 0 then
|
||||
require("src.core.game3.audio").playCry(species, { mode = 4, pan = pan, volume = 125 })
|
||||
end
|
||||
K.destroy(t)
|
||||
end),
|
||||
}
|
||||
end
|
||||
|
||||
@@ -2198,6 +2198,10 @@ local function is_rs_battle()
|
||||
return P.font.nativeLayout == "rs"
|
||||
end
|
||||
|
||||
function Ui.drawMenuFrames(panelMode)
|
||||
BattleChrome.drawMenuFrames(panelMode, is_rs_battle() and "rs" or nil)
|
||||
end
|
||||
|
||||
local function c5to8(x)
|
||||
return (x * 8 + math.floor(x / 4)) / 255
|
||||
end
|
||||
@@ -2804,7 +2808,7 @@ function Ui.draw(w, h)
|
||||
panelMode = "moves"
|
||||
end
|
||||
BattleChrome.drawPanel(panelMode)
|
||||
BattleChrome.drawMenuFrames(panelMode)
|
||||
Ui.drawMenuFrames(panelMode)
|
||||
|
||||
if Ui._mode == "menu" then
|
||||
draw_action_menu(st)
|
||||
|
||||
@@ -2008,6 +2008,7 @@ function BattleTransition.drawWorld(canvas, vw, vh)
|
||||
local fx = BattleTransition._fx
|
||||
G.push("all")
|
||||
G.origin()
|
||||
G.setCanvas(canvas)
|
||||
if fx and fx.def.redraw then
|
||||
local scratch = BattleTransition._scratch
|
||||
if not scratch or scratch:getWidth() ~= vw or scratch:getHeight() ~= vh then
|
||||
|
||||
@@ -360,6 +360,7 @@ local function presentPlanes(game)
|
||||
local vw, vh = Renderer:worldViewSize()
|
||||
drawFieldPlane(game, vw, vh, Renderer)
|
||||
love.graphics.pop()
|
||||
love.graphics.setCanvas(Renderer.worldCanvas)
|
||||
local Transition = package.loaded["src.core.game3.battle_transition"]
|
||||
if Transition and Transition.isActive and Transition.isActive() then
|
||||
Transition.drawWorld(Renderer.worldCanvas, vw, vh)
|
||||
|
||||
@@ -2111,6 +2111,11 @@ function Field.respawnAtHeal(opts)
|
||||
Flags.setVar(Space.store, Space.vm and Space.vm.ctx or nil, Ctx.VAR_LAST_TALKED, healerId)
|
||||
end
|
||||
end
|
||||
if whiteOut and healRow then
|
||||
-- pokeemerald/src/overworld.c:1563
|
||||
local Fade = lazyReq("src.ui.game3.fade")
|
||||
if Fade.active or (Fade.t or 0) > 0 then Fade.begin(Fade.MODE.FROM_BLACK, 1) end
|
||||
end
|
||||
if whiteOut and not healRow then
|
||||
-- pokefirered/src/overworld.c:1558
|
||||
local home = HealLocations.get(1)
|
||||
|
||||
@@ -264,6 +264,8 @@ local function newEventObject(src, neighbor, prepared)
|
||||
eo.originMapGroup = tonumber(def.originMapGroup or def.mapGroup) or mg
|
||||
eo.originMapNum = tonumber(def.originMapNum or def.mapNum) or mn
|
||||
end
|
||||
-- pokeemerald/src/event_object_movement.c:1301
|
||||
eo.spawnElevation = true
|
||||
return eo
|
||||
end
|
||||
|
||||
@@ -737,7 +739,11 @@ function Objects.snapshot()
|
||||
local protos = {}
|
||||
for _, def in ipairs(Objects._defs or {}) do
|
||||
local lid = tonumber(def.localId or def.index) or 0
|
||||
if lid > 0 then protos[lid] = newEventObject(def) end
|
||||
if lid > 0 then
|
||||
local p = newEventObject(def)
|
||||
Objects.spawnElevation(p)
|
||||
protos[lid] = p
|
||||
end
|
||||
end
|
||||
local list = {}
|
||||
for _, lid in ipairs(Objects._order) do
|
||||
@@ -1045,6 +1051,20 @@ function Objects.updateElevation(eo)
|
||||
cx, cy, eo.cellX, eo.cellY)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/event_object_movement.c:7737
|
||||
function Objects.spawnElevation(eo)
|
||||
eo.spawnElevation = nil
|
||||
if eo.fixedPriority then return end
|
||||
local Coll = Collision()
|
||||
if not (Coll and Coll.nextElevation) then return end
|
||||
local cx, cy = eo.cellX, eo.cellY
|
||||
if eo.moving then cx, cy = eo.targetX, eo.targetY end
|
||||
local cur, prev = Coll.nextElevation(eo.mapDef or Coll._mapDef, eo.currentElevation or 0,
|
||||
cx, cy, eo.cellX, eo.cellY)
|
||||
eo.currentElevation = cur
|
||||
if prev then eo.elevation = prev end
|
||||
end
|
||||
|
||||
local function beginStep(eo, tx, ty)
|
||||
eo.moving = true
|
||||
eo.progress = 0
|
||||
@@ -1815,6 +1835,7 @@ function Objects.update(game)
|
||||
for _, lid in ipairs(Objects._order) do
|
||||
local eo = Objects._byId[lid]
|
||||
if eo then
|
||||
if eo.spawnElevation then Objects.spawnElevation(eo) end
|
||||
if eo.bowFrames and eo.bowFrames > 0 then
|
||||
eo.bowFrames = eo.bowFrames - 1
|
||||
if eo.bowFrames <= 0 then eo.bowFrames = nil end
|
||||
@@ -2139,6 +2160,7 @@ function Objects.tickPool(pool, game, ctx)
|
||||
for _, lid in ipairs(pool.order or {}) do
|
||||
local eo = pool.byId[lid]
|
||||
if eo then
|
||||
if eo.spawnElevation then Objects.spawnElevation(eo) end
|
||||
if eo.bowFrames and eo.bowFrames > 0 then
|
||||
eo.bowFrames = eo.bowFrames - 1
|
||||
if eo.bowFrames <= 0 then eo.bowFrames = nil end
|
||||
|
||||
@@ -47,6 +47,8 @@ return {
|
||||
eggHatchText = "Text_EggHatchHuh",
|
||||
-- pokeemerald/src/field_poison.c:76
|
||||
poisonFaintText = "gText_PkmnFainted_FldPsn",
|
||||
-- pokeemerald/src/field_control_avatar.c:551
|
||||
fieldPoisonScript = "EventScript_FieldPoison",
|
||||
-- pokeemerald/src/event_object_movement.c:1927
|
||||
invalidGfx = "OBJ_EVENT_GFX_NINJA_BOY",
|
||||
-- pokeemerald/data/scripts/field_move_scripts.inc:60
|
||||
|
||||
@@ -44,6 +44,8 @@ return {
|
||||
eggHatchText = "UnknownString_81B2C68",
|
||||
-- pokeruby/src/field_poison.c:81
|
||||
poisonFaintText = "fieldPoisonText_PokemonFainted",
|
||||
-- pokeruby/src/field_control_avatar.c:580
|
||||
fieldPoisonScript = "gUnknown_081A14B8",
|
||||
-- pokeruby/src/event_object_movement.c:1767
|
||||
invalidGfx = "OBJ_EVENT_GFX_LITTLE_BOY_1",
|
||||
fieldMoveScripts = true,
|
||||
|
||||
@@ -84,26 +84,6 @@ local function field_white_out_event(session, game)
|
||||
end
|
||||
local Field = package.loaded["src.core.game3.field"] or require("src.core.game3.field")
|
||||
Field.lock()
|
||||
local Rse = require("src.core.game3.rse.init")
|
||||
if Rse.isRse(session) then
|
||||
local Pike = require("src.core.game3.rse.frontier.pike")
|
||||
local Pyramid = require("src.core.game3.rse.frontier.pyramid")
|
||||
local Hill = require("src.core.game3.rse.trainer_hill")
|
||||
if Pike.inBattlePike(session) or Pyramid.inPyramid(session) or Hill.inChallenge(session) then
|
||||
-- pokeemerald/data/scripts/field_poison.inc:23
|
||||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||||
or require("src.core.game3.scripting.space")
|
||||
local key = Space.scriptKey("EventScript_FrontierFieldWhiteOut")
|
||||
if key and Space.startScript(key) then
|
||||
ev.phase = "frontier_script"
|
||||
ev.tick = function()
|
||||
local vm = Space.vm
|
||||
if not vm or not vm.active then onDone() end
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
local BattleBridge = require("src.core.game3.battle_bridge")
|
||||
local save = game and game.save
|
||||
local name = session.name or session.playerName or ""
|
||||
@@ -147,6 +127,23 @@ local function field_white_out_event(session, game)
|
||||
return ev
|
||||
end
|
||||
|
||||
local function field_poison_script_event(scriptName)
|
||||
local ev = { type = "field_poison_script", script = scriptName }
|
||||
ev.run = function(onDone)
|
||||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||||
or require("src.core.game3.scripting.space")
|
||||
local key = Space.scriptKey(scriptName)
|
||||
if not (key and Space.startScript(key)) then
|
||||
error("field poison script did not start: " .. tostring(scriptName))
|
||||
end
|
||||
ev.tick = function()
|
||||
local vm = Space.vm
|
||||
if not vm or not vm.active then onDone() end
|
||||
end
|
||||
end
|
||||
return ev
|
||||
end
|
||||
|
||||
--- Evaluate step counters upon completing a grid step (Walk, Run, Bike, Surf).
|
||||
function StepEvents.onStepTaken(session, game)
|
||||
if not session then return end
|
||||
@@ -241,6 +238,8 @@ function StepEvents.onStepTaken(session, game)
|
||||
psnSteps = 0
|
||||
local anyPoisonDamage = false
|
||||
local faintedMons = {}
|
||||
local profileField = require("src.core.game3.profile").forSession(session).field
|
||||
local poisonScript = profileField and profileField.fieldPoisonScript
|
||||
|
||||
for slotIdx, mon in ipairs(party) do
|
||||
local isEgg = mon.isEgg or (type(mon.egg) == "boolean" and mon.egg)
|
||||
@@ -251,7 +250,9 @@ function StepEvents.onStepTaken(session, game)
|
||||
if not isEgg and isPsn and hp > 0 then
|
||||
anyPoisonDamage = true
|
||||
mon.hp = math.max(0, hp - 1)
|
||||
if mon.hp == 0 then
|
||||
if mon.hp == 0 and poisonScript then
|
||||
faintedMons[#faintedMons + 1] = { slot = slotIdx, mon = mon }
|
||||
elseif mon.hp == 0 then
|
||||
-- pokefirered/src/field_poison.c:36
|
||||
Pokemon.adjustFriendship(mon, Pokemon.FRIENDSHIP_EVENT_FAINT_OUTSIDE_BATTLE,
|
||||
{ mapSec = Pokemon.currentMapSec(session) })
|
||||
@@ -273,6 +274,11 @@ function StepEvents.onStepTaken(session, game)
|
||||
|
||||
-- pokefirered/src/field_control_avatar.c:727 FLDPSN_FNT
|
||||
poisonFainted = #faintedMons > 0
|
||||
if poisonFainted and poisonScript then
|
||||
-- pokeemerald/src/field_control_avatar.c:551
|
||||
push_event(field_poison_script_event(poisonScript))
|
||||
faintedMons = {}
|
||||
end
|
||||
for _, fainted in ipairs(faintedMons) do
|
||||
push_event({
|
||||
type = "poison_faint",
|
||||
@@ -289,7 +295,7 @@ function StepEvents.onStepTaken(session, game)
|
||||
})
|
||||
end
|
||||
-- pokefirered/src/field_poison.c:76
|
||||
if poisonFainted then push_event(field_white_out_event(session, game)) end
|
||||
if poisonFainted and not poisonScript then push_event(field_white_out_event(session, game)) end
|
||||
end
|
||||
end
|
||||
session.vars[psnVar] = psnSteps
|
||||
|
||||
@@ -23,6 +23,54 @@ local SaveData = require("src.core.SaveData")
|
||||
|
||||
local LauncherSettings = {}
|
||||
|
||||
local function copyValue(v)
|
||||
if type(v) ~= "table" then return v end
|
||||
local out = {}
|
||||
for k, val in pairs(v) do out[k] = copyValue(val) end
|
||||
return out
|
||||
end
|
||||
|
||||
local function sameValue(a, b)
|
||||
if type(a) ~= "table" or type(b) ~= "table" then return a == b end
|
||||
for k, v in pairs(a) do
|
||||
if not sameValue(v, b[k]) then return false end
|
||||
end
|
||||
for k in pairs(b) do
|
||||
if a[k] == nil then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function restoreInto(dst, key, value)
|
||||
local cur = dst[key]
|
||||
if type(cur) == "table" and type(value) == "table" then
|
||||
for k in pairs(cur) do cur[k] = nil end
|
||||
for k, v in pairs(value) do cur[k] = copyValue(v) end
|
||||
else
|
||||
dst[key] = copyValue(value)
|
||||
end
|
||||
end
|
||||
|
||||
function LauncherSettings.persist(opts, base)
|
||||
local changed = {}
|
||||
for k, v in pairs(opts) do
|
||||
if not sameValue(v, base[k]) then changed[#changed + 1] = k end
|
||||
end
|
||||
for k in pairs(base) do
|
||||
if opts[k] == nil then changed[#changed + 1] = k end
|
||||
end
|
||||
if #changed == 0 then return true end
|
||||
SaveData.invalidateOptionsCache()
|
||||
local fresh = SaveData.loadOptions()
|
||||
for _, k in ipairs(changed) do fresh[k] = copyValue(opts[k]) end
|
||||
if SaveData.saveOptions(fresh) ~= nil then
|
||||
for _, k in ipairs(changed) do base[k] = copyValue(opts[k]) end
|
||||
return true
|
||||
end
|
||||
for _, k in ipairs(changed) do restoreInto(opts, k, base[k]) end
|
||||
return false
|
||||
end
|
||||
|
||||
local function bgLocked(opts)
|
||||
return opts.battleLayout == "wide" and opts.battleFit == "fill"
|
||||
and opts.battleHud == "extended"
|
||||
@@ -850,7 +898,6 @@ function LauncherSettings.open(hooks, version)
|
||||
})
|
||||
end
|
||||
local RomSources = require("src.import.RomSources")
|
||||
local forgotRoms = false
|
||||
launcher.rows[#launcher.rows + 1] = {
|
||||
label = Strings("Auto Re-import"),
|
||||
value = function()
|
||||
@@ -875,7 +922,6 @@ function LauncherSettings.open(hooks, version)
|
||||
doneText = Strings("Saved ROMs forgotten."),
|
||||
action = function()
|
||||
RomSources.forgetAll(opts)
|
||||
forgotRoms = true
|
||||
return true
|
||||
end,
|
||||
}
|
||||
@@ -888,17 +934,22 @@ function LauncherSettings.open(hooks, version)
|
||||
sections[#sections + 1] = { title = mod.name, rows = rows }
|
||||
end
|
||||
end
|
||||
return {
|
||||
local base = copyValue(opts)
|
||||
local model = {
|
||||
opts = opts,
|
||||
version = version,
|
||||
sections = sections,
|
||||
save = function()
|
||||
if not forgotRoms then
|
||||
opts.romSources = SaveData.loadOptions().romSources
|
||||
end
|
||||
SaveData.saveOptions(opts)
|
||||
end,
|
||||
}
|
||||
model.save = function()
|
||||
if LauncherSettings.persist(opts, base) then
|
||||
model.saveError = nil
|
||||
return true
|
||||
end
|
||||
model.saveError = Strings("Settings could not be saved. Check the log.")
|
||||
model.flash = nil
|
||||
return false
|
||||
end
|
||||
return model
|
||||
end
|
||||
|
||||
return LauncherSettings
|
||||
|
||||
@@ -5716,7 +5716,10 @@ local function buildSettingsModal(imp, m)
|
||||
local x, cy, width = px + pad, py + pad, pw - 2 * pad
|
||||
Kit.textBold("title", Strings("Settings"), x, cy, PAL.heading)
|
||||
local subtitleW = width - m.btnH - gap
|
||||
if model.flash then
|
||||
if model.saveError then
|
||||
Kit.text("small", Kit.ellipsize("small", model.saveError, subtitleW), x,
|
||||
cy + Kit.textHeight("title") + math.floor(3 * m.s), PAL.red)
|
||||
elseif model.flash then
|
||||
Kit.text("small", Kit.ellipsize("small", model.flash, subtitleW), x,
|
||||
cy + Kit.textHeight("title") + math.floor(3 * m.s), PAL.green)
|
||||
else
|
||||
@@ -5832,8 +5835,7 @@ local function buildSettingsModal(imp, m)
|
||||
local aw = math.min(inner, chipWidth(label, m))
|
||||
local function run()
|
||||
if row.action() ~= false then
|
||||
model.save()
|
||||
if row.doneText then model.flash = row.doneText end
|
||||
if model.save() ~= false and row.doneText then model.flash = row.doneText end
|
||||
end
|
||||
end
|
||||
control(rx - aw, aw, key .. "-act", label, {
|
||||
|
||||
@@ -257,7 +257,7 @@ local function sha1(data)
|
||||
end
|
||||
|
||||
local function readExternalPath(path)
|
||||
local file, openError = io.open(path, "rb")
|
||||
local file, openError = require("src.core.SaveData").openNative(path, "rb")
|
||||
if not file then return nil, openError end
|
||||
local data = file:read("*a")
|
||||
file:close()
|
||||
@@ -2458,7 +2458,18 @@ function RomImporter:_offerReimport(version, cand)
|
||||
end
|
||||
local name = GameVersion.info(version).displayName
|
||||
local lines
|
||||
if cand.pick then
|
||||
if cand.missing then
|
||||
local why = cand.missing ~= "changed"
|
||||
and Strings("The saved ROM at %s could not be found or opened.",
|
||||
RomSources.shortPath(cand.path))
|
||||
or Strings("The saved ROM at %s no longer matches this game.",
|
||||
RomSources.shortPath(cand.path))
|
||||
lines = {
|
||||
Strings("%s needs its ROM imported again.", name),
|
||||
why,
|
||||
Strings("Choose the ROM file to re-import it?"),
|
||||
}
|
||||
elseif cand.pick then
|
||||
lines = {
|
||||
Strings("%s needs its ROM imported again.", name),
|
||||
Strings("Choose the ROM file to re-import it?"),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Logger = require("src.core.Logger")
|
||||
|
||||
local RomSources = {}
|
||||
|
||||
@@ -236,13 +237,22 @@ end
|
||||
|
||||
local function readSource(rec)
|
||||
if rec.kept then return RomSources.readKept(rec.path) end
|
||||
local file = io.open(rec.path, "rb")
|
||||
local file = SaveData.openNative(rec.path, "rb")
|
||||
if not file then return nil end
|
||||
local data = file:read("*a")
|
||||
file:close()
|
||||
return data
|
||||
end
|
||||
|
||||
local function unwrapArchive(data, rec)
|
||||
local RomArchive = require("src.import.RomArchive")
|
||||
if not RomArchive.kind(data) then return data end
|
||||
local bytes, why = RomArchive.unwrap(data, rec.path:match("[^/\\]+$"), {
|
||||
prefer = function(candidate) return sha1(candidate) == rec.sha1 end,
|
||||
})
|
||||
return bytes, why
|
||||
end
|
||||
|
||||
function RomSources.candidate(version, mobile, opts)
|
||||
local rec = RomSources.get(version, opts)
|
||||
if not rec or type(rec.sha1) ~= "string" then return nil end
|
||||
@@ -251,11 +261,25 @@ function RomSources.candidate(version, mobile, opts)
|
||||
return nil
|
||||
end
|
||||
local ok, data = pcall(readSource, rec)
|
||||
local okHash, digest = false, nil
|
||||
if ok and data then okHash, digest = pcall(sha1, data) end
|
||||
if not okHash or digest ~= rec.sha1 then
|
||||
local reason
|
||||
if not ok or type(data) ~= "string" then
|
||||
reason = "missing"
|
||||
else
|
||||
local okUnwrap, bytes, why = pcall(unwrapArchive, data, rec)
|
||||
if not okUnwrap or not bytes then
|
||||
reason = "archive"
|
||||
Logger.warn("re-import source %s: %s", tostring(rec.path),
|
||||
tostring(okUnwrap and why or bytes))
|
||||
else
|
||||
local okHash, digest = pcall(sha1, bytes)
|
||||
if not okHash or digest ~= rec.sha1 then reason = "changed" end
|
||||
end
|
||||
end
|
||||
if reason then
|
||||
Logger.warn("re-import source for %s is unusable (%s): %s",
|
||||
tostring(version), reason, tostring(rec.path))
|
||||
if rec.kept then return { pick = true, broken = true } end
|
||||
return nil
|
||||
return { pick = true, missing = reason, path = rec.path }
|
||||
end
|
||||
return { path = rec.path, kept = rec.kept == true }
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ local Rs = {}
|
||||
local function tables(game, revision)
|
||||
local V = VersionsRse.new(game, revision.build)
|
||||
local sym, count = V.sym, V.count
|
||||
V.CACHE_VERSION = 4
|
||||
V.CACHE_VERSION = 5
|
||||
V.NATIVE_VERSION = 2
|
||||
V.OW_VERSION = 1
|
||||
V.ANIM_VERSION = 1
|
||||
|
||||
@@ -27,16 +27,9 @@ return function(V)
|
||||
SURF_OPPONENT = {gfx = sym("gBattleAnimBackgroundImage_Surf"), pal = sym("gBattleAnimBackgroundPalette_Surf"), map = sym("gUnknown_08E70968")},
|
||||
}}
|
||||
V.ANIM_NAMES, V.ANIM_TAG_NAMES = Names, Names.tagNames
|
||||
local function byPrefix(prefix)
|
||||
local out = {}
|
||||
for n, id in pairs(require("src.core.game3.constants").of(V.GAME).battle.byName) do
|
||||
if n:sub(1, #prefix) == prefix then out[id] = n:sub(8) end
|
||||
end
|
||||
return out
|
||||
end
|
||||
V.BATTLE_ANIM_STATUS_NAMES = byPrefix("B_ANIM_STATUS_")
|
||||
V.BATTLE_ANIM_GENERAL_NAMES = {[0] = "CASTFORM_CHANGE", "STATS_CHANGE", "SUBSTITUTE_FADE", "SUBSTITUTE_APPEAR", "POKEBLOCK_THROW", "ITEM_KNOCKOFF", "TURN_TRAP", "ITEM_EFFECT", "SMOKEBALL_ESCAPE", "HANGED_ON", "RAIN_CONTINUES", "SUN_CONTINUES", "SANDSTORM_CONTINUES", "HAIL_CONTINUES", "LEECH_SEED_DRAIN", "MON_HIT", "ITEM_STEAL", "SNATCH_MOVE", "FUTURE_SIGHT_HIT", "DOOM_DESIRE_HIT", "FOCUS_PUNCH_SETUP", "INGRAIN_HEAL", "WISH_HEAL"}
|
||||
V.BATTLE_ANIM_SPECIAL_NAMES = {[0] = "LVL_UP", "SWITCH_OUT_PLAYER_MON", "SWITCH_OUT_OPPONENT_MON", "BALL_THROW", "BALL_THROW_WITH_TRAINER", "SUBSTITUTE_TO_MON", "MON_TO_SUBSTITUTE"}
|
||||
V.BATTLE_ANIM_STATUS_NAMES = Names.statusNames
|
||||
V.BATTLE_ANIM_GENERAL_NAMES = Names.generalNames
|
||||
V.BATTLE_ANIM_SPECIAL_NAMES = Names.specialNames
|
||||
local templates, callbacks, tasks = {}, {}, {}
|
||||
local function functionOffset(name)
|
||||
if S.hasFunc(name) then return S.funcOff(name) end
|
||||
|
||||
+860
-807
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
||||
local K = require("src.import.gba.rse.boot_gfx")
|
||||
local A = require("src.import.gba.rs.assets")
|
||||
local M = {SUB = "rse/bag", FILES = {"bg.png", "bg_female.png", "bag_male.png", "bag_female.png", "ball.png", "select.png",
|
||||
"arrows_vertical.png", "arrows_horizontal.png", "hm.png", "number.png", "indicator_0.png", "indicator_1.png", "list_cursor.png"}}
|
||||
"arrows_vertical.png", "arrows_horizontal.png", "hm.png", "number.png", "indicator_0.png", "indicator_1.png",
|
||||
"indicator_0_female.png", "indicator_1_female.png", "list_cursor.png"}}
|
||||
for i = 0, 4 do for _, g in ipairs({"male", "female"}) do M.FILES[#M.FILES + 1] = "label_" .. i .. "_" .. g .. ".png" end end
|
||||
M.REQUIRED = K.required(M.SUB, M.FILES)
|
||||
local N, H = "item_menu.o:", "menu_helpers.o:"
|
||||
@@ -39,11 +40,15 @@ function M.run(rom, cache, opts)
|
||||
local words = {}; for _, word in ipairs(g[2]) do words[#words + 1] = string.char(word % 256, math.floor(word / 256)) end
|
||||
local pixels, gw, gh = K.bakeText(gfx, table.concat(words), g[3], g[4], {linear = true, mapWidth = g[3]})
|
||||
local rec = {png = c:png(g[1] .. ".png", gw, gh, pixels, male, true), w = gw, h = gh, words = g[2], paletteBank = math.floor(g[2][1] / 4096)}
|
||||
if g[1]:find("indicator", 1, true) then man.indicators[tonumber(g[1]:sub(-1))] = rec else man.glyphs[g[1]] = rec end
|
||||
if g[1]:find("indicator", 1, true) then
|
||||
rec.female = c:png(g[1] .. "_female.png", gw, gh, pixels, female, true)
|
||||
man.indicators[tonumber(g[1]:sub(-1))] = rec
|
||||
else man.glyphs[g[1]] = rec end
|
||||
end
|
||||
man.indicatorGeometry = {x = 40, y = 72, spacing = 8, pockets = 5}
|
||||
man.hm, man.number = man.glyphs.hm.png, man.glyphs.number.png
|
||||
man.indicators.idle, man.indicators.selected = man.indicators[0].png, man.indicators[1].png
|
||||
man.indicators.idleFemale, man.indicators.selectedFemale = man.indicators[0].female, man.indicators[1].female
|
||||
local tableOff = c:off("gSubspriteTables_842F6C0") + 15 * 8
|
||||
local count, parts, tiles = c:u8(tableOff), {}, c:raw("OutlineCursorTiles_12")
|
||||
local sub = assert(c:ptr(tableOff + 4))
|
||||
|
||||
@@ -6,7 +6,7 @@ local M = {REQUIRED = Base.REQUIRED}
|
||||
function M.run(rom, cache, opts)
|
||||
local c = A.context(rom, cache, opts, "pokemon/battle_anims")
|
||||
local templates = {}
|
||||
for name in pairs(Names.templates) do
|
||||
for name, canonical in pairs(Names.templates) do
|
||||
local off = c:off(name)
|
||||
local row = c:readTemplate(name)
|
||||
local affine, tableOff = {}, c:ptr(off + 16)
|
||||
@@ -21,7 +21,8 @@ function M.run(rom, cache, opts)
|
||||
end
|
||||
row.affineAnims, row.romOffset = affine, off
|
||||
row.images = nil
|
||||
templates[name] = row
|
||||
assert(templates[canonical] == nil, "RS template canonical name collides: " .. canonical)
|
||||
templates[canonical] = row
|
||||
end
|
||||
local result = Base.run(rom, cache, opts)
|
||||
local path = c:path("pack.lua")
|
||||
|
||||
@@ -882,8 +882,8 @@ end
|
||||
function ManagerState:restartGame()
|
||||
if self.game.restartWithMods then
|
||||
self.game:restartWithMods()
|
||||
elseif love.event and love.event.quit then
|
||||
love.event.quit("restart")
|
||||
else
|
||||
require("src.core.HostShell").restart()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -493,10 +493,22 @@ BattleChrome.RSE_MENU_FRAMES = {
|
||||
moves = { { 1, 15, 18, 4 }, { 21, 15, 8, 4 } },
|
||||
}
|
||||
|
||||
-- pokeruby/graphics/interface/menu_map.bin
|
||||
BattleChrome.RS_MENU_FRAMES = {
|
||||
menu = { { 18, 15, 11, 4 } },
|
||||
moves = { { 1, 15, 20, 4 }, { 23, 15, 6, 4 } },
|
||||
}
|
||||
|
||||
function BattleChrome.menuFrameRects(mode, layout)
|
||||
local set = layout == "rs" and BattleChrome.RS_MENU_FRAMES or BattleChrome.RSE_MENU_FRAMES
|
||||
return set[mode]
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_bg.c:744
|
||||
function BattleChrome.drawMenuFrames(mode)
|
||||
-- pokeruby/src/battle_bg.c:266
|
||||
function BattleChrome.drawMenuFrames(mode, layout)
|
||||
if not BattleChrome.isRse() then return end
|
||||
local rects = BattleChrome.RSE_MENU_FRAMES[mode]
|
||||
local rects = BattleChrome.menuFrameRects(mode, layout)
|
||||
if not rects then return end
|
||||
local Chrome = require("src.ui.game3.chrome")
|
||||
for _, r in ipairs(rects) do
|
||||
|
||||
@@ -383,6 +383,12 @@ end
|
||||
local function drawText(str, x, y, colors, width)
|
||||
Font.draw(str, x, y, {font = "normal", colors = colors, maxWidth = width or 120, linePitch = 16})
|
||||
end
|
||||
local function quantity(qty, multX, y, colors)
|
||||
local count = tostring(qty)
|
||||
drawText("×", multX, y, colors)
|
||||
-- pokeruby/src/text.c:3474
|
||||
drawText(count, 232 - Font.measure(count, {font = "normal"}), y, colors)
|
||||
end
|
||||
local function frame(left, top, width, height) Chrome.stdFrame(left + 1, top + 1, width - 1, height - 1) end
|
||||
|
||||
function M.draw(Bag)
|
||||
@@ -409,7 +415,10 @@ function M.draw(Bag)
|
||||
local labels = m.labels[Bag.pocketIdx]
|
||||
image(fem and labels.female or labels.male, 32, 80)
|
||||
if m.indicators then
|
||||
for p = 1, 5 do image(p == Bag.pocketIdx and m.indicators.selected or m.indicators.idle, 32 + p * 8, 72) end
|
||||
-- pokeruby/src/item_menu.c:779
|
||||
local selected, idle = m.indicators.selected, m.indicators.idle
|
||||
if fem then selected, idle = m.indicators.selectedFemale, m.indicators.idleFemale end
|
||||
for p = 1, 5 do image(p == Bag.pocketIdx and selected or idle, 32 + p * 8, 72) end
|
||||
end
|
||||
local rows = Bag.list()
|
||||
local scroll = Bag.scroll
|
||||
@@ -428,16 +437,26 @@ function M.draw(Bag)
|
||||
local hm = pocket == "TM_CASE" and Items.isHm(row.id)
|
||||
image(hm and m.hm or m.number, 112, y)
|
||||
local number = pocket == "TM_CASE" and Items.tmNumber(row.id) or Items.berryNumber(row.id)
|
||||
drawText(hm and tostring(number) or string.format("%02d", number or 0), 129, y, rowColors)
|
||||
local move = pocket == "TM_CASE" and Pokemon.moveFromTmItem(row.id)
|
||||
drawText(move and Pokemon.moveName(move) or row.name, 136, y, rowColors, hm and 96 or 78)
|
||||
if not hm then drawText("×" .. tostring(row.qty), 232 - Font.measure("×" .. tostring(row.qty), {font = "normal"}), y, rowColors) end
|
||||
local name = move and Pokemon.moveName(move) or row.name
|
||||
if hm then
|
||||
-- pokeruby/src/item_menu.c:1249
|
||||
drawText(tostring(number), 129, y, rowColors)
|
||||
drawText(name, 136, y, rowColors, 96)
|
||||
else
|
||||
-- pokeruby/src/item_menu.c:1091
|
||||
local multX = 112 + 120 - ((pocket == "TM_CASE" and 2 or 3) + 1) * 6
|
||||
drawText(string.format("%02d", number or 0), 120, y, rowColors)
|
||||
drawText(name, 136, y, rowColors, multX - 136)
|
||||
quantity(row.qty, multX, y, rowColors)
|
||||
end
|
||||
elseif pocket == "KEY_ITEMS" then
|
||||
drawText(row.name, 112, y, rowColors, 96)
|
||||
if Bag._session and Items.toNumericId(Bag._session.registeredItem) == Items.toNumericId(row.id) then image(m.select, 208, y) end
|
||||
else
|
||||
-- pokeruby/src/item_menu.c:1166
|
||||
drawText(row.name, 112, y, rowColors, 102)
|
||||
drawText("×" .. tostring(row.qty), 232 - Font.measure("×" .. tostring(row.qty), {font = "normal"}), y, rowColors)
|
||||
quantity(row.qty, 214, y, rowColors)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -377,6 +377,75 @@ function BattleState.trainerArt(data, classId)
|
||||
return path, (classDef and classDef.trueColor) and true or false
|
||||
end
|
||||
|
||||
local BATTLE_SFX = {
|
||||
"Sfx_Damage", "Sfx_SuperEffective", "Sfx_NotVeryEffective",
|
||||
"Sfx_ExpBar", "Sfx_HitEndOfExpBar", "Sfx_DexFanfare5079",
|
||||
"Sfx_Faint", "Sfx_Kinesis", "Sfx_CaughtMon", "Sfx_Run",
|
||||
"Sfx_SwitchPokemon", "Sfx_Potion",
|
||||
}
|
||||
|
||||
local function animSounds(anims, order, key, out, seen)
|
||||
if not key or seen[key] then return end
|
||||
seen[key] = true
|
||||
for _, row in ipairs(anims.scripts[key] or {}) do
|
||||
if row[1] == "sound" then
|
||||
local name = order[(row[3] or 0) + 1]
|
||||
if name then out[#out + 1] = name end
|
||||
else
|
||||
for i = 2, #row do
|
||||
local target = row[i]
|
||||
if type(target) == "string" and anims.scripts[target] then
|
||||
animSounds(anims, order, target, out, seen)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function moveIdOf(move)
|
||||
if type(move) == "table" then return move.id end
|
||||
return move
|
||||
end
|
||||
|
||||
function BattleState.battleSfxNames(data, anims, battle)
|
||||
local audio = data and data.audio
|
||||
local sfx = audio and audio.sfx
|
||||
if not sfx then return {} end
|
||||
local order = audio.sfxOrder or {}
|
||||
local names, listed = {}, {}
|
||||
local function add(name)
|
||||
if name and sfx[name] and not listed[name] then
|
||||
listed[name] = true
|
||||
names[#names + 1] = name
|
||||
end
|
||||
end
|
||||
local function addMoves(mon)
|
||||
if not (mon and anims and anims.scripts and anims.moves) then return end
|
||||
for _, move in ipairs(mon.moves or {}) do
|
||||
local out = {}
|
||||
animSounds(anims, order, anims.moves[moveIdOf(move)], out, {})
|
||||
for _, name in ipairs(out) do add(name) end
|
||||
end
|
||||
end
|
||||
battle = battle or {}
|
||||
addMoves(battle.enemy)
|
||||
addMoves(battle.player)
|
||||
for _, name in ipairs(BATTLE_SFX) do add(name) end
|
||||
if anims and anims.scripts and anims.ids then
|
||||
local ids = {}
|
||||
for id in pairs(anims.ids) do ids[#ids + 1] = id end
|
||||
table.sort(ids)
|
||||
for _, id in ipairs(ids) do
|
||||
local out = {}
|
||||
animSounds(anims, order, anims.ids[id], out, {})
|
||||
for _, name in ipairs(out) do add(name) end
|
||||
end
|
||||
end
|
||||
for _, mon in ipairs(battle.enemyParty or {}) do addMoves(mon) end
|
||||
for _, mon in ipairs(battle.party or {}) do addMoves(mon) end
|
||||
return names
|
||||
end
|
||||
|
||||
-- opts: battle (a Battle), onDone(outcome), save
|
||||
function BattleState.new(game, opts)
|
||||
opts = opts or {}
|
||||
@@ -629,6 +698,11 @@ function BattleState.new(game, opts)
|
||||
for _, mon in ipairs({ enemy or false, player or false }) do
|
||||
if mon and mon.species then pcall(Sound.prewarmCry, data, mon.species) end
|
||||
end
|
||||
local okNames, names = pcall(BattleState.battleSfxNames, data, self.anims,
|
||||
self.battle)
|
||||
if okNames then
|
||||
for _, name in ipairs(names) do pcall(Sound.prewarmSfx, data, name) end
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user