diff --git a/docs/new-features.md b/docs/new-features.md index 08162192..00b9fc77 100644 --- a/docs/new-features.md +++ b/docs/new-features.md @@ -26,6 +26,7 @@ Features intentionally added to the games themselves, beyond what the original c * **Bank-style Box migration** from Gen 1 and 2 to Gen 3: nature from EXP, three perfect IVs (five for Mew and Celebi), EVs reset, second ability when there is one * **EXP-to-nature helper** in Box migration, showing how much EXP each nature needs before a Gen 1 or 2 Pokémon moves to Gen 3 * **Box item stash**: store bag items in Box, send them to another game's bag, give or take held items and change a Gen 3 Pokémon's ball +* **Unown form picker** in the save editor on Gen 2 and Gen 3, keeping HP DV, nature and shininess where the cart allows ## Gen 2 Specifics diff --git a/docs/updater.md b/docs/updater.md index de5e3848..b045f272 100644 --- a/docs/updater.md +++ b/docs/updater.md @@ -179,8 +179,13 @@ bundled game, in that case. in-app. If neither transport exists, the worker reports `needs_full` and the launcher chip opens `Check.releaseUrl()`. Native package-only changes still need a full reinstall (`minShell` / `payloadHost` gate → - `needs_full`). Applying a downloaded payload on Android relaunches via - `love.system.restartApp`; iOS still uses in-process `quit("restart")`. + `needs_full`). Applying a downloaded payload on Android restarts + in-process with `quit("restart")` only when the running `love.run` sets + `POKEPORT_LOOP_RESTART`; an older APK shell's stepper would hand that + string to `os.exit`, so there `HostShell.restart` falls back to + `love.system.restartApp` (or a plain quit) and EXIT GAME rebuilds the + launcher in the same Lua state instead. iOS uses a bare `quit()`, which + its `love.cpp` turns into a restart. - **Android full updates are user-confirmed and certificate-bound.** The app uses a private `FileProvider` cache path plus `Intent.ACTION_INSTALL_PACKAGE`, checks Android 8+'s per-app diff --git a/main.lua b/main.lua index 360f3d06..5e220e44 100644 --- a/main.lua +++ b/main.lua @@ -525,10 +525,56 @@ local function makeLauncher(launcherOpts) }) end +local deferredLaunchRequest + +local function rebuildLauncherInProcess(opts) + if require("src.core.RequireGuard").repair() then + print("boot: restored love.filesystem searcher (see #2001)") + end + + local currentVersion = require("src.core.GameVersion").get() + SessionLifecycle.endGameSession(Game) + Game = nil + pcall(function() require("src.online.Trade").hostIsLive = nil end) + local syncEngine = package.loaded["src.sync.SyncEngine"] + if type(syncEngine) == "table" and type(syncEngine._shared) == "table" then + pcall(syncEngine._shared.protectPlaythrough, syncEngine._shared, nil, nil) + end + autopilot = nil + driverCo = nil + local SaveData = require("src.core.SaveData") + local cartId = SaveData.getCart() + SaveData.setCart(nil) + require("src.core.GameSpeed").setAllowed(nil) + + SessionLifecycle.endMountedSession(currentVersion) + SaveData.refreshSlotResolution(currentVersion) + if cartId then SaveData.refreshSlotResolution("cart_" .. cartId) end + + applySavedOrientation() + + local preload = require("src.mods.LauncherMods").translationStrings() + if preload then require("src.core.Strings").load({ strings = preload }) end + + if love.window and love.window.setTitle then + local Version = require("src.core.Version") + love.window.setTitle(Version.title("Gen 1 Recompilation Project")) + end + + Importer = makeLauncher({ initialTab = opts.tab, invite = opts.invite }) + if Importer.ignoreReturningPointer then + Importer:ignoreReturningPointer() + end + if type(opts.request) == "table" then deferredLaunchRequest = opts.request end +end + local function returnToLauncher(opts) if not Game or quitToLauncher then return end - quitToLauncher = true opts = opts or {} + if not require("src.core.HostShell").canRestart() then + return rebuildLauncherInProcess(opts) + end + quitToLauncher = true local handoff = { tab = opts.tab, invite = opts.invite, request = opts.request } local okEncode, body = pcall(require("src.core.SaveSerializer").encode, handoff) pcall(love.filesystem.write, RELAUNCH_MARKER, okEncode and body or "return {}\n") @@ -538,6 +584,7 @@ local function returnToLauncher(opts) autopilot = nil driverCo = nil endProcessOnce() + if opts.restarting then return end require("src.core.HostShell").restart() end @@ -673,8 +720,6 @@ local function autoUpdateMods(request, tab) end end -local deferredLaunchRequest - local function launcherBusy() return launcherSplash ~= nil or Importer ~= nil and (Importer._updateAll ~= nil or Importer._modInstall ~= nil or Importer._cartInstall ~= nil @@ -1617,8 +1662,13 @@ function love.quit() and (mobile or not launchedIntoGame) end) if wouldReturnToLauncher then - returnToLauncher() - return true + local HostShell = require("src.core.HostShell") + if HostShell.restarting and HostShell.canRestart() then + returnToLauncher({ restarting = true }) + else + returnToLauncher() + return true + end end endProcessOnce() end @@ -1658,6 +1708,7 @@ local function idlePresentationCap(idleFor) end function love.run() + _G.POKEPORT_LOOP_RESTART = true if love.load then love.load(love.arg.parseGameArguments(arg), arg) end -- don't let love.load's cost land in the first frame's dt diff --git a/src/box/Transaction.lua b/src/box/Transaction.lua index 6c1b2d34..0caac15d 100644 --- a/src/box/Transaction.lua +++ b/src/box/Transaction.lua @@ -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." diff --git a/src/core/ChipAudio.lua b/src/core/ChipAudio.lua index 1bfa58cb..d75edb1d 100644 --- a/src/core/ChipAudio.lua +++ b/src/core/ChipAudio.lua @@ -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: diff --git a/src/core/Game2.lua b/src/core/Game2.lua index 4cd831f1..10fd86ce 100644 --- a/src/core/Game2.lua +++ b/src/core/Game2.lua @@ -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 diff --git a/src/core/Game3.lua b/src/core/Game3.lua index 96abe3bb..a19cc260 100644 --- a/src/core/Game3.lua +++ b/src/core/Game3.lua @@ -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 diff --git a/src/core/HostShell.lua b/src/core/HostShell.lua index 3f1b47d7..f745f0e0 100644 --- a/src/core/HostShell.lua +++ b/src/core/HostShell.lua @@ -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 diff --git a/src/core/SaveData.lua b/src/core/SaveData.lua index acf63848..0798aedf 100644 --- a/src/core/SaveData.lua +++ b/src/core/SaveData.lua @@ -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 diff --git a/src/core/game3/battle/anim_port/rs_sound_tasks.lua b/src/core/game3/battle/anim_port/rs_sound_tasks.lua index 27f0f2a0..d7283228 100644 --- a/src/core/game3/battle/anim_port/rs_sound_tasks.lua +++ b/src/core/game3/battle/anim_port/rs_sound_tasks.lua @@ -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 diff --git a/src/core/game3/battle/ui.lua b/src/core/game3/battle/ui.lua index e4498081..84432611 100644 --- a/src/core/game3/battle/ui.lua +++ b/src/core/game3/battle/ui.lua @@ -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) diff --git a/src/core/game3/battle_transition.lua b/src/core/game3/battle_transition.lua index c909b4bb..d576732c 100644 --- a/src/core/game3/battle_transition.lua +++ b/src/core/game3/battle_transition.lua @@ -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 diff --git a/src/core/game3/display.lua b/src/core/game3/display.lua index dcd57252..956b26fe 100644 --- a/src/core/game3/display.lua +++ b/src/core/game3/display.lua @@ -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) diff --git a/src/core/game3/field.lua b/src/core/game3/field.lua index c87754f8..cc93c67d 100644 --- a/src/core/game3/field.lua +++ b/src/core/game3/field.lua @@ -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) diff --git a/src/core/game3/objects.lua b/src/core/game3/objects.lua index f55fd879..cc2efa57 100644 --- a/src/core/game3/objects.lua +++ b/src/core/game3/objects.lua @@ -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 diff --git a/src/core/game3/profiles/emerald/field.lua b/src/core/game3/profiles/emerald/field.lua index fb472714..5e384a81 100644 --- a/src/core/game3/profiles/emerald/field.lua +++ b/src/core/game3/profiles/emerald/field.lua @@ -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 diff --git a/src/core/game3/profiles/rs/field.lua b/src/core/game3/profiles/rs/field.lua index 1eb44320..82e83681 100644 --- a/src/core/game3/profiles/rs/field.lua +++ b/src/core/game3/profiles/rs/field.lua @@ -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, diff --git a/src/core/game3/step_events.lua b/src/core/game3/step_events.lua index 52349291..d342a747 100644 --- a/src/core/game3/step_events.lua +++ b/src/core/game3/step_events.lua @@ -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 diff --git a/src/import/LauncherSettings.lua b/src/import/LauncherSettings.lua index d1898a25..d1fb670b 100644 --- a/src/import/LauncherSettings.lua +++ b/src/import/LauncherSettings.lua @@ -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 diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index 064a5c23..bc31d724 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -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, { diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 5739830d..dff478d5 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -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?"), diff --git a/src/import/RomSources.lua b/src/import/RomSources.lua index 67c5dee2..e090b4f0 100644 --- a/src/import/RomSources.lua +++ b/src/import/RomSources.lua @@ -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 diff --git a/src/import/gba/games/rs.lua b/src/import/gba/games/rs.lua index 520fa1ff..f694d8bb 100644 --- a/src/import/gba/games/rs.lua +++ b/src/import/gba/games/rs.lua @@ -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 diff --git a/src/import/gba/games/rs/anims.lua b/src/import/gba/games/rs/anims.lua index 3b8daee3..7b178583 100644 --- a/src/import/gba/games/rs/anims.lua +++ b/src/import/gba/games/rs/anims.lua @@ -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 diff --git a/src/import/gba/rs/anim_names.lua b/src/import/gba/rs/anim_names.lua index 5f13a534..c5b9faf2 100644 --- a/src/import/gba/rs/anim_names.lua +++ b/src/import/gba/rs/anim_names.lua @@ -1,817 +1,825 @@ +-- tools/gen_anim_names.py --game ruby from pokeruby.elf, pokeruby/data/battle_anim_scripts.s aligned with pokeemerald/data/battle_anim_scripts.s return { game = "rs", templates = { - ["IceBeamCenterParticleSpriteTemplate"] = "IceBeamCenterParticleSpriteTemplate", - ["RainbowRingSpriteTemplate"] = "RainbowRingSpriteTemplate", - ["gAncientPowerRockSpriteTemplate"] = "gAncientPowerRockSpriteTemplate", - ["gBasicHitSplatSpriteTemplate"] = "gBasicHitSplatSpriteTemplate", - ["gBattleAnimSpriteTemplate_83930A8"] = "gBattleAnimSpriteTemplate_83930A8", - ["gBattleAnimSpriteTemplate_83930C0"] = "gBattleAnimSpriteTemplate_83930C0", - ["gBattleAnimSpriteTemplate_83930F4"] = "gBattleAnimSpriteTemplate_83930F4", - ["gBattleAnimSpriteTemplate_83C2010"] = "gBattleAnimSpriteTemplate_83C2010", - ["gBattleAnimSpriteTemplate_83D62EC"] = "gBattleAnimSpriteTemplate_83D62EC", - ["gBattleAnimSpriteTemplate_83D6304"] = "gBattleAnimSpriteTemplate_83D6304", - ["gBattleAnimSpriteTemplate_83D6350"] = "gBattleAnimSpriteTemplate_83D6350", - ["gBattleAnimSpriteTemplate_83D637C"] = "gBattleAnimSpriteTemplate_83D637C", - ["gBattleAnimSpriteTemplate_83D6394"] = "gBattleAnimSpriteTemplate_83D6394", - ["gBattleAnimSpriteTemplate_83D63F8"] = "gBattleAnimSpriteTemplate_83D63F8", - ["gBattleAnimSpriteTemplate_83D6428"] = "gBattleAnimSpriteTemplate_83D6428", - ["gBattleAnimSpriteTemplate_83D6440"] = "gBattleAnimSpriteTemplate_83D6440", - ["gBattleAnimSpriteTemplate_83D649C"] = "gBattleAnimSpriteTemplate_83D649C", - ["gBattleAnimSpriteTemplate_83D64B4"] = "gBattleAnimSpriteTemplate_83D64B4", - ["gBattleAnimSpriteTemplate_83D64E4"] = "gBattleAnimSpriteTemplate_83D64E4", - ["gBattleAnimSpriteTemplate_83D65A0"] = "gBattleAnimSpriteTemplate_83D65A0", - ["gBattleAnimSpriteTemplate_83D65E8"] = "gBattleAnimSpriteTemplate_83D65E8", - ["gBattleAnimSpriteTemplate_83D6658"] = "gBattleAnimSpriteTemplate_83D6658", - ["gBattleAnimSpriteTemplate_83D6670"] = "gBattleAnimSpriteTemplate_83D6670", - ["gBattleAnimSpriteTemplate_83D6698"] = "gBattleAnimSpriteTemplate_83D6698", - ["gBattleAnimSpriteTemplate_83D671C"] = "gBattleAnimSpriteTemplate_83D671C", - ["gBattleAnimSpriteTemplate_83D6764"] = "gBattleAnimSpriteTemplate_83D6764", - ["gBattleAnimSpriteTemplate_83D677C"] = "gBattleAnimSpriteTemplate_83D677C", - ["gBattleAnimSpriteTemplate_83D67F4"] = "gBattleAnimSpriteTemplate_83D67F4", - ["gBattleAnimSpriteTemplate_83D68B8"] = "gBattleAnimSpriteTemplate_83D68B8", - ["gBattleAnimSpriteTemplate_83D68D0"] = "gBattleAnimSpriteTemplate_83D68D0", - ["gBattleAnimSpriteTemplate_83D693C"] = "gBattleAnimSpriteTemplate_83D693C", - ["gBattleAnimSpriteTemplate_83D6954"] = "gBattleAnimSpriteTemplate_83D6954", - ["gBattleAnimSpriteTemplate_83D696C"] = "gBattleAnimSpriteTemplate_83D696C", - ["gBattleAnimSpriteTemplate_83D6994"] = "gBattleAnimSpriteTemplate_83D6994", - ["gBattleAnimSpriteTemplate_83D69DC"] = "gBattleAnimSpriteTemplate_83D69DC", - ["gBattleAnimSpriteTemplate_83D69F4"] = "gBattleAnimSpriteTemplate_83D69F4", - ["gBattleAnimSpriteTemplate_83D6B40"] = "gBattleAnimSpriteTemplate_83D6B40", - ["gBattleAnimSpriteTemplate_83D6BE8"] = "gBattleAnimSpriteTemplate_83D6BE8", - ["gBattleAnimSpriteTemplate_83D6C48"] = "gBattleAnimSpriteTemplate_83D6C48", - ["gBattleAnimSpriteTemplate_83D6C88"] = "gBattleAnimSpriteTemplate_83D6C88", - ["gBattleAnimSpriteTemplate_83D6CA0"] = "gBattleAnimSpriteTemplate_83D6CA0", - ["gBattleAnimSpriteTemplate_83D6D94"] = "gBattleAnimSpriteTemplate_83D6D94", - ["gBattleAnimSpriteTemplate_83D6DAC"] = "gBattleAnimSpriteTemplate_83D6DAC", - ["gBattleAnimSpriteTemplate_83D6DC4"] = "gBattleAnimSpriteTemplate_83D6DC4", - ["gBattleAnimSpriteTemplate_83D6DE4"] = "gBattleAnimSpriteTemplate_83D6DE4", - ["gBattleAnimSpriteTemplate_83D6E38"] = "gBattleAnimSpriteTemplate_83D6E38", - ["gBattleAnimSpriteTemplate_83D6E50"] = "gBattleAnimSpriteTemplate_83D6E50", - ["gBattleAnimSpriteTemplate_83D6E68"] = "gBattleAnimSpriteTemplate_83D6E68", - ["gBattleAnimSpriteTemplate_83D6EF0"] = "gBattleAnimSpriteTemplate_83D6EF0", - ["gBattleAnimSpriteTemplate_83D6F08"] = "gBattleAnimSpriteTemplate_83D6F08", - ["gBattleAnimSpriteTemplate_83D6F3C"] = "gBattleAnimSpriteTemplate_83D6F3C", - ["gBattleAnimSpriteTemplate_83D6F80"] = "gBattleAnimSpriteTemplate_83D6F80", - ["gBattleAnimSpriteTemplate_83D6FB0"] = "gBattleAnimSpriteTemplate_83D6FB0", - ["gBattleAnimSpriteTemplate_83D6FC8"] = "gBattleAnimSpriteTemplate_83D6FC8", - ["gBattleAnimSpriteTemplate_83D6FF8"] = "gBattleAnimSpriteTemplate_83D6FF8", - ["gBattleAnimSpriteTemplate_83D7038"] = "gBattleAnimSpriteTemplate_83D7038", - ["gBattleAnimSpriteTemplate_83D7050"] = "gBattleAnimSpriteTemplate_83D7050", - ["gBattleAnimSpriteTemplate_83D7080"] = "gBattleAnimSpriteTemplate_83D7080", - ["gBattleAnimSpriteTemplate_83D7114"] = "gBattleAnimSpriteTemplate_83D7114", - ["gBattleAnimSpriteTemplate_83D715C"] = "gBattleAnimSpriteTemplate_83D715C", - ["gBattleAnimSpriteTemplate_83D7174"] = "gBattleAnimSpriteTemplate_83D7174", - ["gBattleAnimSpriteTemplate_83D71A8"] = "gBattleAnimSpriteTemplate_83D71A8", - ["gBattleAnimSpriteTemplate_83D7220"] = "gBattleAnimSpriteTemplate_83D7220", - ["gBattleAnimSpriteTemplate_83D72C8"] = "gBattleAnimSpriteTemplate_83D72C8", - ["gBattleAnimSpriteTemplate_83D72E0"] = "gBattleAnimSpriteTemplate_83D72E0", - ["gBattleAnimSpriteTemplate_83D7358"] = "gBattleAnimSpriteTemplate_83D7358", - ["gBattleAnimSpriteTemplate_83D7450"] = "gBattleAnimSpriteTemplate_83D7450", - ["gBattleAnimSpriteTemplate_83D748C"] = "gBattleAnimSpriteTemplate_83D748C", - ["gBattleAnimSpriteTemplate_83D7564"] = "gBattleAnimSpriteTemplate_83D7564", - ["gBattleAnimSpriteTemplate_83D757C"] = "gBattleAnimSpriteTemplate_83D757C", - ["gBattleAnimSpriteTemplate_83D7594"] = "gBattleAnimSpriteTemplate_83D7594", - ["gBattleAnimSpriteTemplate_83D75E4"] = "gBattleAnimSpriteTemplate_83D75E4", - ["gBattleAnimSpriteTemplate_83D75FC"] = "gBattleAnimSpriteTemplate_83D75FC", - ["gBattleAnimSpriteTemplate_83D7628"] = "gBattleAnimSpriteTemplate_83D7628", - ["gBattleAnimSpriteTemplate_83D765C"] = "gBattleAnimSpriteTemplate_83D765C", - ["gBattleAnimSpriteTemplate_83D769C"] = "gBattleAnimSpriteTemplate_83D769C", - ["gBattleAnimSpriteTemplate_83D76DC"] = "gBattleAnimSpriteTemplate_83D76DC", - ["gBattleAnimSpriteTemplate_83D7764"] = "gBattleAnimSpriteTemplate_83D7764", - ["gBattleAnimSpriteTemplate_83D7798"] = "gBattleAnimSpriteTemplate_83D7798", - ["gBattleAnimSpriteTemplate_83D77E0"] = "gBattleAnimSpriteTemplate_83D77E0", - ["gBattleAnimSpriteTemplate_83D77F8"] = "gBattleAnimSpriteTemplate_83D77F8", - ["gBattleAnimSpriteTemplate_83D7828"] = "gBattleAnimSpriteTemplate_83D7828", - ["gBattleAnimSpriteTemplate_83D78BC"] = "gBattleAnimSpriteTemplate_83D78BC", - ["gBattleAnimSpriteTemplate_83D7928"] = "gBattleAnimSpriteTemplate_83D7928", - ["gBattleAnimSpriteTemplate_83D795C"] = "gBattleAnimSpriteTemplate_83D795C", - ["gBattleAnimSpriteTemplate_83D7974"] = "gBattleAnimSpriteTemplate_83D7974", - ["gBattleAnimSpriteTemplate_83D798C"] = "gBattleAnimSpriteTemplate_83D798C", - ["gBattleAnimSpriteTemplate_83D79A4"] = "gBattleAnimSpriteTemplate_83D79A4", - ["gBattleAnimSpriteTemplate_83D7A44"] = "gBattleAnimSpriteTemplate_83D7A44", - ["gBattleAnimSpriteTemplate_83D7A68"] = "gBattleAnimSpriteTemplate_83D7A68", - ["gBattleAnimSpriteTemplate_83D7A80"] = "gBattleAnimSpriteTemplate_83D7A80", - ["gBattleAnimSpriteTemplate_83D7AB0"] = "gBattleAnimSpriteTemplate_83D7AB0", - ["gBattleAnimSpriteTemplate_83D7AC8"] = "gBattleAnimSpriteTemplate_83D7AC8", - ["gBattleAnimSpriteTemplate_83D7AE0"] = "gBattleAnimSpriteTemplate_83D7AE0", - ["gBattleAnimSpriteTemplate_83D7B60"] = "gBattleAnimSpriteTemplate_83D7B60", - ["gBattleAnimSpriteTemplate_83D7B94"] = "gBattleAnimSpriteTemplate_83D7B94", - ["gBattleAnimSpriteTemplate_83D7BB8"] = "gBattleAnimSpriteTemplate_83D7BB8", - ["gBattleAnimSpriteTemplate_83D7BD0"] = "gBattleAnimSpriteTemplate_83D7BD0", - ["gBattleAnimSpriteTemplate_83D7C00"] = "gBattleAnimSpriteTemplate_83D7C00", - ["gBattleAnimSpriteTemplate_83D7C48"] = "gBattleAnimSpriteTemplate_83D7C48", - ["gBattleAnimSpriteTemplate_83D7C90"] = "gBattleAnimSpriteTemplate_83D7C90", - ["gBattleAnimSpriteTemplate_83D7CC8"] = "gBattleAnimSpriteTemplate_83D7CC8", - ["gBattleAnimSpriteTemplate_83D7D1C"] = "gBattleAnimSpriteTemplate_83D7D1C", - ["gBattleAnimSpriteTemplate_83D7D34"] = "gBattleAnimSpriteTemplate_83D7D34", - ["gBattleAnimSpriteTemplate_83D7D74"] = "gBattleAnimSpriteTemplate_83D7D74", - ["gBattleAnimSpriteTemplate_83D9178"] = "gBattleAnimSpriteTemplate_83D9178", - ["gBattleAnimSpriteTemplate_83D91F4"] = "gBattleAnimSpriteTemplate_83D91F4", - ["gBattleAnimSpriteTemplate_83D920C"] = "gBattleAnimSpriteTemplate_83D920C", - ["gBattleAnimSpriteTemplate_83D9224"] = "gBattleAnimSpriteTemplate_83D9224", - ["gBattleAnimSpriteTemplate_83D923C"] = "gBattleAnimSpriteTemplate_83D923C", - ["gBattleAnimSpriteTemplate_83D9268"] = "gBattleAnimSpriteTemplate_83D9268", - ["gBattleAnimSpriteTemplate_83D9280"] = "gBattleAnimSpriteTemplate_83D9280", - ["gBattleAnimSpriteTemplate_83D92D0"] = "gBattleAnimSpriteTemplate_83D92D0", - ["gBattleAnimSpriteTemplate_83D92E8"] = "gBattleAnimSpriteTemplate_83D92E8", - ["gBattleAnimSpriteTemplate_83D9318"] = "gBattleAnimSpriteTemplate_83D9318", - ["gBattleAnimSpriteTemplate_83D9330"] = "gBattleAnimSpriteTemplate_83D9330", - ["gBattleAnimSpriteTemplate_83D9348"] = "gBattleAnimSpriteTemplate_83D9348", - ["gBattleAnimSpriteTemplate_83D9360"] = "gBattleAnimSpriteTemplate_83D9360", - ["gBattleAnimSpriteTemplate_83D9408"] = "gBattleAnimSpriteTemplate_83D9408", - ["gBattleAnimSpriteTemplate_83D9438"] = "gBattleAnimSpriteTemplate_83D9438", - ["gBattleAnimSpriteTemplate_83D9478"] = "gBattleAnimSpriteTemplate_83D9478", - ["gBattleAnimSpriteTemplate_83D9490"] = "gBattleAnimSpriteTemplate_83D9490", - ["gBattleAnimSpriteTemplate_83D9508"] = "gBattleAnimSpriteTemplate_83D9508", - ["gBattleAnimSpriteTemplate_83D9520"] = "gBattleAnimSpriteTemplate_83D9520", - ["gBattleAnimSpriteTemplate_83D9538"] = "gBattleAnimSpriteTemplate_83D9538", - ["gBattleAnimSpriteTemplate_83D95B0"] = "gBattleAnimSpriteTemplate_83D95B0", - ["gBattleAnimSpriteTemplate_83D9614"] = "gBattleAnimSpriteTemplate_83D9614", - ["gBattleAnimSpriteTemplate_83D9694"] = "gBattleAnimSpriteTemplate_83D9694", - ["gBattleAnimSpriteTemplate_83D96AC"] = "gBattleAnimSpriteTemplate_83D96AC", - ["gBattleAnimSpriteTemplate_83D96F8"] = "gBattleAnimSpriteTemplate_83D96F8", - ["gBattleAnimSpriteTemplate_83D974C"] = "gBattleAnimSpriteTemplate_83D974C", - ["gBattleAnimSpriteTemplate_83D977C"] = "gBattleAnimSpriteTemplate_83D977C", - ["gBattleAnimSpriteTemplate_83D97D0"] = "gBattleAnimSpriteTemplate_83D97D0", - ["gBattleAnimSpriteTemplate_83D985C"] = "gBattleAnimSpriteTemplate_83D985C", - ["gBattleAnimSpriteTemplate_83D9874"] = "gBattleAnimSpriteTemplate_83D9874", - ["gBattleAnimSpriteTemplate_83D98A0"] = "gBattleAnimSpriteTemplate_83D98A0", - ["gBattleAnimSpriteTemplate_83D98F0"] = "gBattleAnimSpriteTemplate_83D98F0", - ["gBattleAnimSpriteTemplate_83D9908"] = "gBattleAnimSpriteTemplate_83D9908", - ["gBattleAnimSpriteTemplate_83D9950"] = "gBattleAnimSpriteTemplate_83D9950", - ["gBattleAnimSpriteTemplate_83D9A6C"] = "gBattleAnimSpriteTemplate_83D9A6C", - ["gBattleAnimSpriteTemplate_83D9A9C"] = "gBattleAnimSpriteTemplate_83D9A9C", - ["gBattleAnimSpriteTemplate_83D9AB4"] = "gBattleAnimSpriteTemplate_83D9AB4", - ["gBattleAnimSpriteTemplate_83D9B28"] = "gBattleAnimSpriteTemplate_83D9B28", - ["gBattleAnimSpriteTemplate_83D9F78"] = "gBattleAnimSpriteTemplate_83D9F78", - ["gBattleAnimSpriteTemplate_83D9F90"] = "gBattleAnimSpriteTemplate_83D9F90", - ["gBattleAnimSpriteTemplate_83D9FC0"] = "gBattleAnimSpriteTemplate_83D9FC0", - ["gBattleAnimSpriteTemplate_83D9FD8"] = "gBattleAnimSpriteTemplate_83D9FD8", - ["gBattleAnimSpriteTemplate_83D9FF0"] = "gBattleAnimSpriteTemplate_83D9FF0", - ["gBattleAnimSpriteTemplate_83DA024"] = "gBattleAnimSpriteTemplate_83DA024", - ["gBattleAnimSpriteTemplate_83DA088"] = "gBattleAnimSpriteTemplate_83DA088", - ["gBattleAnimSpriteTemplate_83DA0A0"] = "gBattleAnimSpriteTemplate_83DA0A0", - ["gBattleAnimSpriteTemplate_83DA0B8"] = "gBattleAnimSpriteTemplate_83DA0B8", - ["gBattleAnimSpriteTemplate_83DA0FC"] = "gBattleAnimSpriteTemplate_83DA0FC", - ["gBattleAnimSpriteTemplate_83DA114"] = "gBattleAnimSpriteTemplate_83DA114", - ["gBattleAnimSpriteTemplate_83DA12C"] = "gBattleAnimSpriteTemplate_83DA12C", - ["gBattleAnimSpriteTemplate_83DA144"] = "gBattleAnimSpriteTemplate_83DA144", - ["gBattleAnimSpriteTemplate_83DA198"] = "gBattleAnimSpriteTemplate_83DA198", - ["gBattleAnimSpriteTemplate_83DA1E0"] = "gBattleAnimSpriteTemplate_83DA1E0", - ["gBattleAnimSpriteTemplate_83DA214"] = "gBattleAnimSpriteTemplate_83DA214", - ["gBattleAnimSpriteTemplate_83DA244"] = "gBattleAnimSpriteTemplate_83DA244", - ["gBattleAnimSpriteTemplate_83DA2B8"] = "gBattleAnimSpriteTemplate_83DA2B8", - ["gBattleAnimSpriteTemplate_83DA2D0"] = "gBattleAnimSpriteTemplate_83DA2D0", - ["gBattleAnimSpriteTemplate_83DA2E8"] = "gBattleAnimSpriteTemplate_83DA2E8", - ["gBattleAnimSpriteTemplate_83DA31C"] = "gBattleAnimSpriteTemplate_83DA31C", - ["gBattleAnimSpriteTemplate_83DA380"] = "gBattleAnimSpriteTemplate_83DA380", - ["gBattleAnimSpriteTemplate_83DA3B4"] = "gBattleAnimSpriteTemplate_83DA3B4", - ["gBattleAnimSpriteTemplate_83DA3E4"] = "gBattleAnimSpriteTemplate_83DA3E4", - ["gBattleAnimSpriteTemplate_83DA450"] = "gBattleAnimSpriteTemplate_83DA450", - ["gBattleAnimSpriteTemplate_83DA468"] = "gBattleAnimSpriteTemplate_83DA468", - ["gBattleAnimSpriteTemplate_83DA498"] = "gBattleAnimSpriteTemplate_83DA498", - ["gBattleAnimSpriteTemplate_83DA51C"] = "gBattleAnimSpriteTemplate_83DA51C", - ["gBattleAnimSpriteTemplate_83DA568"] = "gBattleAnimSpriteTemplate_83DA568", - ["gBattleAnimSpriteTemplate_83DA594"] = "gBattleAnimSpriteTemplate_83DA594", - ["gBattleAnimSpriteTemplate_83DA5D8"] = "gBattleAnimSpriteTemplate_83DA5D8", - ["gBattleAnimSpriteTemplate_83DA614"] = "gBattleAnimSpriteTemplate_83DA614", - ["gBattleAnimSpriteTemplate_83DA62C"] = "gBattleAnimSpriteTemplate_83DA62C", - ["gBattleAnimSpriteTemplate_83DA65C"] = "gBattleAnimSpriteTemplate_83DA65C", - ["gBattleAnimSpriteTemplate_83DA690"] = "gBattleAnimSpriteTemplate_83DA690", - ["gBattleAnimSpriteTemplate_83DA6A8"] = "gBattleAnimSpriteTemplate_83DA6A8", - ["gBattleAnimSpriteTemplate_83DA6C0"] = "gBattleAnimSpriteTemplate_83DA6C0", - ["gBattleAnimSpriteTemplate_83DA6D8"] = "gBattleAnimSpriteTemplate_83DA6D8", - ["gBattleAnimSpriteTemplate_83DA6F0"] = "gBattleAnimSpriteTemplate_83DA6F0", - ["gBattleAnimSpriteTemplate_83DA708"] = "gBattleAnimSpriteTemplate_83DA708", - ["gBattleAnimSpriteTemplate_83DA73C"] = "gBattleAnimSpriteTemplate_83DA73C", - ["gBattleAnimSpriteTemplate_83DA76C"] = "gBattleAnimSpriteTemplate_83DA76C", - ["gBattleAnimSpriteTemplate_83DA784"] = "gBattleAnimSpriteTemplate_83DA784", - ["gBattleAnimSpriteTemplate_83DA824"] = "gBattleAnimSpriteTemplate_83DA824", - ["gBattleAnimSpriteTemplate_83DA88C"] = "gBattleAnimSpriteTemplate_83DA88C", - ["gBattleAnimSpriteTemplate_83DA8F4"] = "gBattleAnimSpriteTemplate_83DA8F4", - ["gBattleAnimSpriteTemplate_83DA9E0"] = "gBattleAnimSpriteTemplate_83DA9E0", - ["gBattleAnimSpriteTemplate_83DAA68"] = "gBattleAnimSpriteTemplate_83DAA68", - ["gBattleAnimSpriteTemplate_83DAABC"] = "gBattleAnimSpriteTemplate_83DAABC", - ["gBattleAnimSpriteTemplate_83DAB10"] = "gBattleAnimSpriteTemplate_83DAB10", - ["gBattleAnimSpriteTemplate_83DAB28"] = "gBattleAnimSpriteTemplate_83DAB28", - ["gBattleAnimSpriteTemplate_83DAB40"] = "gBattleAnimSpriteTemplate_83DAB40", - ["gBattleAnimSpriteTemplate_83DAB74"] = "gBattleAnimSpriteTemplate_83DAB74", - ["gBattleAnimSpriteTemplate_83DAC10"] = "gBattleAnimSpriteTemplate_83DAC10", - ["gBattleAnimSpriteTemplate_83DAC4C"] = "gBattleAnimSpriteTemplate_83DAC4C", - ["gBattleAnimSpriteTemplate_83DAC64"] = "gBattleAnimSpriteTemplate_83DAC64", - ["gBattleAnimSpriteTemplate_83DAC7C"] = "gBattleAnimSpriteTemplate_83DAC7C", - ["gBattleAnimSpriteTemplate_83DACB8"] = "gBattleAnimSpriteTemplate_83DACB8", - ["gBattleAnimSpriteTemplate_83DACD0"] = "gBattleAnimSpriteTemplate_83DACD0", - ["gBattleAnimSpriteTemplate_83DADA8"] = "gBattleAnimSpriteTemplate_83DADA8", - ["gBattleAnimSpriteTemplate_83DADE8"] = "gBattleAnimSpriteTemplate_83DADE8", - ["gBattleAnimSpriteTemplate_83DAE00"] = "gBattleAnimSpriteTemplate_83DAE00", - ["gBattleAnimSpriteTemplate_83DAE18"] = "gBattleAnimSpriteTemplate_83DAE18", - ["gBattleAnimSpriteTemplate_83DAE30"] = "gBattleAnimSpriteTemplate_83DAE30", - ["gBattleAnimSpriteTemplate_83DAE64"] = "gBattleAnimSpriteTemplate_83DAE64", - ["gBattleAnimSpriteTemplate_83DAE7C"] = "gBattleAnimSpriteTemplate_83DAE7C", - ["gBattleAnimSpriteTemplate_83DAEDC"] = "gBattleAnimSpriteTemplate_83DAEDC", - ["gBattleAnimSpriteTemplate_83DAF20"] = "gBattleAnimSpriteTemplate_83DAF20", - ["gBattleAnimSpriteTemplate_83DAF38"] = "gBattleAnimSpriteTemplate_83DAF38", - ["gBattleAnimSpriteTemplate_83DAF50"] = "gBattleAnimSpriteTemplate_83DAF50", - ["gBattleAnimSpriteTemplate_83DAFCC"] = "gBattleAnimSpriteTemplate_83DAFCC", - ["gBattleAnimSpriteTemplate_83DB044"] = "gBattleAnimSpriteTemplate_83DB044", - ["gBattleAnimSpriteTemplate_83DB078"] = "gBattleAnimSpriteTemplate_83DB078", - ["gBattleAnimSpriteTemplate_83DB0D0"] = "gBattleAnimSpriteTemplate_83DB0D0", - ["gBattleAnimSpriteTemplate_83DB0E8"] = "gBattleAnimSpriteTemplate_83DB0E8", - ["gBattleAnimSpriteTemplate_83DB100"] = "gBattleAnimSpriteTemplate_83DB100", - ["gBattleAnimSpriteTemplate_83DB1D0"] = "gBattleAnimSpriteTemplate_83DB1D0", - ["gBattleAnimSpriteTemplate_83DB1E8"] = "gBattleAnimSpriteTemplate_83DB1E8", - ["gBattleAnimSpriteTemplate_83DB238"] = "gBattleAnimSpriteTemplate_83DB238", - ["gBattleAnimSpriteTemplate_83DB288"] = "gBattleAnimSpriteTemplate_83DB288", - ["gBattleAnimSpriteTemplate_83DB3DC"] = "gBattleAnimSpriteTemplate_83DB3DC", - ["gBattleAnimSpriteTemplate_83DB428"] = "gBattleAnimSpriteTemplate_83DB428", - ["gBattleAnimSpriteTemplate_83DB4C0"] = "gBattleAnimSpriteTemplate_83DB4C0", - ["gBattleAnimSpriteTemplate_83DB4D8"] = "gBattleAnimSpriteTemplate_83DB4D8", - ["gBattleAnimSpriteTemplate_83DB4F0"] = "gBattleAnimSpriteTemplate_83DB4F0", - ["gBattleAnimSpriteTemplate_83DB508"] = "gBattleAnimSpriteTemplate_83DB508", - ["gBattleAnimSpriteTemplate_83DB520"] = "gBattleAnimSpriteTemplate_83DB520", - ["gBattleAnimSpriteTemplate_83DB538"] = "gBattleAnimSpriteTemplate_83DB538", - ["gBattleAnimSpriteTemplate_83DB550"] = "gBattleAnimSpriteTemplate_83DB550", - ["gBattleAnimSpriteTemplate_8402180"] = "gBattleAnimSpriteTemplate_8402180", - ["gBattleAnimSpriteTemplate_8402198"] = "gBattleAnimSpriteTemplate_8402198", - ["gBattleAnimSpriteTemplate_84021B0"] = "gBattleAnimSpriteTemplate_84021B0", - ["gBattleAnimSpriteTemplate_84021DC"] = "gBattleAnimSpriteTemplate_84021DC", - ["gBattleAnimSpriteTemplate_84021F4"] = "gBattleAnimSpriteTemplate_84021F4", - ["gBattleAnimSpriteTemplate_840220C"] = "gBattleAnimSpriteTemplate_840220C", - ["gBattleAnimSpriteTemplate_8402264"] = "gBattleAnimSpriteTemplate_8402264", - ["gBattleAnimSpriteTemplate_840227C"] = "gBattleAnimSpriteTemplate_840227C", - ["gBattleAnimSpriteTemplate_84022B0"] = "gBattleAnimSpriteTemplate_84022B0", - ["gBattleAnimSpriteTemplate_84022F0"] = "gBattleAnimSpriteTemplate_84022F0", - ["gBattleAnimSpriteTemplate_840233C"] = "gBattleAnimSpriteTemplate_840233C", - ["gBattleAnimSpriteTemplate_840238C"] = "gBattleAnimSpriteTemplate_840238C", - ["gBattleAnimSpriteTemplate_84023A4"] = "gBattleAnimSpriteTemplate_84023A4", - ["gBattleAnimSpriteTemplate_84023BC"] = "gBattleAnimSpriteTemplate_84023BC", - ["gBattleAnimSpriteTemplate_84023E8"] = "gBattleAnimSpriteTemplate_84023E8", - ["gBattleAnimSpriteTemplate_8402458"] = "gBattleAnimSpriteTemplate_8402458", - ["gBattleAnimSpriteTemplate_8402498"] = "gBattleAnimSpriteTemplate_8402498", - ["gBattleAnimSpriteTemplate_84024D0"] = "gBattleAnimSpriteTemplate_84024D0", - ["gBattleAnimSpriteTemplate_84024E8"] = "gBattleAnimSpriteTemplate_84024E8", - ["gBattleAnimSpriteTemplate_8402578"] = "gBattleAnimSpriteTemplate_8402578", - ["gBattleAnimSpriteTemplate_84025EC"] = "gBattleAnimSpriteTemplate_84025EC", - ["gBattleAnimSpriteTemplate_8402630"] = "gBattleAnimSpriteTemplate_8402630", - ["gBattleAnimSpriteTemplate_84026A4"] = "gBattleAnimSpriteTemplate_84026A4", - ["gBattleAnimSpriteTemplate_84026F0"] = "gBattleAnimSpriteTemplate_84026F0", - ["gBattleAnimSpriteTemplate_8402708"] = "gBattleAnimSpriteTemplate_8402708", - ["gBattleAnimSpriteTemplate_8402720"] = "gBattleAnimSpriteTemplate_8402720", - ["gBattleAnimSpriteTemplate_8402738"] = "gBattleAnimSpriteTemplate_8402738", - ["gBattleAnimSpriteTemplate_84027EC"] = "gBattleAnimSpriteTemplate_84027EC", - ["gBattleAnimSpriteTemplate_8402934"] = "gBattleAnimSpriteTemplate_8402934", - ["gBattleAnimSpriteTemplate_8402964"] = "gBattleAnimSpriteTemplate_8402964", - ["gBattleAnimSpriteTemplate_84029C4"] = "gBattleAnimSpriteTemplate_84029C4", - ["gBattleAnimSpriteTemplate_84029F4"] = "gBattleAnimSpriteTemplate_84029F4", - ["gBattleAnimSpriteTemplate_8402A0C"] = "gBattleAnimSpriteTemplate_8402A0C", - ["gBattleAnimSpriteTemplate_8402A24"] = "gBattleAnimSpriteTemplate_8402A24", - ["gBattleAnimSpriteTemplate_8402A3C"] = "gBattleAnimSpriteTemplate_8402A3C", - ["gBattleAnimSpriteTemplate_8402A6C"] = "gBattleAnimSpriteTemplate_8402A6C", - ["gBattleAnimSpriteTemplate_8402AE4"] = "gBattleAnimSpriteTemplate_8402AE4", - ["gBattleAnimSpriteTemplate_8402B10"] = "gBattleAnimSpriteTemplate_8402B10", - ["gBattleAnimSpriteTemplate_840B4FC"] = "gBattleAnimSpriteTemplate_840B4FC", - ["gBattleAnimSpriteTemplate_EndureFlame"] = "gBattleAnimSpriteTemplate_EndureFlame", - ["gBoneHitSpriteTemplate"] = "gBoneHitSpriteTemplate", - ["gBonemerangSpriteTemplate"] = "gBonemerangSpriteTemplate", - ["gConfusionDuckSpriteTemplate"] = "gConfusionDuckSpriteTemplate", - ["gCuttingSliceSpriteTemplate"] = "gCuttingSliceSpriteTemplate", - ["gDigDirtMoundSpriteTemplate"] = "gDigDirtMoundSpriteTemplate", - ["gDirtPlumeSpriteTemplate"] = "gDirtPlumeSpriteTemplate", - ["gElectricitySpriteTemplate"] = "gElectricitySpriteTemplate", - ["gEmberFlareSpriteTemplate"] = "gEmberFlareSpriteTemplate", - ["gEmberSpriteTemplate"] = "gEmberSpriteTemplate", - ["gFallingItemBagTemplate"] = "gFallingItemBagTemplate", - ["gFireCrossSpriteTemplate"] = "gFireCrossSpriteTemplate", - ["gFireRingSpriteTemplate"] = "gFireRingSpriteTemplate", - ["gFistFootSpriteTemplate"] = "gFistFootSpriteTemplate", - ["gFlyingDirtSpriteTemplate"] = "gFlyingDirtSpriteTemplate", - ["gHiddenPowerOrbScatterSpriteTemplate"] = "gHiddenPowerOrbScatterSpriteTemplate", - ["gHiddenPowerOrbSpriteTemplate"] = "gHiddenPowerOrbSpriteTemplate", - ["gHorizontalLungeSpriteTemplate"] = "gHorizontalLungeSpriteTemplate", - ["gIceBallParticleSpriteTemplate"] = "gIceBallParticleSpriteTemplate", - ["gIceBallSpriteTemplate"] = "gIceBallSpriteTemplate", - ["gIceBeamOuterParticleSpriteTemplate"] = "gIceBeamOuterParticleSpriteTemplate", - ["gIceSpikeSpriteTemplate"] = "gIceSpikeSpriteTemplate", - ["gIcicleSpearSpriteTemplate"] = "gIcicleSpearSpriteTemplate", - ["gLargeIceEffectParticleSpriteTemplate"] = "gLargeIceEffectParticleSpriteTemplate", - ["gLargeIcePunchCrystalSpriteTemplate"] = "gLargeIcePunchCrystalSpriteTemplate", - ["gLargeSnowballSpriteTemplate"] = "gLargeSnowballSpriteTemplate", - ["gLeechSeedSpriteTemplate"] = "gLeechSeedSpriteTemplate", - ["gLinearStingerSpriteTemplate"] = "gLinearStingerSpriteTemplate", - ["gMegaPunchKickSpriteTemplate"] = "gMegaPunchKickSpriteTemplate", - ["gMistBallSpriteTemplate"] = "gMistBallSpriteTemplate", - ["gMistCloudSpriteTemplate"] = "gMistCloudSpriteTemplate", - ["gMudSlapDirtSpriteTemplate"] = "gMudSlapDirtSpriteTemplate", - ["gMudSportDirtSpriteTemplate"] = "gMudSportDirtSpriteTemplate", - ["gPinMissileSpriteTemplate"] = "gPinMissileSpriteTemplate", - ["gPinkSmokeTemplate"] = "gPinkSmokeTemplate", - ["gPoisonBubbleSpriteTemplate"] = "gPoisonBubbleSpriteTemplate", - ["gPoisonGasCloudSpriteTemplate"] = "gPoisonGasCloudSpriteTemplate", - ["gPoisonPowderParticleSpriteTemplate"] = "gPoisonPowderParticleSpriteTemplate", - ["gSandAttackDirtSpriteTemplate"] = "gSandAttackDirtSpriteTemplate", - ["gScreechWaveSpriteTemplate"] = "gScreechWaveSpriteTemplate", - ["gShadowBallSpriteTemplate"] = "gShadowBallSpriteTemplate", - ["gSimplePaletteBlendSpriteTemplate"] = "gSimplePaletteBlendSpriteTemplate", - ["gSleepPowderParticleSpriteTemplate"] = "gSleepPowderParticleSpriteTemplate", - ["gSlideMonToOffsetSpriteTemplate"] = "gSlideMonToOffsetSpriteTemplate", - ["gSlideMonToOriginalPosSpriteTemplate"] = "gSlideMonToOriginalPosSpriteTemplate", - ["gSmallIceEffectParticleSpriteTemplate"] = "gSmallIceEffectParticleSpriteTemplate", - ["gSmallIcePunchCrystalSpriteTemplate"] = "gSmallIcePunchCrystalSpriteTemplate", - ["gSmallSnowballSpriteTemplate1"] = "gSmallSnowballSpriteTemplate1", - ["gSmallSnowballSpriteTemplate2"] = "gSmallSnowballSpriteTemplate2", - ["gSmogCloudSpriteTemplate"] = "gSmogCloudSpriteTemplate", - ["gSonicBoomSpriteTemplate"] = "gSonicBoomSpriteTemplate", - ["gStompFootSpriteTemplate"] = "gStompFootSpriteTemplate", - ["gStunSporeParticleSpriteTemplate"] = "gStunSporeParticleSpriteTemplate", - ["gSupersonicWaveSpriteTemplate"] = "gSupersonicWaveSpriteTemplate", - ["gSwiftStarSpriteTemplate"] = "gSwiftStarSpriteTemplate", - ["gVerticalDipSpriteTemplate"] = "gVerticalDipSpriteTemplate", - ["gWaterBubbleSpriteTemplate"] = "gWaterBubbleSpriteTemplate", - ["gWeatherBallHailSpriteTemplate"] = "gWeatherBallHailSpriteTemplate", + IceBeamCenterParticleSpriteTemplate = "gIceBeamInnerCrystalSpriteTemplate", + RainbowRingSpriteTemplate = "gAuroraBeamRingSpriteTemplate", + gAncientPowerRockSpriteTemplate = "gAncientPowerRockSpriteTemplate", + gBasicHitSplatSpriteTemplate = "gBasicHitSplatSpriteTemplate", + gBattleAnimSpriteTemplate_83930A8 = "gWeatherBallUpSpriteTemplate", + gBattleAnimSpriteTemplate_83930C0 = "gWeatherBallNormalDownSpriteTemplate", + gBattleAnimSpriteTemplate_83930F4 = "gSpinningSparkleSpriteTemplate", + gBattleAnimSpriteTemplate_83C2010 = "gSlideMonToOffsetAndBackSpriteTemplate", + gBattleAnimSpriteTemplate_83D62EC = "gPowerAbsorptionOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D6304 = "gSolarBeamBigOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D6350 = "gStockpileAbsorptionOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D637C = "gAbsorptionOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D6394 = "gHyperBeamOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D63F8 = "gSporeParticleSpriteTemplate", + gBattleAnimSpriteTemplate_83D6428 = "gPetalDanceBigFlowerSpriteTemplate", + gBattleAnimSpriteTemplate_83D6440 = "gPetalDanceSmallFlowerSpriteTemplate", + gBattleAnimSpriteTemplate_83D649C = "gRazorLeafParticleSpriteTemplate", + gBattleAnimSpriteTemplate_83D64B4 = "gTwisterLeafSpriteTemplate", + gBattleAnimSpriteTemplate_83D64E4 = "gRazorLeafCutterSpriteTemplate", + gBattleAnimSpriteTemplate_83D65A0 = "gConstrictBindingSpriteTemplate", + gBattleAnimSpriteTemplate_83D65E8 = "gMimicOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D6658 = "gIngrainRootSpriteTemplate", + gBattleAnimSpriteTemplate_83D6670 = "gFrenzyPlantRootSpriteTemplate", + gBattleAnimSpriteTemplate_83D6698 = "gIngrainOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D671C = "gPresentSpriteTemplate", + gBattleAnimSpriteTemplate_83D6764 = "gPresentHealParticleSpriteTemplate", + gBattleAnimSpriteTemplate_83D677C = "gItemStealSpriteTemplate", + gBattleAnimSpriteTemplate_83D67F4 = "gTrickBagSpriteTemplate", + gBattleAnimSpriteTemplate_83D68B8 = "gAromatherapySmallFlowerSpriteTemplate", + gBattleAnimSpriteTemplate_83D68D0 = "gAromatherapyBigFlowerSpriteTemplate", + gBattleAnimSpriteTemplate_83D693C = "gSilverWindBigSparkSpriteTemplate", + gBattleAnimSpriteTemplate_83D6954 = "gSilverWindMediumSparkSpriteTemplate", + gBattleAnimSpriteTemplate_83D696C = "gSilverWindSmallSparkSpriteTemplate", + gBattleAnimSpriteTemplate_83D6994 = "gNeedleArmSpikeSpriteTemplate", + gBattleAnimSpriteTemplate_83D69DC = "gSlamHitSpriteTemplate", + gBattleAnimSpriteTemplate_83D69F4 = "gVineWhipSpriteTemplate", + gBattleAnimSpriteTemplate_83D6B40 = "gAirCutterSliceSpriteTemplate", + gBattleAnimSpriteTemplate_83D6BE8 = "gProtectSpriteTemplate", + gBattleAnimSpriteTemplate_83D6C48 = "gMilkBottleSpriteTemplate", + gBattleAnimSpriteTemplate_83D6C88 = "gGrantingStarsSpriteTemplate", + gBattleAnimSpriteTemplate_83D6CA0 = "gSparklingStarsSpriteTemplate", + gBattleAnimSpriteTemplate_83D6D94 = "gSleepLetterZSpriteTemplate", + gBattleAnimSpriteTemplate_83D6DAC = "gLockOnTargetSpriteTemplate", + gBattleAnimSpriteTemplate_83D6DC4 = "gLockOnMoveTargetSpriteTemplate", + gBattleAnimSpriteTemplate_83D6DE4 = "gBowMonSpriteTemplate", + gBattleAnimSpriteTemplate_83D6E38 = "gSlashSliceSpriteTemplate", + gBattleAnimSpriteTemplate_83D6E50 = "gFalseSwipeSliceSpriteTemplate", + gBattleAnimSpriteTemplate_83D6E68 = "gFalseSwipePositionedSliceSpriteTemplate", + gBattleAnimSpriteTemplate_83D6EF0 = "gSharpenSphereSpriteTemplate", + gBattleAnimSpriteTemplate_83D6F08 = "gOctazookaBallSpriteTemplate", + gBattleAnimSpriteTemplate_83D6F3C = "gOctazookaSmokeSpriteTemplate", + gBattleAnimSpriteTemplate_83D6F80 = "gConversionSpriteTemplate", + gBattleAnimSpriteTemplate_83D6FB0 = "gConversion2SpriteTemplate", + gBattleAnimSpriteTemplate_83D6FC8 = "gMoonSpriteTemplate", + gBattleAnimSpriteTemplate_83D6FF8 = "gMoonlightSparkleSpriteTemplate", + gBattleAnimSpriteTemplate_83D7038 = "gHealingBlueStarSpriteTemplate", + gBattleAnimSpriteTemplate_83D7050 = "gHornHitSpriteTemplate", + gBattleAnimSpriteTemplate_83D7080 = "gSuperFangSpriteTemplate", + gBattleAnimSpriteTemplate_83D7114 = "gWavyMusicNotesSpriteTemplate", + gBattleAnimSpriteTemplate_83D715C = "gFastFlyingMusicNotesSpriteTemplate", + gBattleAnimSpriteTemplate_83D7174 = "gBellyDrumHandSpriteTemplate", + gBattleAnimSpriteTemplate_83D71A8 = "gSlowFlyingMusicNotesSpriteTemplate", + gBattleAnimSpriteTemplate_83D7220 = "gThoughtBubbleSpriteTemplate", + gBattleAnimSpriteTemplate_83D72C8 = "gMetronomeFingerSpriteTemplate", + gBattleAnimSpriteTemplate_83D72E0 = "gFollowMeFingerSpriteTemplate", + gBattleAnimSpriteTemplate_83D7358 = "gTauntFingerSpriteTemplate", + gBattleAnimSpriteTemplate_83D7450 = "gKinesisZapEnergySpriteTemplate", + gBattleAnimSpriteTemplate_83D748C = "gSwordsDanceBladeSpriteTemplate", + gBattleAnimSpriteTemplate_83D7564 = "gMetalSoundSpriteTemplate", + gBattleAnimSpriteTemplate_83D757C = "gWaterPulseRingSpriteTemplate", + gBattleAnimSpriteTemplate_83D7594 = "gEggThrowSpriteTemplate", + gBattleAnimSpriteTemplate_83D75E4 = "gCoinThrowSpriteTemplate", + gBattleAnimSpriteTemplate_83D75FC = "gFallingCoinSpriteTemplate", + gBattleAnimSpriteTemplate_83D7628 = "gBulletSeedSpriteTemplate", + gBattleAnimSpriteTemplate_83D765C = "gRazorWindTornadoSpriteTemplate", + gBattleAnimSpriteTemplate_83D769C = "gViceGripSpriteTemplate", + gBattleAnimSpriteTemplate_83D76DC = "gGuillotineSpriteTemplate", + gBattleAnimSpriteTemplate_83D7764 = "gBreathPuffSpriteTemplate", + gBattleAnimSpriteTemplate_83D7798 = "gAngerMarkSpriteTemplate", + gBattleAnimSpriteTemplate_83D77E0 = "gPencilSpriteTemplate", + gBattleAnimSpriteTemplate_83D77F8 = "gSnoreZSpriteTemplate", + gBattleAnimSpriteTemplate_83D7828 = "gExplosionSpriteTemplate", + gBattleAnimSpriteTemplate_83D78BC = "gSoftBoiledEggSpriteTemplate", + gBattleAnimSpriteTemplate_83D7928 = "gThinRingExpandingSpriteTemplate", + gBattleAnimSpriteTemplate_83D795C = "gThinRingShrinkingSpriteTemplate", + gBattleAnimSpriteTemplate_83D7974 = "gBlendThinRingExpandingSpriteTemplate", + gBattleAnimSpriteTemplate_83D798C = "gHyperVoiceRingSpriteTemplate", + gBattleAnimSpriteTemplate_83D79A4 = "gUproarRingSpriteTemplate", + gBattleAnimSpriteTemplate_83D7A44 = "gBellSpriteTemplate", + gBattleAnimSpriteTemplate_83D7A68 = "gHealBellMusicNoteSpriteTemplate", + gBattleAnimSpriteTemplate_83D7A80 = "gMagentaHeartSpriteTemplate", + gBattleAnimSpriteTemplate_83D7AB0 = "gRedHeartProjectileSpriteTemplate", + gBattleAnimSpriteTemplate_83D7AC8 = "gRedHeartBurstSpriteTemplate", + gBattleAnimSpriteTemplate_83D7AE0 = "gRedHeartRisingSpriteTemplate", + gBattleAnimSpriteTemplate_83D7B60 = "gSpitUpOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D7B94 = "gEyeSparkleSpriteTemplate", + gBattleAnimSpriteTemplate_83D7BB8 = "gAngelSpriteTemplate", + gBattleAnimSpriteTemplate_83D7BD0 = "gPinkHeartSpriteTemplate", + gBattleAnimSpriteTemplate_83D7C00 = "gDevilSpriteTemplate", + gBattleAnimSpriteTemplate_83D7C48 = "gFurySwipesSpriteTemplate", + gBattleAnimSpriteTemplate_83D7C90 = "gMovementWavesSpriteTemplate", + gBattleAnimSpriteTemplate_83D7CC8 = "gJaggedMusicNoteSpriteTemplate", + gBattleAnimSpriteTemplate_83D7D1C = "gPerishSongMusicNoteSpriteTemplate", + gBattleAnimSpriteTemplate_83D7D34 = "gPerishSongMusicNote2SpriteTemplate", + gBattleAnimSpriteTemplate_83D7D74 = "gGuardRingSpriteTemplate", + gBattleAnimSpriteTemplate_83D9178 = "gWaterBubbleProjectileSpriteTemplate", + gBattleAnimSpriteTemplate_83D91F4 = "gHydroPumpOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D920C = "gMudShotOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D9224 = "gSignalBeamRedOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D923C = "gSignalBeamGreenOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D9268 = "gFlamethrowerFlameSpriteTemplate", + gBattleAnimSpriteTemplate_83D9280 = "gPsywaveRingSpriteTemplate", + gBattleAnimSpriteTemplate_83D92D0 = "gHydroCannonChargeSpriteTemplate", + gBattleAnimSpriteTemplate_83D92E8 = "gHydroCannonBeamSpriteTemplate", + gBattleAnimSpriteTemplate_83D9318 = "gWaterGunProjectileSpriteTemplate", + gBattleAnimSpriteTemplate_83D9330 = "gWaterGunDropletSpriteTemplate", + gBattleAnimSpriteTemplate_83D9348 = "gSmallBubblePairSpriteTemplate", + gBattleAnimSpriteTemplate_83D9360 = "gSmallDriftingBubblesSpriteTemplate", + gBattleAnimSpriteTemplate_83D9408 = "gWaterPulseBubbleSpriteTemplate", + gBattleAnimSpriteTemplate_83D9438 = "gWeatherBallWaterDownSpriteTemplate", + gBattleAnimSpriteTemplate_83D9478 = "gFireSpiralInwardSpriteTemplate", + gBattleAnimSpriteTemplate_83D9490 = "gFireSpreadSpriteTemplate", + gBattleAnimSpriteTemplate_83D9508 = "gLargeFlameSpriteTemplate", + gBattleAnimSpriteTemplate_83D9520 = "gLargeFlameScatterSpriteTemplate", + gBattleAnimSpriteTemplate_83D9538 = "gFirePlumeSpriteTemplate", + gBattleAnimSpriteTemplate_83D95B0 = "gSunlightRaySpriteTemplate", + gBattleAnimSpriteTemplate_83D9614 = "gBurnFlameSpriteTemplate", + gBattleAnimSpriteTemplate_83D9694 = "gFireSpiralOutwardSpriteTemplate", + gBattleAnimSpriteTemplate_83D96AC = "gWeatherBallFireDownSpriteTemplate", + gBattleAnimSpriteTemplate_83D96F8 = "gEruptionFallingRockSpriteTemplate", + gBattleAnimSpriteTemplate_83D974C = "gWillOWispOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D977C = "gWillOWispFireSpriteTemplate", + gBattleAnimSpriteTemplate_83D97D0 = "gLightningSpriteTemplate", + gBattleAnimSpriteTemplate_83D985C = "gSparkElectricitySpriteTemplate", + gBattleAnimSpriteTemplate_83D9874 = "gZapCannonBallSpriteTemplate", + gBattleAnimSpriteTemplate_83D98A0 = "gZapCannonSparkSpriteTemplate", + gBattleAnimSpriteTemplate_83D98F0 = "gThunderboltOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D9908 = "gSparkElectricityFlashingSpriteTemplate", + gBattleAnimSpriteTemplate_83D9950 = "gThunderWaveSpriteTemplate", + gBattleAnimSpriteTemplate_83D9A6C = "gGrowingChargeOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D9A9C = "gElectricPuffSpriteTemplate", + gBattleAnimSpriteTemplate_83D9AB4 = "gVoltTackleOrbSlideSpriteTemplate", + gBattleAnimSpriteTemplate_83D9B28 = "gGrowingShockWaveOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83D9F78 = "gKarateChopSpriteTemplate", + gBattleAnimSpriteTemplate_83D9F90 = "gJumpKickSpriteTemplate", + gBattleAnimSpriteTemplate_83D9FC0 = "gFistFootRandomPosSpriteTemplate", + gBattleAnimSpriteTemplate_83D9FD8 = "gCrossChopHandSpriteTemplate", + gBattleAnimSpriteTemplate_83D9FF0 = "gSlidingKickSpriteTemplate", + gBattleAnimSpriteTemplate_83DA024 = "gSpinningHandOrFootSpriteTemplate", + gBattleAnimSpriteTemplate_83DA088 = "gDizzyPunchDuckSpriteTemplate", + gBattleAnimSpriteTemplate_83DA0A0 = "gBrickBreakWallSpriteTemplate", + gBattleAnimSpriteTemplate_83DA0B8 = "gBrickBreakWallShardSpriteTemplate", + gBattleAnimSpriteTemplate_83DA0FC = "gSuperpowerOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83DA114 = "gSuperpowerRockSpriteTemplate", + gBattleAnimSpriteTemplate_83DA12C = "gSuperpowerFireballSpriteTemplate", + gBattleAnimSpriteTemplate_83DA144 = "gArmThrustHandSpriteTemplate", + gBattleAnimSpriteTemplate_83DA198 = "gRevengeSmallScratchSpriteTemplate", + gBattleAnimSpriteTemplate_83DA1E0 = "gRevengeBigScratchSpriteTemplate", + gBattleAnimSpriteTemplate_83DA214 = "gFocusPunchFistSpriteTemplate", + gBattleAnimSpriteTemplate_83DA244 = "gToxicBubbleSpriteTemplate", + gBattleAnimSpriteTemplate_83DA2B8 = "gSludgeProjectileSpriteTemplate", + gBattleAnimSpriteTemplate_83DA2D0 = "gAcidPoisonBubbleSpriteTemplate", + gBattleAnimSpriteTemplate_83DA2E8 = "gSludgeBombHitParticleSpriteTemplate", + gBattleAnimSpriteTemplate_83DA31C = "gAcidPoisonDropletSpriteTemplate", + gBattleAnimSpriteTemplate_83DA380 = "gEllipticalGustSpriteTemplate", + gBattleAnimSpriteTemplate_83DA3B4 = "gGustToTargetSpriteTemplate", + gBattleAnimSpriteTemplate_83DA3E4 = "gAirWaveCrescentSpriteTemplate", + gBattleAnimSpriteTemplate_83DA450 = "gFlyBallUpSpriteTemplate", + gBattleAnimSpriteTemplate_83DA468 = "gFlyBallAttackSpriteTemplate", + gBattleAnimSpriteTemplate_83DA498 = "gFallingFeatherSpriteTemplate", + gBattleAnimSpriteTemplate_83DA51C = "gWhirlwindLineSpriteTemplate", + gBattleAnimSpriteTemplate_83DA568 = "gBounceBallShrinkSpriteTemplate", + gBattleAnimSpriteTemplate_83DA594 = "gBounceBallLandSpriteTemplate", + gBattleAnimSpriteTemplate_83DA5D8 = "gDiveBallSpriteTemplate", + gBattleAnimSpriteTemplate_83DA614 = "gDiveWaterSplashSpriteTemplate", + gBattleAnimSpriteTemplate_83DA62C = "gSprayWaterDropletSpriteTemplate", + gBattleAnimSpriteTemplate_83DA65C = "gSkyAttackBirdSpriteTemplate", + gBattleAnimSpriteTemplate_83DA690 = "gPsychUpSpiralSpriteTemplate", + gBattleAnimSpriteTemplate_83DA6A8 = "gLightScreenWallSpriteTemplate", + gBattleAnimSpriteTemplate_83DA6C0 = "gReflectWallSpriteTemplate", + gBattleAnimSpriteTemplate_83DA6D8 = "gMirrorCoatWallSpriteTemplate", + gBattleAnimSpriteTemplate_83DA6F0 = "gBarrierWallSpriteTemplate", + gBattleAnimSpriteTemplate_83DA708 = "gMagicCoatWallSpriteTemplate", + gBattleAnimSpriteTemplate_83DA73C = "gReflectSparkleSpriteTemplate", + gBattleAnimSpriteTemplate_83DA76C = "gSpecialScreenSparkleSpriteTemplate", + gBattleAnimSpriteTemplate_83DA784 = "gGoldRingSpriteTemplate", + gBattleAnimSpriteTemplate_83DA824 = "gBentSpoonSpriteTemplate", + gBattleAnimSpriteTemplate_83DA88C = "gQuestionMarkSpriteTemplate", + gBattleAnimSpriteTemplate_83DA8F4 = "gRedXSpriteTemplate", + gBattleAnimSpriteTemplate_83DA9E0 = "gLusterPurgeCircleSpriteTemplate", + gBattleAnimSpriteTemplate_83DAA68 = "gPsychoBoostOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83DAABC = "gMegahornHornSpriteTemplate", + gBattleAnimSpriteTemplate_83DAB10 = "gLeechLifeNeedleSpriteTemplate", + gBattleAnimSpriteTemplate_83DAB28 = "gWebThreadSpriteTemplate", + gBattleAnimSpriteTemplate_83DAB40 = "gStringWrapSpriteTemplate", + gBattleAnimSpriteTemplate_83DAB74 = "gSpiderWebSpriteTemplate", + gBattleAnimSpriteTemplate_83DAC10 = "gTailGlowOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83DAC4C = "gFallingRockSpriteTemplate", + gBattleAnimSpriteTemplate_83DAC64 = "gRockFragmentSpriteTemplate", + gBattleAnimSpriteTemplate_83DAC7C = "gSwirlingDirtSpriteTemplate", + gBattleAnimSpriteTemplate_83DACB8 = "gWhirlpoolSpriteTemplate", + gBattleAnimSpriteTemplate_83DACD0 = "gFireSpinSpriteTemplate", + gBattleAnimSpriteTemplate_83DADA8 = "gRockTombRockSpriteTemplate", + gBattleAnimSpriteTemplate_83DADE8 = "gRockBlastRockSpriteTemplate", + gBattleAnimSpriteTemplate_83DAE00 = "gRockScatterSpriteTemplate", + gBattleAnimSpriteTemplate_83DAE18 = "gTwisterRockSpriteTemplate", + gBattleAnimSpriteTemplate_83DAE30 = "gWeatherBallRockDownSpriteTemplate", + gBattleAnimSpriteTemplate_83DAE64 = "gConfuseRayBallBounceSpriteTemplate", + gBattleAnimSpriteTemplate_83DAE7C = "gConfuseRayBallSpiralSpriteTemplate", + gBattleAnimSpriteTemplate_83DAEDC = "gLickSpriteTemplate", + gBattleAnimSpriteTemplate_83DAF20 = "gCurseNailSpriteTemplate", + gBattleAnimSpriteTemplate_83DAF38 = "gCurseGhostSpriteTemplate", + gBattleAnimSpriteTemplate_83DAF50 = "gNightmareDevilSpriteTemplate", + gBattleAnimSpriteTemplate_83DAFCC = "gOutrageFlameSpriteTemplate", + gBattleAnimSpriteTemplate_83DB044 = "gDragonBreathFireSpriteTemplate", + gBattleAnimSpriteTemplate_83DB078 = "gDragonRageFirePlumeSpriteTemplate", + gBattleAnimSpriteTemplate_83DB0D0 = "gDragonRageFireSpitSpriteTemplate", + gBattleAnimSpriteTemplate_83DB0E8 = "gDragonDanceOrbSpriteTemplate", + gBattleAnimSpriteTemplate_83DB100 = "gOverheatFlameSpriteTemplate", + gBattleAnimSpriteTemplate_83DB1D0 = "gSharpTeethSpriteTemplate", + gBattleAnimSpriteTemplate_83DB1E8 = "gClampJawSpriteTemplate", + gBattleAnimSpriteTemplate_83DB238 = "gTearDropSpriteTemplate", + gBattleAnimSpriteTemplate_83DB288 = "gClawSlashSpriteTemplate", + gBattleAnimSpriteTemplate_83DB3DC = "gComplexPaletteBlendSpriteTemplate", + gBattleAnimSpriteTemplate_83DB428 = "gShakeMonOrTerrainSpriteTemplate", + gBattleAnimSpriteTemplate_83DB4C0 = "gHandleInvertHitSplatSpriteTemplate", + gBattleAnimSpriteTemplate_83DB4D8 = "gWaterHitSplatSpriteTemplate", + gBattleAnimSpriteTemplate_83DB4F0 = "gRandomPosHitSplatSpriteTemplate", + gBattleAnimSpriteTemplate_83DB508 = "gMonEdgeHitSplatSpriteTemplate", + gBattleAnimSpriteTemplate_83DB520 = "gCrossImpactSpriteTemplate", + gBattleAnimSpriteTemplate_83DB538 = "gFlashingHitSplatSpriteTemplate", + gBattleAnimSpriteTemplate_83DB550 = "gPersistHitSplatSpriteTemplate", + gBattleAnimSpriteTemplate_8402180 = "gScratchSpriteTemplate", + gBattleAnimSpriteTemplate_8402198 = "gBlackSmokeSpriteTemplate", + gBattleAnimSpriteTemplate_84021B0 = "gBlackBallSpriteTemplate", + gBattleAnimSpriteTemplate_84021DC = "gOpeningEyeSpriteTemplate", + gBattleAnimSpriteTemplate_84021F4 = "gWhiteHaloSpriteTemplate", + gBattleAnimSpriteTemplate_840220C = "gTealAlertSpriteTemplate", + gBattleAnimSpriteTemplate_8402264 = "gMeanLookEyeSpriteTemplate", + gBattleAnimSpriteTemplate_840227C = "gSpikesSpriteTemplate", + gBattleAnimSpriteTemplate_84022B0 = "gLeerSpriteTemplate", + gBattleAnimSpriteTemplate_84022F0 = "gLetterZSpriteTemplate", + gBattleAnimSpriteTemplate_840233C = "gFangSpriteTemplate", + gBattleAnimSpriteTemplate_840238C = "gSpotlightSpriteTemplate", + gBattleAnimSpriteTemplate_84023A4 = "gClappingHandSpriteTemplate", + gBattleAnimSpriteTemplate_84023BC = "gClappingHand2SpriteTemplate", + gBattleAnimSpriteTemplate_84023E8 = "gRapidSpinSpriteTemplate", + gBattleAnimSpriteTemplate_8402458 = "gTriAttackTriangleSpriteTemplate", + gBattleAnimSpriteTemplate_8402498 = "gEclipsingOrbSpriteTemplate", + gBattleAnimSpriteTemplate_84024D0 = "gBatonPassPokeballSpriteTemplate", + gBattleAnimSpriteTemplate_84024E8 = "gWishStarSpriteTemplate", + gBattleAnimSpriteTemplate_8402578 = "gSwallowBlueOrbSpriteTemplate", + gBattleAnimSpriteTemplate_84025EC = "gGreenStarSpriteTemplate", + gBattleAnimSpriteTemplate_8402630 = "gWeakFrustrationAngerMarkSpriteTemplate", + gBattleAnimSpriteTemplate_84026A4 = "gSweetScentPetalSpriteTemplate", + gBattleAnimSpriteTemplate_84026F0 = "gPainSplitProjectileSpriteTemplate", + gBattleAnimSpriteTemplate_8402708 = "gFlatterConfettiSpriteTemplate", + gBattleAnimSpriteTemplate_8402720 = "gFlatterSpotlightSpriteTemplate", + gBattleAnimSpriteTemplate_8402738 = "gReversalOrbSpriteTemplate", + gBattleAnimSpriteTemplate_84027EC = "gYawnCloudSpriteTemplate", + gBattleAnimSpriteTemplate_8402934 = "gRoarNoiseLineSpriteTemplate", + gBattleAnimSpriteTemplate_8402964 = "gAssistPawprintSpriteTemplate", + gBattleAnimSpriteTemplate_84029C4 = "gSmellingSaltsHandSpriteTemplate", + gBattleAnimSpriteTemplate_84029F4 = "gSmellingSaltExclamationSpriteTemplate", + gBattleAnimSpriteTemplate_8402A0C = "gHelpingHandClapSpriteTemplate", + gBattleAnimSpriteTemplate_8402A24 = "gForesightMagnifyingGlassSpriteTemplate", + gBattleAnimSpriteTemplate_8402A3C = "gMeteorMashStarSpriteTemplate", + gBattleAnimSpriteTemplate_8402A6C = "gBlockXSpriteTemplate", + gBattleAnimSpriteTemplate_8402AE4 = "gKnockOffStrikeSpriteTemplate", + gBattleAnimSpriteTemplate_8402B10 = "gRecycleSpriteTemplate", + gBattleAnimSpriteTemplate_840B4FC = "gSafariBaitSpriteTemplate", + gBattleAnimSpriteTemplate_EndureFlame = "gEndureEnergySpriteTemplate", + gBoneHitSpriteTemplate = "gSpinningBoneSpriteTemplate", + gBonemerangSpriteTemplate = "gBonemerangSpriteTemplate", + gConfusionDuckSpriteTemplate = "gConfusionDuckSpriteTemplate", + gCuttingSliceSpriteTemplate = "gCuttingSliceSpriteTemplate", + gDigDirtMoundSpriteTemplate = "gDirtMoundSpriteTemplate", + gDirtPlumeSpriteTemplate = "gDirtPlumeSpriteTemplate", + gElectricitySpriteTemplate = "gElectricitySpriteTemplate", + gEmberFlareSpriteTemplate = "gEmberFlareSpriteTemplate", + gEmberSpriteTemplate = "gEmberSpriteTemplate", + gFallingItemBagTemplate = "gKnockOffItemSpriteTemplate", + gFireCrossSpriteTemplate = "gFireBlastCrossSpriteTemplate", + gFireRingSpriteTemplate = "gFireBlastRingSpriteTemplate", + gFistFootSpriteTemplate = "gFistFootSpriteTemplate", + gFlyingDirtSpriteTemplate = "gFlyingSandCrescentSpriteTemplate", + gHiddenPowerOrbScatterSpriteTemplate = "gHiddenPowerOrbScatterSpriteTemplate", + gHiddenPowerOrbSpriteTemplate = "gHiddenPowerOrbSpriteTemplate", + gHorizontalLungeSpriteTemplate = "gHorizontalLungeSpriteTemplate", + gIceBallParticleSpriteTemplate = "gIceBallImpactShardSpriteTemplate", + gIceBallSpriteTemplate = "gIceBallChunkSpriteTemplate", + gIceBeamOuterParticleSpriteTemplate = "gIceBeamOuterCrystalSpriteTemplate", + gIceSpikeSpriteTemplate = "gIceGroundSpikeSpriteTemplate", + gIcicleSpearSpriteTemplate = "gIcicleSpearSpriteTemplate", + gLargeIceEffectParticleSpriteTemplate = "gIceCrystalHitLargeSpriteTemplate", + gLargeIcePunchCrystalSpriteTemplate = "gIceCrystalSpiralInwardLarge", + gLargeSnowballSpriteTemplate = "gBlizzardIceCrystalSpriteTemplate", + gLeechSeedSpriteTemplate = "gLeechSeedSpriteTemplate", + gLinearStingerSpriteTemplate = "gLinearStingerSpriteTemplate", + gMegaPunchKickSpriteTemplate = "gMegaPunchKickSpriteTemplate", + gMistBallSpriteTemplate = "gMistBallSpriteTemplate", + gMistCloudSpriteTemplate = "gMistCloudSpriteTemplate", + gMudSlapDirtSpriteTemplate = "gMudSlapMudSpriteTemplate", + gMudSportDirtSpriteTemplate = "gMudsportMudSpriteTemplate", + gPinMissileSpriteTemplate = "gPinMissileSpriteTemplate", + gPinkSmokeTemplate = "gSmokeBallEscapeCloudSpriteTemplate", + gPoisonBubbleSpriteTemplate = "gPoisonBubbleSpriteTemplate", + gPoisonGasCloudSpriteTemplate = "gPoisonGasCloudSpriteTemplate", + gPoisonPowderParticleSpriteTemplate = "gPoisonPowderParticleSpriteTemplate", + gSandAttackDirtSpriteTemplate = "gSandAttackDirtSpriteTemplate", + gScreechWaveSpriteTemplate = "gScreechRingSpriteTemplate", + gShadowBallSpriteTemplate = "gShadowBallSpriteTemplate", + gSimplePaletteBlendSpriteTemplate = "gSimplePaletteBlendSpriteTemplate", + gSleepPowderParticleSpriteTemplate = "gSleepPowderParticleSpriteTemplate", + gSlideMonToOffsetSpriteTemplate = "gSlideMonToOffsetSpriteTemplate", + gSlideMonToOriginalPosSpriteTemplate = "gSlideMonToOriginalPosSpriteTemplate", + gSmallIceEffectParticleSpriteTemplate = "gIceCrystalHitSmallSpriteTemplate", + gSmallIcePunchCrystalSpriteTemplate = "gIceCrystalSpiralInwardSmall", + gSmallSnowballSpriteTemplate1 = "gSwirlingSnowballSpriteTemplate", + gSmallSnowballSpriteTemplate2 = "gPowderSnowSnowballSpriteTemplate", + gSmogCloudSpriteTemplate = "gSmogCloudSpriteTemplate", + gSonicBoomSpriteTemplate = "gSonicBoomSpriteTemplate", + gStompFootSpriteTemplate = "gStompFootSpriteTemplate", + gStunSporeParticleSpriteTemplate = "gStunSporeParticleSpriteTemplate", + gSupersonicWaveSpriteTemplate = "gSupersonicRingSpriteTemplate", + gSwiftStarSpriteTemplate = "gSwiftStarSpriteTemplate", + gVerticalDipSpriteTemplate = "gVerticalDipSpriteTemplate", + gWaterBubbleSpriteTemplate = "gWaterBubbleSpriteTemplate", + gWeatherBallHailSpriteTemplate = "gWeatherBallIceDownSpriteTemplate", }, callbacks = { - sub_807A908 = "sub_807A908", - sub_807A9BC = "sub_807A9BC", - sub_807A63C = "sub_807A63C", - ["AnimAuroraRings"] = "AuroraRings", - ["AnimBasicFistOrFoot"] = "BasicFistOrFoot", - ["AnimBoneHitProjectile"] = "BoneHitProjectile", - ["AnimBonemerangProjectile"] = "BonemerangProjectile", - ["AnimBubbleEffect"] = "BubbleEffect", - ["AnimConfusionDuck"] = "ConfusionDuck", - ["AnimCuttingSlice"] = "CuttingSlice", - ["AnimDigDirtMound"] = "DigDirtMound", - ["AnimDirtParticleAcrossScreen"] = "DirtParticleAcrossScreen", - ["AnimDirtScatter"] = "DirtScatter", - ["AnimEmberFlare"] = "EmberFlare", - ["AnimFallingFeather"] = "FallingFeather", - ["AnimFireCross"] = "FireCross", - ["AnimFireRing"] = "FireRing", - ["AnimFissureDirtPlumeParticle"] = "FissureDirtPlumeParticle", - ["AnimIceBeamParticle"] = "IceBeamParticle", - ["AnimIceEffectParticle"] = "IceEffectParticle", - ["AnimIcePunchSwirlingParticle"] = "IcePunchSwirlingParticle", - ["AnimKnockOffStrike"] = "KnockOffStrike", - ["AnimLeechSeed"] = "LeechSeed", - ["AnimMissileArc"] = "MissileArc", - ["AnimMoveParticleBeyondTarget"] = "MoveParticleBeyondTarget", - ["AnimMovePowderParticle"] = "MovePowderParticle", - ["AnimMoveTwisterParticle"] = "MoveTwisterParticle", - ["AnimMudSportDirt"] = "MudSportDirt", - ["AnimOrbitFast"] = "OrbitFast", - ["AnimOrbitScatter"] = "OrbitScatter", - ["AnimRaiseSprite"] = "RaiseSprite", - ["AnimRecycle"] = "Recycle", - ["AnimSimplePaletteBlend"] = "SimplePaletteBlend", - ["AnimSonicBoomProjectile"] = "SonicBoomProjectile", - ["AnimSpinningKickOrPunch"] = "SpinningKickOrPunch", - ["AnimStompFoot"] = "StompFoot", - ["AnimSwirlingSnowball_Step1"] = "SwirlingSnowball_Step1", - ["AnimThrowMistBall"] = "ThrowMistBall", - ["AnimTranslateLinearSingleSineWave"] = "TranslateLinearSingleSineWave", - ["AnimTranslateStinger"] = "TranslateStinger", - ["AnimWaveFromCenterOfTarget"] = "WaveFromCenterOfTarget", - ["DoHorizontalLunge"] = "DoHorizontalLunge", - ["DoVerticalDip"] = "DoVerticalDip", - ["InitAnimShadowBall"] = "InitAnimShadowBall", - ["InitIceBallAnim"] = "InitIceBallAnim", - ["InitIceBallParticle"] = "InitIceBallParticle", - ["InitPoisonGasCloudAnim"] = "InitPoisonGasCloudAnim", - ["InitSwirlingFogAnim"] = "InitSwirlingFogAnim", - ["SlideMonToOffset"] = "SlideMonToOffset", - ["SlideMonToOriginalPos"] = "SlideMonToOriginalPos", - ["TranslateAnimSpriteToTargetMonLocation"] = "TranslateAnimSpriteToTargetMonLocation", - ["sub_80793C4"] = "sub_80793C4", - ["sub_80794A8"] = "sub_80794A8", - ["sub_8079534"] = "sub_8079534", - ["sub_807A9BC"] = "sub_807A9BC", - ["sub_80A8818"] = "sub_80A8818", - ["sub_80CA7B0"] = "sub_80CA7B0", - ["sub_80CA800"] = "sub_80CA800", - ["sub_80CA9A8"] = "sub_80CA9A8", - ["sub_80CAA14"] = "sub_80CAA14", - ["sub_80CABF8"] = "sub_80CABF8", - ["sub_80CAD54"] = "sub_80CAD54", - ["sub_80CAE20"] = "sub_80CAE20", - ["sub_80CAED8"] = "sub_80CAED8", - ["sub_80CB25C"] = "sub_80CB25C", - ["sub_80CB4CC"] = "sub_80CB4CC", - ["sub_80CB59C"] = "sub_80CB59C", - ["sub_80CB620"] = "sub_80CB620", - ["sub_80CB768"] = "sub_80CB768", - ["sub_80CB94C"] = "sub_80CB94C", - ["sub_80CBA28"] = "sub_80CBA28", - ["sub_80CBAA4"] = "sub_80CBAA4", - ["sub_80CBAE8"] = "sub_80CBAE8", - ["sub_80CBBF0"] = "sub_80CBBF0", - ["sub_80CC474"] = "sub_80CC474", - ["sub_80CC6CC"] = "sub_80CC6CC", - ["sub_80CC884"] = "sub_80CC884", - ["sub_80CC9BC"] = "sub_80CC9BC", - ["sub_80CCD24"] = "sub_80CCD24", - ["sub_80CCF04"] = "sub_80CCF04", - ["sub_80CD140"] = "sub_80CD140", - ["sub_80CD190"] = "sub_80CD190", - ["sub_80CD328"] = "sub_80CD328", - ["sub_80CD3E0"] = "sub_80CD3E0", - ["sub_80CD6CC"] = "sub_80CD6CC", - ["sub_80CD774"] = "sub_80CD774", - ["sub_80CDD74"] = "sub_80CDD74", - ["sub_80CDDDC"] = "sub_80CDDDC", - ["sub_80CDE24"] = "sub_80CDE24", - ["sub_80CDF0C"] = "sub_80CDF0C", - ["sub_80CDFB0"] = "sub_80CDFB0", - ["sub_80CE09C"] = "sub_80CE09C", - ["sub_80CE17C"] = "sub_80CE17C", - ["sub_80CE30C"] = "sub_80CE30C", - ["sub_80CE36C"] = "sub_80CE36C", - ["sub_80CE670"] = "sub_80CE670", - ["sub_80CEA04"] = "sub_80CEA04", - ["sub_80CEB0C"] = "sub_80CEB0C", - ["sub_80CECE8"] = "sub_80CECE8", - ["sub_80CEDF0"] = "sub_80CEDF0", - ["sub_80CEE60"] = "sub_80CEE60", - ["sub_80CEF9C"] = "sub_80CEF9C", - ["sub_80CF040"] = "sub_80CF040", - ["sub_80CF0BC"] = "sub_80CF0BC", - ["sub_80CF1C8"] = "sub_80CF1C8", - ["sub_80CF610"] = "sub_80CF610", - ["sub_80CF690"] = "sub_80CF690", - ["sub_80CFE9C"] = "sub_80CFE9C", - ["sub_80CFF50"] = "sub_80CFF50", - ["sub_80CFFD8"] = "sub_80CFFD8", - ["sub_80D0118"] = "sub_80D0118", - ["sub_80D0178"] = "sub_80D0178", - ["sub_80D0228"] = "sub_80D0228", - ["sub_80D0930"] = "sub_80D0930", - ["sub_80D09C0"] = "sub_80D09C0", - ["sub_80D0E30"] = "sub_80D0E30", - ["sub_80D0FD8"] = "sub_80D0FD8", - ["sub_80D10B8"] = "sub_80D10B8", - ["sub_80D1318"] = "sub_80D1318", - ["sub_80D1368"] = "sub_80D1368", - ["sub_80D1C08"] = "sub_80D1C08", - ["sub_80D1C80"] = "sub_80D1C80", - ["sub_80D1F58"] = "sub_80D1F58", - ["sub_80D1FDC"] = "sub_80D1FDC", - ["sub_80D2064"] = "sub_80D2064", - ["sub_80D28AC"] = "sub_80D28AC", - ["sub_80D2920"] = "sub_80D2920", - ["sub_80D2938"] = "sub_80D2938", - ["sub_80D2A38"] = "sub_80D2A38", - ["sub_80D2ABC"] = "sub_80D2ABC", - ["sub_80D2BE8"] = "sub_80D2BE8", - ["sub_80D2C38"] = "sub_80D2C38", - ["sub_80D2D68"] = "sub_80D2D68", - ["sub_80D2E68"] = "sub_80D2E68", - ["sub_80D2EC8"] = "sub_80D2EC8", - ["sub_80D3014"] = "sub_80D3014", - ["sub_80D31C8"] = "sub_80D31C8", - ["sub_80D3554"] = "sub_80D3554", - ["sub_80D3698"] = "sub_80D3698", - ["sub_80D3728"] = "sub_80D3728", - ["sub_80D37FC"] = "sub_80D37FC", - ["sub_80D3838"] = "sub_80D3838", - ["sub_80D4044"] = "sub_80D4044", - ["sub_80D4BF0"] = "sub_80D4BF0", - ["sub_80D4CA4"] = "sub_80D4CA4", - ["sub_80D4ED8"] = "sub_80D4ED8", - ["sub_80D4F18"] = "sub_80D4F18", - ["sub_80D4F5C"] = "sub_80D4F5C", - ["sub_80D4FCC"] = "sub_80D4FCC", - ["sub_80D517C"] = "sub_80D517C", - ["sub_80D5210"] = "sub_80D5210", - ["sub_80D53B4"] = "sub_80D53B4", - ["sub_80D5A20"] = "sub_80D5A20", - ["sub_80D5B0C"] = "sub_80D5B0C", - ["sub_80D5CC0"] = "sub_80D5CC0", - ["sub_80D61C8"] = "sub_80D61C8", - ["sub_80D6328"] = "sub_80D6328", - ["sub_80D648C"] = "sub_80D648C", - ["sub_80D65DC"] = "sub_80D65DC", - ["sub_80D6658"] = "sub_80D6658", - ["sub_80D679C"] = "sub_80D679C", - ["sub_80D6A6C"] = "sub_80D6A6C", - ["sub_80D6D18"] = "sub_80D6D18", - ["sub_80D6D70"] = "sub_80D6D70", - ["sub_80D6DD8"] = "sub_80D6DD8", - ["sub_80D727C"] = "sub_80D727C", - ["sub_80D902C"] = "sub_80D902C", - ["sub_80D9078"] = "sub_80D9078", - ["sub_80D90F4"] = "sub_80D90F4", - ["sub_80D92D0"] = "sub_80D92D0", - ["sub_80D9378"] = "sub_80D9378", - ["sub_80D9540"] = "sub_80D9540", - ["sub_80D95D0"] = "sub_80D95D0", - ["sub_80D96B8"] = "sub_80D96B8", - ["sub_80D97CC"] = "sub_80D97CC", - ["sub_80D98D8"] = "sub_80D98D8", - ["sub_80D9A38"] = "sub_80D9A38", - ["sub_80D9B48"] = "sub_80D9B48", - ["sub_80D9BD4"] = "sub_80D9BD4", - ["sub_80D9C40"] = "sub_80D9C40", - ["sub_80D9D70"] = "sub_80D9D70", - ["sub_80D9DF0"] = "sub_80D9DF0", - ["sub_80D9E94"] = "sub_80D9E94", - ["sub_80D9F14"] = "sub_80D9F14", - ["sub_80DA034"] = "sub_80DA034", - ["sub_80DA16C"] = "sub_80DA16C", - ["sub_80DA208"] = "sub_80DA208", - ["sub_80DA300"] = "sub_80DA300", - ["sub_80DA38C"] = "sub_80DA38C", - ["sub_80DB000"] = "sub_80DB000", - ["sub_80DB194"] = "sub_80DB194", - ["sub_80DB1F4"] = "sub_80DB1F4", - ["sub_80DB288"] = "sub_80DB288", - ["sub_80DB374"] = "sub_80DB374", - ["sub_80DB458"] = "sub_80DB458", - ["sub_80DB5E4"] = "sub_80DB5E4", - ["sub_80DB74C"] = "sub_80DB74C", - ["sub_80DBA4C"] = "sub_80DBA4C", - ["sub_80DBAF4"] = "sub_80DBAF4", - ["sub_80DBB70"] = "sub_80DBB70", - ["sub_80DC068"] = "sub_80DC068", - ["sub_80DC700"] = "sub_80DC700", - ["sub_80DC824"] = "sub_80DC824", - ["sub_80DC8F4"] = "sub_80DC8F4", - ["sub_80DC9A0"] = "sub_80DC9A0", - ["sub_80DCA70"] = "sub_80DCA70", - ["sub_80DCB38"] = "sub_80DCB38", - ["sub_80DCE40"] = "sub_80DCE40", - ["sub_80DCE9C"] = "sub_80DCE9C", - ["sub_80DCF60"] = "sub_80DCF60", - ["sub_80DCFE4"] = "sub_80DCFE4", - ["sub_80DD8E8"] = "sub_80DD8E8", - ["sub_80DD978"] = "sub_80DD978", - ["sub_80DD9A4"] = "sub_80DD9A4", - ["sub_80DDB6C"] = "sub_80DDB6C", - ["sub_80DDD58"] = "sub_80DDD58", - ["sub_80DE0FC"] = "sub_80DE0FC", - ["sub_80DEF3C"] = "sub_80DEF3C", - ["sub_80DF0B8"] = "sub_80DF0B8", - ["sub_80DF5A0"] = "sub_80DF5A0", - ["sub_80DF6F0"] = "sub_80DF6F0", - ["sub_80DF760"] = "sub_80DF760", - ["sub_80DF78C"] = "sub_80DF78C", - ["sub_80DFB28"] = "sub_80DFB28", - ["sub_80DFF1C"] = "sub_80DFF1C", - ["sub_80DFFD0"] = "sub_80DFFD0", - ["sub_80E0A10"] = "sub_80E0A10", - ["sub_80E1E2C"] = "sub_80E1E2C", - ["sub_80E24B8"] = "sub_80E24B8", - ["sub_80E27A0"] = "sub_80E27A0", - ["sub_80E27E8"] = "sub_80E27E8", - ["sub_80E2838"] = "sub_80E2838", - ["sub_80E2870"] = "sub_80E2870", - ["sub_80E2908"] = "sub_80E2908", - ["sub_80E2978"] = "sub_80E2978", - ["sub_80E29C0"] = "sub_80E29C0", - ["sub_812C144"] = "sub_812C144", - ["sub_812C220"] = "sub_812C220", - ["sub_812C2BC"] = "sub_812C2BC", - ["sub_812C358"] = "sub_812C358", - ["sub_812C720"] = "sub_812C720", - ["sub_812C80C"] = "sub_812C80C", - ["sub_812C848"] = "sub_812C848", - ["sub_812C908"] = "sub_812C908", - ["sub_812C990"] = "sub_812C990", - ["sub_812CAFC"] = "sub_812CAFC", - ["sub_812CC28"] = "sub_812CC28", - ["sub_812CCE8"] = "sub_812CCE8", - ["sub_812D294"] = "sub_812D294", - ["sub_812D3AC"] = "sub_812D3AC", - ["sub_812D4B4"] = "sub_812D4B4", - ["sub_812D724"] = "sub_812D724", - ["sub_812DEAC"] = "sub_812DEAC", - ["sub_812E4F0"] = "sub_812E4F0", - ["sub_812E7A0"] = "sub_812E7A0", - ["sub_812EA4C"] = "sub_812EA4C", - ["sub_812EC78"] = "sub_812EC78", - ["sub_812ED84"] = "sub_812ED84", - ["sub_812EEA4"] = "sub_812EEA4", - ["sub_812F88C"] = "sub_812F88C", - ["sub_812F948"] = "sub_812F948", - ["sub_81300F4"] = "sub_81300F4", - ["sub_813051C"] = "sub_813051C", - ["sub_81307B0"] = "sub_81307B0", - ["sub_8130A2C"] = "sub_8130A2C", - ["sub_8130AEC"] = "sub_8130AEC", - ["sub_8130F5C"] = "sub_8130F5C", - ["sub_8131264"] = "sub_8131264", - ["sub_8131564"] = "sub_8131564", - ["sub_8141C30"] = "sub_8141C30", + AnimAuroraRings = "AuroraBeamRings", + AnimBasicFistOrFoot = "BasicFistOrFoot", + AnimBoneHitProjectile = "BoneHitProjectile", + AnimBonemerangProjectile = "BonemerangProjectile", + AnimBubbleEffect = "BubbleEffect", + AnimConfusionDuck = "ConfusionDuck", + AnimCuttingSlice = "CuttingSlice", + AnimDigDirtMound = "DigDirtMound", + AnimDirtParticleAcrossScreen = "FlyingSandCrescent", + AnimDirtScatter = "DirtScatter", + AnimEmberFlare = "EmberFlare", + AnimFallingFeather = "FallingFeather", + AnimFireCross = "FireCross", + AnimFireRing = "FireRing", + AnimFissureDirtPlumeParticle = "DirtPlumeParticle", + AnimIceBeamParticle = "IceBeamParticle", + AnimIceEffectParticle = "IceEffectParticle", + AnimIcePunchSwirlingParticle = "IcePunchSwirlingParticle", + AnimKnockOffStrike = "KnockOffStrike", + AnimLeechSeed = "LeechSeed", + AnimMissileArc = "MissileArc", + AnimMoveParticleBeyondTarget = "MoveParticleBeyondTarget", + AnimMovePowderParticle = "MovePowderParticle", + AnimMoveTwisterParticle = "MoveTwisterParticle", + AnimMudSportDirt = "MudSportDirt", + AnimOrbitFast = "OrbitFast", + AnimOrbitScatter = "OrbitScatter", + AnimRaiseSprite = "RaiseSprite", + AnimRecycle = "Recycle", + AnimSimplePaletteBlend = "SimplePaletteBlend", + AnimSonicBoomProjectile = "SonicBoomProjectile", + AnimSpinningKickOrPunch = "SpinningKickOrPunch", + AnimStompFoot = "StompFoot", + AnimSwirlingSnowball_Step1 = "SwirlingSnowball", + AnimThrowMistBall = "ThrowMistBall", + AnimTranslateLinearSingleSineWave = "TranslateLinearSingleSineWave", + AnimTranslateStinger = "TranslateStinger", + AnimWaveFromCenterOfTarget = "WaveFromCenterOfTarget", + DoHorizontalLunge = "DoHorizontalLunge", + DoVerticalDip = "DoVerticalDip", + InitAnimShadowBall = "ShadowBall", + InitIceBallAnim = "InitIceBallAnim", + InitIceBallParticle = "InitIceBallParticle", + InitPoisonGasCloudAnim = "InitPoisonGasCloudAnim", + InitSwirlingFogAnim = "InitSwirlingFogAnim", + SlideMonToOffset = "SlideMonToOffset", + SlideMonToOriginalPos = "SlideMonToOriginalPos", + TranslateAnimSpriteToTargetMonLocation = "TranslateAnimSpriteToTargetMonLocation", + sub_80793C4 = "sub_80793C4", + sub_80794A8 = "sub_80794A8", + sub_8079534 = "sub_8079534", + sub_807A63C = "SpinningSparkle", + sub_807A908 = "WeatherBallUp", + sub_807A9BC = "WeatherBallDown", + sub_80A8818 = "SlideMonToOffsetAndBack", + sub_80CA7B0 = "PowerAbsorptionOrb", + sub_80CA800 = "SolarBeamBigOrb", + sub_80CA9A8 = "sub_80CA9A8", + sub_80CAA14 = "HyperBeamOrb", + sub_80CABF8 = "SporeParticle", + sub_80CAD54 = "PetalDanceBigFlower", + sub_80CAE20 = "PetalDanceSmallFlower", + sub_80CAED8 = "RazorLeafParticle", + sub_80CB25C = "ConstrictBinding", + sub_80CB4CC = "MimicOrb", + sub_80CB59C = "IngrainRoot", + sub_80CB620 = "FrenzyPlantRoot", + sub_80CB768 = "IngrainOrb", + sub_80CB94C = "Present", + sub_80CBA28 = "KnockOffItem", + sub_80CBAA4 = "PresentHealParticle", + sub_80CBAE8 = "ItemSteal", + sub_80CBBF0 = "TrickBag", + sub_80CC474 = "FlyingParticle", + sub_80CC6CC = "NeedleArmSpike", + sub_80CC884 = "WhipHit", + sub_80CC9BC = "AirCutterSlice", + sub_80CCD24 = "Protect", + sub_80CCF04 = "MilkBottle", + sub_80CD140 = "GrantingStars", + sub_80CD190 = "SparklingStars", + sub_80CD328 = "SleepLetterZ", + sub_80CD3E0 = "LockOnTarget", + sub_80CD6CC = "LockOnMoveTarget", + sub_80CD774 = "BowMon", + sub_80CDD74 = "SlashSlice", + sub_80CDDDC = "FalseSwipeSlice", + sub_80CDE24 = "FalseSwipePositionedSlice", + sub_80CDF0C = "sub_80CDF0C", + sub_80CDFB0 = "SharpenSphere", + sub_80CE09C = "Conversion", + sub_80CE17C = "Conversion2", + sub_80CE30C = "Moon", + sub_80CE36C = "MoonlightSparkle", + sub_80CE670 = "HornHit", + sub_80CEA04 = "SuperFang", + sub_80CEB0C = "WavyMusicNotes", + sub_80CECE8 = "FlyingMusicNotes", + sub_80CEDF0 = "BellyDrumHand", + sub_80CEE60 = "SlowFlyingMusicNotes", + sub_80CEF9C = "ThoughtBubble", + sub_80CF040 = "MetronomeFinger", + sub_80CF0BC = "FollowMeFinger", + sub_80CF1C8 = "TauntFinger", + sub_80CF610 = "KinesisZapEnergy", + sub_80CF690 = "SwordsDanceBlade", + sub_80CFE9C = "CoinThrow", + sub_80CFF50 = "FallingCoin", + sub_80CFFD8 = "BulletSeed", + sub_80D0118 = "RazorWindTornado", + sub_80D0178 = "ViceGripPincer", + sub_80D0228 = "GuillotinePincer", + sub_80D0930 = "BreathPuff", + sub_80D09C0 = "AngerMark", + sub_80D0E30 = "Pencil", + sub_80D0FD8 = "BlendThinRing", + sub_80D10B8 = "HyperVoiceRing", + sub_80D1318 = "UproarRing", + sub_80D1368 = "SoftBoiledEgg", + sub_80D1C08 = "HealBellMusicNote", + sub_80D1C80 = "MagentaHeart", + sub_80D1F58 = "RedHeartProjectile", + sub_80D1FDC = "ParticleBurst", + sub_80D2064 = "RedHeartRising", + sub_80D28AC = "SpitUpOrb", + sub_80D2920 = "EyeSparkle", + sub_80D2938 = "Angel", + sub_80D2A38 = "PinkHeart", + sub_80D2ABC = "Devil", + sub_80D2BE8 = "FurySwipes", + sub_80D2C38 = "MovementWaves", + sub_80D2D68 = "JaggedMusicNote", + sub_80D2E68 = "PerishSongMusicNote2", + sub_80D2EC8 = "PerishSongMusicNote", + sub_80D3014 = "GuardRing", + sub_80D31C8 = "WaterBubbleProjectile", + sub_80D3554 = "ToTargetInSinWave", + sub_80D3698 = "HydroCannonCharge", + sub_80D3728 = "HydroCannonBeam", + sub_80D37FC = "sub_80D37FC", + sub_80D3838 = "SmallBubblePair", + sub_80D4044 = "SmallDriftingBubbles", + sub_80D4BF0 = "WaterPulseBubble", + sub_80D4CA4 = "WaterPulseRing", + sub_80D4ED8 = "FireSpiralInward", + sub_80D4F18 = "FireSpread", + sub_80D4F5C = "FirePlume", + sub_80D4FCC = "LargeFlame", + sub_80D517C = "Sunlight", + sub_80D5210 = "sub_80D5210", + sub_80D53B4 = "FireSpiralOutward", + sub_80D5A20 = "EruptionFallingRock", + sub_80D5B0C = "WillOWispOrb", + sub_80D5CC0 = "WillOWispFire", + sub_80D61C8 = "Lightning", + sub_80D6328 = "SparkElectricity", + sub_80D648C = "ZapCannonSpark", + sub_80D65DC = "ThunderboltOrb", + sub_80D6658 = "SparkElectricityFlashing", + sub_80D679C = "Electricity", + sub_80D6A6C = "ThunderWave", + sub_80D6D18 = "GrowingChargeOrb", + sub_80D6D70 = "ElectricPuff", + sub_80D6DD8 = "VoltTackleOrbSlide", + sub_80D727C = "GrowingShockWaveOrb", + sub_80D902C = "SlideHandOrFootToTarget", + sub_80D9078 = "JumpKick", + sub_80D90F4 = "sub_80D90F4", + sub_80D92D0 = "CrossChopHand", + sub_80D9378 = "SlidingKick", + sub_80D9540 = "DizzyPunchDuck", + sub_80D95D0 = "BrickBreakWall", + sub_80D96B8 = "BrickBreakWallShard", + sub_80D97CC = "SuperpowerOrb", + sub_80D98D8 = "SuperpowerRock", + sub_80D9A38 = "SuperpowerFireball", + sub_80D9B48 = "ArmThrustHit", + sub_80D9BD4 = "RevengeScratch", + sub_80D9C40 = "FocusPunchFist", + sub_80D9D70 = "SludgeProjectile", + sub_80D9DF0 = "AcidPoisonBubble", + sub_80D9E94 = "SludgeBombHitParticle", + sub_80D9F14 = "AcidPoisonDroplet", + sub_80DA034 = "sub_80DA034", + sub_80DA16C = "GustToTarget", + sub_80DA208 = "AirWaveCrescent", + sub_80DA300 = "FlyBallUp", + sub_80DA38C = "FlyBallAttack", + sub_80DB000 = "WhirlwindLine", + sub_80DB194 = "BounceBallShrink", + sub_80DB1F4 = "BounceBallLand", + sub_80DB288 = "DiveBall", + sub_80DB374 = "DiveWaterSplash", + sub_80DB458 = "SprayWaterDroplet", + sub_80DB5E4 = "SkyAttackBird", + sub_80DB74C = "DefensiveWall", + sub_80DBA4C = "WallSparkle", + sub_80DBAF4 = "BentSpoon", + sub_80DBB70 = "QuestionMark", + sub_80DC068 = "RedX", + sub_80DC700 = "PsychoBoost", + sub_80DC824 = "MegahornHorn", + sub_80DC8F4 = "LeechLifeNeedle", + sub_80DC9A0 = "TranslateWebThread", + sub_80DCA70 = "StringWrap", + sub_80DCB38 = "SpiderWeb", + sub_80DCE40 = "TailGlowOrb", + sub_80DCE9C = "FallingRock", + sub_80DCF60 = "RockFragment", + sub_80DCFE4 = "ParticleInVortex", + sub_80DD8E8 = "RockTomb", + sub_80DD978 = "RockBlastRock", + sub_80DD9A4 = "RockScatter", + sub_80DDB6C = "ConfuseRayBallBounce", + sub_80DDD58 = "ConfuseRayBallSpiral", + sub_80DE0FC = "Lick", + sub_80DEF3C = "CurseNail", + sub_80DF0B8 = "GhostStatusSprite", + sub_80DF5A0 = "OutrageFlame", + sub_80DF6F0 = "DragonRageFirePlume", + sub_80DF760 = "DragonFireToTarget", + sub_80DF78C = "DragonDanceOrb", + sub_80DFB28 = "OverheatFlame", + sub_80DFF1C = "Bite", + sub_80DFFD0 = "TearDrop", + sub_80E0A10 = "ClawSlash", + sub_80E1E2C = "ComplexPaletteBlend", + sub_80E24B8 = "ShakeMonOrBattleTerrain", + sub_80E27A0 = "sub_80E27A0", + sub_80E27E8 = "sub_80E27E8", + sub_80E2838 = "sub_80E2838", + sub_80E2870 = "sub_80E2870", + sub_80E2908 = "sub_80E2908", + sub_80E2978 = "CrossImpact", + sub_80E29C0 = "sub_80E29C0", + sub_812C144 = "BlackSmoke", + sub_812C220 = "WhiteHalo", + sub_812C2BC = "TealAlert", + sub_812C358 = "MeanLookEye", + sub_812C720 = "Spikes", + sub_812C80C = "sub_812C80C", + sub_812C848 = "LetterZ", + sub_812C908 = "Fang", + sub_812C990 = "Spotlight", + sub_812CAFC = "ClappingHand", + sub_812CC28 = "ClappingHand2", + sub_812CCE8 = "RapidSpin", + sub_812D294 = "TriAttackTriangle", + sub_812D3AC = "BatonPassPokeball", + sub_812D4B4 = "WishStar", + sub_812D724 = "SwallowBlueOrb", + sub_812DEAC = "GreenStar", + sub_812E4F0 = "WeakFrustrationAngerMark", + sub_812E7A0 = "SweetScentPetal", + sub_812EA4C = "PainSplitProjectile", + sub_812EC78 = "FlatterConfetti", + sub_812ED84 = "FlatterSpotlight", + sub_812EEA4 = "ReversalOrb", + sub_812F88C = "YawnCloud", + sub_812F948 = "SmokeBallEscapeCloud", + sub_81300F4 = "sub_81300F4", + sub_813051C = "AssistPawprint", + sub_81307B0 = "SmellingSaltsHand", + sub_8130A2C = "SmellingSaltExclamation", + sub_8130AEC = "HelpingHandClap", + sub_8130F5C = "ForesightMagnifyingGlass", + sub_8131264 = "MeteorMashStar", + sub_8131564 = "BlockX", + sub_8141C30 = "SpriteCB_SafariBaitOrRock_Init", }, tasks = { - ["AnimTask_BlendInterfaceColor"] = "BlendInterfaceColor", - ["AnimTask_BlendMonInAndOut"] = "BlendMonInAndOut", - ["AnimTask_BlendSpriteColor"] = "BlendSpriteColor", - ["AnimTask_CountIceBallThrows"] = "CountIceBallThrows", - ["AnimTask_CreateSurfWave"] = "CreateSurfWave", - ["AnimTask_GetBattleEnvironment"] = "GetBattleEnvironment", - ["AnimTask_GetReturnPowerLevel"] = "GetReturnPowerLevel", - ["AnimTask_GetWeather"] = "GetWeather", - ["AnimTask_Hail1"] = "Hail1", - ["AnimTask_Haze1"] = "Haze1", - ["AnimTask_LoadMistTiles"] = "LoadMistTiles", - ["AnimTask_Minimize"] = "Minimize", - ["AnimTask_RolePlaySilhouette"] = "RolePlaySilhouette", - ["AnimTask_RotateMonPalette1"] = "RotateMonPalette1", - ["AnimTask_ScaleMonAndRestore"] = "ScaleMonAndRestore", - ["AnimTask_ShakeAndSinkMon"] = "ShakeAndSinkMon", - ["AnimTask_ShakeMon"] = "ShakeMon", - ["AnimTask_ShakeMon2"] = "ShakeMon2", - ["AnimTask_ShakeMonInPlace"] = "ShakeMonInPlace", - ["AnimTask_SlackOffSquish"] = "SlackOffSquish", - ["AnimTask_SnatchOpposingMonMove"] = "SnatchOpposingMonMove", - ["AnimTask_SnatchPartnerMove"] = "SnatchPartnerMove", - ["AnimTask_Splash"] = "Splash", - ["AnimTask_SwayMon"] = "SwayMon", - ["AnimTask_TeeterDanceMovement"] = "TeeterDanceMovement", - ["AnimTask_TranslateMonElliptical"] = "TranslateMonElliptical", - ["AnimTask_TranslateMonEllipticalRespectSide"] = "TranslateMonEllipticalRespectSide", - ["AnimTask_WindUpLunge"] = "WindUpLunge", - ["CreateAnimRaindrops"] = "CreateAnimRaindrops", - ["UpdateFuryCutterAnimCount"] = "UpdateFuryCutterAnimCount", - ["UpdateFuryCutterAnimDirection"] = "UpdateFuryCutterAnimDirection", - ["c3_80DFBE4"] = "c3_80DFBE4", - ["do_boulder_dust"] = "do_boulder_dust", - ["sub_8079670"] = "sub_8079670", - ["sub_80798AC"] = "sub_80798AC", - ["sub_8079CEC"] = "sub_8079CEC", - ["sub_807A69C"] = "sub_807A69C", - ["sub_807B920"] = "sub_807B920", - ["sub_807BB88"] = "sub_807BB88", - ["sub_80A8A80"] = "sub_80A8A80", - ["sub_80A8E04"] = "sub_80A8E04", - ["sub_80A8EFC"] = "sub_80A8EFC", - ["sub_80A9058"] = "sub_80A9058", - ["sub_80CA928"] = "sub_80CA928", - ["sub_80CACEC"] = "sub_80CACEC", - ["sub_80CB340"] = "sub_80CB340", - ["sub_80CBDF4"] = "sub_80CBDF4", - ["sub_80CC5F8"] = "sub_80CC5F8", - ["sub_80CDAC8"] = "sub_80CDAC8", - ["sub_80CE108"] = "sub_80CE108", - ["sub_80CE210"] = "sub_80CE210", - ["sub_80CE3EC"] = "sub_80CE3EC", - ["sub_80CE7E0"] = "sub_80CE7E0", - ["sub_80CEA20"] = "sub_80CEA20", - ["sub_80CEAD8"] = "sub_80CEAD8", - ["sub_80CF4D8"] = "sub_80CF4D8", - ["sub_80CFB04"] = "sub_80CFB04", - ["sub_80D03C4"] = "sub_80D03C4", - ["sub_80D08C8"] = "sub_80D08C8", - ["sub_80D0A4C"] = "sub_80D0A4C", - ["sub_80D0AB8"] = "sub_80D0AB8", - ["sub_80D0C88"] = "sub_80D0C88", - ["sub_80D15A4"] = "sub_80D15A4", - ["sub_80D1638"] = "sub_80D1638", - ["sub_80D17C4"] = "sub_80D17C4", - ["sub_80D18D4"] = "sub_80D18D4", - ["sub_80D1ADC"] = "sub_80D1ADC", - ["sub_80D1B80"] = "sub_80D1B80", - ["sub_80D1CD0"] = "sub_80D1CD0", - ["sub_80D1E38"] = "sub_80D1E38", - ["sub_80D1EC8"] = "sub_80D1EC8", - ["sub_80D2100"] = "sub_80D2100", - ["sub_80D23B4"] = "sub_80D23B4", - ["sub_80D2CF8"] = "sub_80D2CF8", - ["sub_80D3630"] = "sub_80D3630", - ["sub_80D40F4"] = "sub_80D40F4", - ["sub_80D45D8"] = "sub_80D45D8", - ["sub_80D48F4"] = "sub_80D48F4", - ["sub_80D5470"] = "sub_80D5470", - ["sub_80D5DDC"] = "sub_80D5DDC", - ["sub_80D60B4"] = "sub_80D60B4", - ["sub_80D681C"] = "sub_80D681C", - ["sub_80D6B3C"] = "sub_80D6B3C", - ["sub_80D6E9C"] = "sub_80D6E9C", - ["sub_80D700C"] = "sub_80D700C", - ["sub_80D72DC"] = "sub_80D72DC", - ["sub_80D759C"] = "sub_80D759C", - ["sub_80D9C80"] = "sub_80D9C80", - ["sub_80DA09C"] = "sub_80DA09C", - ["sub_80DB0E8"] = "sub_80DB0E8", - ["sub_80DBC94"] = "sub_80DBC94", - ["sub_80DBCFC"] = "sub_80DBCFC", - ["sub_80DBE00"] = "sub_80DBE00", - ["sub_80DC0B0"] = "sub_80DC0B0", - ["sub_80DC2D4"] = "sub_80DC2D4", - ["sub_80DC4F4"] = "sub_80DC4F4", - ["sub_80DD4D4"] = "sub_80DD4D4", - ["sub_80DDA4C"] = "sub_80DDA4C", - ["sub_80DDA8C"] = "sub_80DDA8C", - ["sub_80DDAF0"] = "sub_80DDAF0", - ["sub_80DDDF0"] = "sub_80DDDF0", - ["sub_80DE1B0"] = "sub_80DE1B0", - ["sub_80DE3AC"] = "sub_80DE3AC", - ["sub_80DE918"] = "sub_80DE918", - ["sub_80DECB0"] = "sub_80DECB0", - ["sub_80DF1A4"] = "sub_80DF1A4", - ["sub_80DF924"] = "sub_80DF924", - ["sub_80DFC24"] = "sub_80DFC24", - ["sub_80DFD24"] = "sub_80DFD24", - ["sub_80DFDC0"] = "sub_80DFDC0", - ["sub_80E00EC"] = "sub_80E00EC", - ["sub_80E03BC"] = "sub_80E03BC", - ["sub_80E0918"] = "sub_80E0918", - ["sub_80E09C4"] = "sub_80E09C4", - ["sub_80E0A4C"] = "sub_80E0A4C", - ["sub_80E0E24"] = "sub_80E0E24", - ["sub_80E0EE8"] = "sub_80E0EE8", - ["sub_80E1244"] = "sub_80E1244", - ["sub_80E149C"] = "sub_80E149C", - ["sub_80E1864"] = "sub_80E1864", - ["sub_80E1B88"] = "sub_80E1B88", - ["sub_80E1BB0"] = "sub_80E1BB0", - ["sub_80E1F8C"] = "sub_80E1F8C", - ["sub_80E2094"] = "sub_80E2094", - ["sub_80E21A8"] = "sub_80E21A8", - ["sub_80E2324"] = "sub_80E2324", - ["sub_80E26BC"] = "sub_80E26BC", - ["sub_80E2A38"] = "sub_80E2A38", - ["sub_80E2A7C"] = "sub_80E2A7C", - ["sub_80E2B74"] = "sub_80E2B74", - ["sub_80E2D78"] = "sub_80E2D78", - ["sub_80E2DD8"] = "sub_80E2DD8", - ["sub_80E2F2C"] = "sub_80E2F2C", - ["sub_80E388C"] = "sub_80E388C", - ["sub_80E3A08"] = "sub_80E3A08", - ["sub_80E3A58"] = "sub_80E3A58", - ["sub_80E3B4C"] = "sub_80E3B4C", - ["sub_80E3B78"] = "sub_80E3B78", - ["sub_80E3BA4"] = "sub_80E3BA4", - ["sub_80E3BDC"] = "sub_80E3BDC", - ["sub_80E4028"] = "sub_80E4028", - ["sub_80E40D0"] = "sub_80E40D0", - ["sub_80E4178"] = "sub_80E4178", - ["sub_80E4200"] = "sub_80E4200", - ["sub_80E4234"] = "sub_80E4234", - ["sub_80E4264"] = "sub_80E4264", - ["sub_80E42B0"] = "sub_80E42B0", - ["sub_80E42D0"] = "sub_80E42D0", - ["sub_80E4300"] = "sub_80E4300", - ["sub_812AF30"] = "sub_812AF30", - ["sub_812B058"] = "sub_812B058", - ["sub_812B18C"] = "sub_812B18C", - ["sub_812B2B8"] = "sub_812B2B8", - ["sub_812B30C"] = "sub_812B30C", - ["sub_812B340"] = "sub_812B340", - ["sub_812B374"] = "sub_812B374", - ["sub_812C1D0"] = "sub_812C1D0", - ["sub_812C560"] = "sub_812C560", - ["sub_812C624"] = "sub_812C624", - ["sub_812C924"] = "sub_812C924", - ["sub_812C960"] = "sub_812C960", - ["sub_812CC44"] = "sub_812CC44", - ["sub_812CCA8"] = "sub_812CCA8", - ["sub_812CDC8"] = "sub_812CDC8", - ["sub_812D008"] = "sub_812D008", - ["sub_812D350"] = "sub_812D350", - ["sub_812D674"] = "sub_812D674", - ["sub_812D6CC"] = "sub_812D6CC", - ["sub_812D790"] = "sub_812D790", - ["sub_812D7E8"] = "sub_812D7E8", - ["sub_812DB58"] = "sub_812DB58", - ["sub_812DB84"] = "sub_812DB84", - ["sub_812E14C"] = "sub_812E14C", - ["sub_812E498"] = "sub_812E498", - ["sub_812E568"] = "sub_812E568", - ["sub_812E860"] = "sub_812E860", - ["sub_812EB10"] = "sub_812EB10", - ["sub_812F314"] = "sub_812F314", - ["sub_812F724"] = "sub_812F724", - ["sub_812FC68"] = "sub_812FC68", - ["sub_812FD7C"] = "sub_812FD7C", - ["sub_812FFE4"] = "sub_812FFE4", - ["sub_81300A4"] = "sub_81300A4", - ["sub_81301EC"] = "sub_81301EC", - ["sub_8130554"] = "sub_8130554", - ["sub_8130918"] = "sub_8130918", - ["sub_8130D20"] = "sub_8130D20", - ["sub_81312E4"] = "sub_81312E4", - ["sub_81316F8"] = "sub_81316F8", - ["sub_813F4EC"] = "sub_813F4EC", - ["sub_813F5E8"] = "sub_813F5E8", - ["sub_813F6A0"] = "sub_813F6A0", - ["sub_813F798"] = "sub_813F798", - ["sub_813F844"] = "sub_813F844", - ["sub_813F990"] = "sub_813F990", - ["sub_813F9B8"] = "sub_813F9B8", - ["sub_813F9E0"] = "sub_813F9E0", - ["sub_813FA94"] = "sub_813FA94", - ["sub_813FBB8"] = "sub_813FBB8", - ["sub_814151C"] = "sub_814151C", - ["sub_81416C4"] = "sub_81416C4", - ["sub_81417D8"] = "sub_81417D8", - ["sub_8141808"] = "sub_8141808", - ["sub_8141BD4"] = "sub_8141BD4", - ["sub_8141C08"] = "sub_8141C08", - ["sub_8141D7C"] = "sub_8141D7C", - ["sub_8141DAC"] = "sub_8141DAC", - ["sub_8141E10"] = "sub_8141E10", + AnimTask_BlendInterfaceColor = "BlendBackground", + AnimTask_BlendMonInAndOut = "BlendMonInAndOut", + AnimTask_BlendSpriteColor = "BlendParticle", + AnimTask_CountIceBallThrows = "GetRolloutCounter", + AnimTask_CreateSurfWave = "CreateSurfWave", + AnimTask_GetBattleEnvironment = "GetBattleTerrain", + AnimTask_GetReturnPowerLevel = "GetReturnPowerLevel", + AnimTask_GetWeather = "GetWeather", + AnimTask_Hail1 = "Hail", + AnimTask_Haze1 = "HazeScrollingFog", + AnimTask_LoadMistTiles = "MistBallFog", + AnimTask_Minimize = "Minimize", + AnimTask_RolePlaySilhouette = "RolePlaySilhouette", + AnimTask_RotateMonPalette1 = "RotateAuroraRingColors", + AnimTask_ScaleMonAndRestore = "ScaleMonAndRestore", + AnimTask_ShakeAndSinkMon = "ShakeAndSinkMon", + AnimTask_ShakeMon = "ShakeMon", + AnimTask_ShakeMon2 = "ShakeMon2", + AnimTask_ShakeMonInPlace = "ShakeMonInPlace", + AnimTask_SlackOffSquish = "SlackOffSquish", + AnimTask_SnatchOpposingMonMove = "SnatchOpposingMonMove", + AnimTask_SnatchPartnerMove = "SnatchPartnerMove", + AnimTask_Splash = "Splash", + AnimTask_SwayMon = "SwayMon", + AnimTask_TeeterDanceMovement = "TeeterDanceMovement", + AnimTask_TranslateMonElliptical = "TranslateMonElliptical", + AnimTask_TranslateMonEllipticalRespectSide = "TranslateMonEllipticalRespectSide", + AnimTask_WindUpLunge = "WindUpLunge", + CreateAnimRaindrops = "CreateRaindrops", + UpdateFuryCutterAnimCount = "GetFuryCutterHitCount", + UpdateFuryCutterAnimDirection = "IsFuryCutterHitRight", + c3_80DFBE4 = "IsMonInvisible", + do_boulder_dust = "LoadSandstormBackground", + sub_8079670 = "AlphaFadeIn", + sub_80798AC = "BlendPalInAndOutByTag", + sub_8079CEC = "GetFrustrationPowerLevel", + sub_807A69C = "AttackerPunchWithTrace", + sub_807B920 = "FrozenIceCube", + sub_807BB88 = "StatsChange", + sub_80A8A80 = "SlideOffScreen", + sub_80A8E04 = "RotateMonSpriteToSide", + sub_80A8EFC = "sub_80A8EFC", + sub_80A9058 = "ShakeTargetBasedOnMovePowerOrDmg", + sub_80CA928 = "CreateSmallSolarBeamOrbs", + sub_80CACEC = "SporeDoubleBattle", + sub_80CB340 = "ShrinkTargetCopy", + sub_80CBDF4 = "LeafBlade", + sub_80CC5F8 = "CycleMagicalLeafPal", + sub_80CDAC8 = "SkullBashPosition", + sub_80CE108 = "ConversionAlphaBlend", + sub_80CE210 = "Conversion2AlphaBlend", + sub_80CE3EC = "MoonlightEndFade", + sub_80CE7E0 = "DoubleTeam", + sub_80CEA20 = "MusicNotesRainbowBlend", + sub_80CEAD8 = "MusicNotesClearRainbowBlend", + sub_80CF4D8 = "Withdraw", + sub_80CFB04 = "AirCutterProjectile", + sub_80D03C4 = "GrowAndGrayscale", + sub_80D08C8 = "GrowAndShrink", + sub_80D0A4C = "ThrashMoveMonHorizontal", + sub_80D0AB8 = "ThrashMoveMonVertical", + sub_80D0C88 = "SketchDrawMon", + sub_80D15A4 = "AttackerStretchAndDisappear", + sub_80D1638 = "ExtremeSpeedImpact", + sub_80D17C4 = "ExtremeSpeedMonReappear", + sub_80D18D4 = "SpeedDust", + sub_80D1ADC = "LoadMusicNotesPals", + sub_80D1B80 = "FreeMusicNotesPals", + sub_80D1CD0 = "FakeOut", + sub_80D1E38 = "StretchTargetUp", + sub_80D1EC8 = "StretchAttackerUp", + sub_80D2100 = "HeartsBackground", + sub_80D23B4 = "ScaryFace", + sub_80D2CF8 = "UproarDistortion", + sub_80D3630 = "StartSinAnimTimer", + sub_80D40F4 = "WaterSpoutLaunch", + sub_80D45D8 = "WaterSpoutRain", + sub_80D48F4 = "WaterSport", + sub_80D5470 = "EruptionLaunchRocks", + sub_80D5DDC = "MoveHeatWaveTargets", + sub_80D60B4 = "ShakeTargetInPattern", + sub_80D681C = "ElectricBolt", + sub_80D6B3C = "ElectricChargingParticles", + sub_80D6E9C = "VoltTackleAttackerReappear", + sub_80D700C = "VoltTackleBolt", + sub_80D72DC = "ShockWaveProgressingBolt", + sub_80D759C = "ShockWaveLightning", + sub_80D9C80 = "MoveSkyUppercutBg", + sub_80DA09C = "sub_80DA09C", + sub_80DB0E8 = "DrillPeckHitSplats", + sub_80DBC94 = "MeditateStretchAttacker", + sub_80DBCFC = "Teleport", + sub_80DBE00 = "ImprisonOrbs", + sub_80DC0B0 = "SkillSwap", + sub_80DC2D4 = "ExtrasensoryDistortion", + sub_80DC4F4 = "TransparentCloneGrowAndShrink", + sub_80DD4D4 = "Rollout", + sub_80DDA4C = "GetSeismicTossDamageLevel", + sub_80DDA8C = "MoveSeismicTossBg", + sub_80DDAF0 = "SeismicTossBgAccelerateDownAtEnd", + sub_80DDDF0 = "NightShadeClone", + sub_80DE1B0 = "NightmareClone", + sub_80DE3AC = "SpiteTargetShadow", + sub_80DE918 = "DestinyBondWhiteShadow", + sub_80DECB0 = "CurseStretchingBlackBg", + sub_80DF1A4 = "GrudgeFlames", + sub_80DF924 = "DragonDanceWaver", + sub_80DFC24 = "AttackerFadeToInvisible", + sub_80DFD24 = "AttackerFadeFromInvisible", + sub_80DFDC0 = "InitAttackerFadeFromInvisible", + sub_80E00EC = "MoveAttackerMementoShadow", + sub_80E03BC = "MoveTargetMementoShadow", + sub_80E0918 = "InitMementoShadow", + sub_80E09C4 = "MementoHandleBg", + sub_80E0A4C = "MetallicShine", + sub_80E0E24 = "SetGrayscaleOrOriginalPal", + sub_80E0EE8 = "GetIsDoomDesireHitTurn", + sub_80E1244 = "DigDownMovement", + sub_80E149C = "DigUpMovement", + sub_80E1864 = "HorizontalShake", + sub_80E1B88 = "IsPowerOver99", + sub_80E1BB0 = "PositionFissureBgOnBattler", + sub_80E1F8C = "sub_80E1F8C", + sub_80E2094 = "BlendColorCycleByTag", + sub_80E21A8 = "FlashAnimTagWithColor", + sub_80E2324 = "InvertScreenColor", + sub_80E26BC = "ShakeBattleTerrain", + sub_80E2A38 = "BlendBattleAnimPal", + sub_80E2A7C = "BlendBattleAnimPalExclude", + sub_80E2B74 = "SetCamouflageBlend", + sub_80E2D78 = "HardwarePaletteFade", + sub_80E2DD8 = "sub_80E2DD8", + sub_80E2F2C = "DrawFallingWhiteLinesOnAttacker", + sub_80E388C = "Flash", + sub_80E3A08 = "BlendNonAttackerPalettes", + sub_80E3A58 = "StartSlidingBg", + sub_80E3B4C = "GetAttackerSide", + sub_80E3B78 = "GetTargetSide", + sub_80E3BA4 = "GetTargetIsAttackerPartner", + sub_80E3BDC = "SetAllNonAttackersInvisiblity", + sub_80E4028 = "CopyPalUnfadedToBackup", + sub_80E40D0 = "CopyPalUnfadedFromBackup", + sub_80E4178 = "CopyPalFadedToUnfaded", + sub_80E4200 = "IsContest", + sub_80E4234 = "SetAnimAttackerAndTargetForEffectTgt", + sub_80E4264 = "IsTargetSameSide", + sub_80E42B0 = "SetAnimTargetToBattlerTarget", + sub_80E42D0 = "SetAnimAttackerAndTargetForEffectAtk", + sub_80E4300 = "SetAttackerInvisibleWaitForSignal", + sub_812AF30 = "SoundTask_FireBlast", + sub_812B058 = "SoundTask_LoopSEAdjustPanning", + sub_812B18C = "sub_812B18C", + sub_812B2B8 = "sub_812B2B8", + sub_812B30C = "SoundTask_PlaySE1WithPanning", + sub_812B340 = "SoundTask_PlaySE2WithPanning", + sub_812B374 = "SoundTask_AdjustPanningVar", + sub_812C1D0 = "SmokescreenImpact", + sub_812C560 = "SetPsychicBackground", + sub_812C624 = "FadeScreenToWhite", + sub_812C924 = "IsTargetPlayerSide", + sub_812C960 = "IsHealingMove", + sub_812CC44 = "CreateSpotlight", + sub_812CCA8 = "RemoveSpotlight", + sub_812CDC8 = "RapinSpinMonElevation", + sub_812D008 = "TormentAttacker", + sub_812D350 = "DefenseCurlDeformMon", + sub_812D674 = "StockpileDeformMon", + sub_812D6CC = "SpitUpDeformMon", + sub_812D790 = "SwallowDeformMon", + sub_812D7E8 = "TransformMon", + sub_812DB58 = "CastformGfxChange", + sub_812DB84 = "MorningSunLightBeam", + sub_812E14C = "DoomDesireLightBeam", + sub_812E498 = "StrongFrustrationGrowAndShrink", + sub_812E568 = "RockMonBackAndForth", + sub_812E860 = "FlailMovement", + sub_812EB10 = "PainSplitMovement", + sub_812F314 = "AcidArmor", + sub_812F724 = "DeepInhale", + sub_812FC68 = "SlideMonForFocusBand", + sub_812FD7C = "SquishAndSweatDroplets", + sub_812FFE4 = "FacadeColorBlend", + sub_81300A4 = "StatusClearedEffect", + sub_81301EC = "GlareEyeDots", + sub_8130554 = "BarrageBall", + sub_8130918 = "SmellingSaltsSquish", + sub_8130D20 = "HelpingHandAttackerMovement", + sub_81312E4 = "MonToSubstitute", + sub_81316F8 = "OdorSleuthMovement", + sub_813F4EC = "LoadHealthboxPalsForLevelUp", + sub_813F5E8 = "FreeHealthboxPalsForLevelUp", + sub_813F6A0 = "FlashHealthboxOnLevelUp", + sub_813F798 = "SwitchOutShrinkMon", + sub_813F844 = "SwitchOutBallEffect", + sub_813F990 = "LoadBallGfx", + sub_813F9B8 = "FreeBallGfx", + sub_813F9E0 = "IsBallBlockedByTrainer", + sub_813FA94 = "ThrowBall", + sub_813FBB8 = "ThrowBall_StandingTrainer", + sub_814151C = "SwapMonSpriteToFromSubstitute", + sub_81416C4 = "SubstituteFadeToInvisible", + sub_81417D8 = "IsAttackerBehindSubstitute", + sub_8141808 = "SetTargetToEffectBattler", + sub_8141BD4 = "LoadBaitGfx", + sub_8141C08 = "FreeBaitGfx", + sub_8141D7C = "SafariOrGhost_DecideAnimSides", + sub_8141DAC = "GetTrappedMoveAnimId", + sub_8141E10 = "GetBattlersFromArg", + }, + rsOnly = { + callbacks = { + }, + tasks = { + }, + templates = { + }, }, tagNames = { [0] = "BONE", @@ -1104,4 +1112,49 @@ return { [287] = "WHIP_HIT", [288] = "BLUE_RING_2", }, + statusNames = { + [0] = "STATUS_PSN", + [1] = "STATUS_CONFUSION", + [2] = "STATUS_BRN", + [3] = "STATUS_INFATUATION", + [4] = "STATUS_SLP", + [5] = "STATUS_PRZ", + [6] = "STATUS_FRZ", + [7] = "STATUS_CURSED", + [8] = "STATUS_NIGHTMARE", + }, + generalNames = { + [0] = "CASTFORM_CHANGE", + [1] = "STATS_CHANGE", + [2] = "SUBSTITUTE_FADE", + [3] = "SUBSTITUTE_APPEAR", + [4] = "POKEBLOCK_THROW", + [5] = "ITEM_KNOCKOFF", + [6] = "TURN_TRAP", + [7] = "HELD_ITEM_EFFECT", + [8] = "SMOKEBALL_ESCAPE", + [9] = "FOCUS_BAND", + [10] = "RAIN_CONTINUES", + [11] = "SUN_CONTINUES", + [12] = "SANDSTORM_CONTINUES", + [13] = "HAIL_CONTINUES", + [14] = "LEECH_SEED_DRAIN", + [15] = "MON_HIT", + [16] = "ITEM_STEAL", + [17] = "SNATCH_MOVE", + [18] = "FUTURE_SIGHT_HIT", + [19] = "DOOM_DESIRE_HIT", + [20] = "FOCUS_PUNCH_SETUP", + [21] = "INGRAIN_HEAL", + [22] = "WISH_HEAL", + }, + specialNames = { + [0] = "LVL_UP", + [1] = "SWITCH_OUT_PLAYER_MON", + [2] = "SWITCH_OUT_OPPONENT_MON", + [3] = "BALL_THROW", + [4] = "BALL_THROW_WITH_TRAINER", + [5] = "SUBSTITUTE_TO_MON", + [6] = "MON_TO_SUBSTITUTE", + }, } diff --git a/src/import/gba/rs/extract_bag.lua b/src/import/gba/rs/extract_bag.lua index f22e1a5b..620d1e6f 100644 --- a/src/import/gba/rs/extract_bag.lua +++ b/src/import/gba/rs/extract_bag.lua @@ -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)) diff --git a/src/import/gba/rs/extract_battle_anims.lua b/src/import/gba/rs/extract_battle_anims.lua index 5334d92d..52054f0e 100644 --- a/src/import/gba/rs/extract_battle_anims.lua +++ b/src/import/gba/rs/extract_battle_anims.lua @@ -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") diff --git a/src/mods/ManagerState.lua b/src/mods/ManagerState.lua index 7df16abe..0bdb833b 100644 --- a/src/mods/ManagerState.lua +++ b/src/mods/ManagerState.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 diff --git a/src/ui/game3/battle_chrome.lua b/src/ui/game3/battle_chrome.lua index 6bfd854b..c43ed6a8 100644 --- a/src/ui/game3/battle_chrome.lua +++ b/src/ui/game3/battle_chrome.lua @@ -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 diff --git a/src/ui/game3/rs/bag_menu.lua b/src/ui/game3/rs/bag_menu.lua index 7222097d..39932c83 100644 --- a/src/ui/game3/rs/bag_menu.lua +++ b/src/ui/game3/rs/bag_menu.lua @@ -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 diff --git a/src/ui/gen2/BattleState.lua b/src/ui/gen2/BattleState.lua index d9235adf..ebdbc223 100644 --- a/src/ui/gen2/BattleState.lua +++ b/src/ui/gen2/BattleState.lua @@ -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 diff --git a/tests/drivers/crystal_sfx_prewarm_2732.lua b/tests/drivers/crystal_sfx_prewarm_2732.lua new file mode 100644 index 00000000..17e9e4db --- /dev/null +++ b/tests/drivers/crystal_sfx_prewarm_2732.lua @@ -0,0 +1,153 @@ +-- ../pokecrystal/engine/overworld/scripting.asm:467 +-- ../pokecrystal/engine/battle_anims/anim_commands.asm:1191 +-- tools/run_driver.sh crystal tests/drivers/crystal_sfx_prewarm_2732.lua +local U = require("tests.drivers.util") +local Sound = require("src.core.Sound") +local ChipAudio = require("src.core.ChipAudio") +local Mon = require("src.battle.gen2.Mon") + +local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") + or ".bazinga/BSA/10-07-26-00-userreported/shots/crystal_sfx_prewarm_2732" + +return function(game) + local fails = 0 + local function say(line) print("[2732] " .. line) end + local function ok(cond, line) + if not cond then fails = fails + 1 end + say((cond and "PASS " or "FAIL ") .. line) + return cond + end + local function finish() + say("stats " .. tostring(ChipAudio.stats().line)) + say(fails == 0 and "all claims passed" or (fails .. " claims failed")) + love.event.quit(fails == 0 and 0 or 1) + while true do U.wait(60) end + end + + if jit and jit.off then jit.off() end + + local function drained(seconds) + local stop = love.timer.getTime() + seconds + while love.timer.getTime() < stop do + local st = ChipAudio.stats() + if (st.prewarmQueued or 0) + (st.prewarmInFlight or 0) == 0 then + return true + end + U.wait(1) + end + return false + end + + local events = {} + local function watch(fnName) + local real = Sound[fnName] + Sound[fnName] = function(data, name, ...) + local before = ChipAudio.stats().syncRenders or 0 + local src = real(data, name, ...) + local after = ChipAudio.stats().syncRenders or 0 + events[#events + 1] = { kind = fnName, name = name, + sync = after - before, ms = ChipAudio.stats().syncRenderMsMax } + return src + end + end + watch("play") + watch("playStereo") + + U.wait(45) + local world = game.world + if not ok(world and world.map, "the crystal world booted") then finish() end + if not ok(ChipAudio.stats().syncRenders ~= nil, + "ChipAudio.stats() counts main-thread sfx renders") then + finish() + end + ok(drained(20), "session sfx prewarm drained") + + world:warpToMapId("ROUTE_29", 48, 3, "up") + U.wait(30) + local ball + for _, npc in ipairs(world.npcs or {}) do + if npc.def and npc.def.itemball then ball = npc end + end + if not ok(ball ~= nil, "Route 29 has its POTION item ball") then finish() end + world:warpToMapId("ROUTE_29", ball.cellX, ball.cellY + 1, "up") + U.wait(30) + ChipAudio.resetSyncStats() + events = {} + world:interact() + local itemEvent + for _ = 1, 600 do + for _, e in ipairs(events) do + if e.name == "Sfx_Item" then itemEvent = e end + end + if itemEvent then break end + U.wait(1) + end + if ok(itemEvent ~= nil, "the item ball script played Sfx_Item") then + ok(itemEvent.sync == 0, "Sfx_Item started with 0 sync renders (got " + .. itemEvent.sync .. ")") + end + U.wait(60) + U.still(game, SHOT_DIR .. "/2732_01_found_potion_chime.png") + for _ = 1, 40 do + if not world:busy() then break end + U.tap(game, "a") + U.wait(6) + end + + local cyndaquil = Mon.new(game.data, "CYNDAQUIL", 20) + game.save.party = { cyndaquil } + ok(world:startBattle({ wild = Mon.new(game.data, "SENTRET", 12) }), + "a wild battle started") + local screen + for _ = 1, 900 do + local top = game.stack:top() + if top and top.battle then screen = top break end + U.wait(1) + end + if not ok(screen ~= nil, "the battle screen came up") then finish() end + ok(drained(20), "battle sfx prewarm drained") + ChipAudio.resetSyncStats() + events = {} + for _ = 1, 300 do + if screen.phase == "menu" then break end + if screen.anim == nil then U.tap(game, "a") end + U.wait(4) + end + if not ok(screen.phase == "menu", "the battle menu came up") then finish() end + local introSounds = #events + U.tap(game, "a") + U.wait(10) + ok(screen.phase == "moves", "FIGHT opened the move list") + local moveName = cyndaquil.moves[1] and cyndaquil.moves[1].id + U.tap(game, "a") + local moveEvent + for _ = 1, 600 do + for i = introSounds + 1, #events do + local e = events[i] + if e.kind == "playStereo" then moveEvent = moveEvent or e end + end + if moveEvent then break end + U.wait(1) + end + if ok(moveEvent ~= nil, "the turn's first move animation played a sound (" + .. tostring(moveName) .. " chosen)") then + ok(moveEvent.sync == 0, ("%s started with 0 sync renders (got %d)") + :format(moveEvent.name, moveEvent.sync)) + end + U.still(game, SHOT_DIR .. "/2732_02_move_anim_sound.png") + for _ = 1, 200 do + if not (game.stack:top() or {}).battle then break end + if screen.anim == nil then U.tap(game, "a") end + U.wait(4) + end + local stereo, played = 0, 0 + for _, e in ipairs(events) do + played = played + 1 + if e.kind == "playStereo" then stereo = stereo + 1 end + end + local st = ChipAudio.stats() + ok(stereo > 0, "the turn played " .. stereo .. " anim sounds of " .. played) + ok((st.syncRenders or 0) == 0, "0 sync sfx renders across the battle (last " + .. tostring(st.syncRenderLast) .. ")") + finish() +end diff --git a/tests/drivers/em_mr_stone_head_2737.lua b/tests/drivers/em_mr_stone_head_2737.lua new file mode 100644 index 00000000..b810a430 --- /dev/null +++ b/tests/drivers/em_mr_stone_head_2737.lua @@ -0,0 +1,29 @@ +local U = require("tests.drivers.util") +local F = require("tests.drivers.em_fa_util") +local S = F.new("em_mr_stone_head_2737", os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_mr_stone_head_2737") + +return function(game) + if not F.boot(game, S) then return S.finish() end + local Runtime = require("src.core.game3.runtime") + local Objects = require("src.core.game3.objects") + local FieldView = require("src.core.game3.field_view") + local session = Runtime.getSession() + if not S.check(session.version == "emerald", "driver runs Emerald") then return S.finish() end + F.setVar("VAR_DEVON_CORP_3F_STATE", 3) + S.check(F.goTo(game, "EM_RUSTBORO_CITY_DEVON_CORP_3F", 13, 5, "right"), "Devon Corp 3F loads") + U.wait(20) + local stone + for _, lid in ipairs(Objects._order or {}) do + local eo = Objects._byId[lid] + if eo and eo.cellX == 17 and eo.cellY == 5 then stone = eo end + end + if not S.check(stone ~= nil, "Mr. Stone spawned at 17,5") then return S.finish() end + S.note("stone elevation=" .. tostring(stone.elevation) .. " current=" .. tostring(stone.currentElevation)) + S.check(stone.elevation == 4, "Mr. Stone elevation follows chair metatile") + local _, over = FieldView.applyDrawOrder({ + { kind = "npc", eventObject = stone, elevation = stone.elevation, y = stone.py }, + }, {}, {}, 0) + S.check(#over == 1, "Mr. Stone draws over the chair back layer") + S.check(S.still(game, "2737_01_mr_stone_head_over_chair.png"), "Mr. Stone office screenshot") + S.finish() +end diff --git a/tests/drivers/em_poison_whiteout_2736.lua b/tests/drivers/em_poison_whiteout_2736.lua new file mode 100644 index 00000000..9dd392b5 --- /dev/null +++ b/tests/drivers/em_poison_whiteout_2736.lua @@ -0,0 +1,181 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_poison_whiteout_2736" + +local failures = 0 +local function check(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + return ok +end + +local function finish() + print((failures == 0 and "PASS" or "FAIL") .. " em_poison_whiteout_2736 failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) + U.wait(10) +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + if not check(game.boot ~= nil, "boot reached") then return finish() end + game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) + U.wait(60) + + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local MapIds = require("src.core.game3.map_ids") + local Player = require("src.core.game3.player") + local Message = require("src.ui.game3.message") + local Party = require("src.core.game3.party") + local Field = require("src.core.game3.field") + local Space = require("src.core.game3.scripting.space") + local Flags = require("src.core.game3.scripting.flags") + local Warp = require("src.core.game3.warp") + local StepEvents = require("src.core.game3.step_events") + local Sem = require("src.core.game3.field_semantics") + local HealLocations = require("src.core.game3.heal_locations") + local RomText = require("src.core.game3.rom_text") + + local session = Runtime.getSession() + if not check(session ~= nil, "field session exists") then return finish() end + local C = require("src.core.game3.constants").active(session) + local id = require("src.core.game3.profile").forSession(session).id + local isEm = id == "emerald" + local tag = id .. ": " + local whiteKey = isEm and "gText_PlayerWhitedOut" or "UnknownString_81A1141" + local route = MapIds.forConst("MAP_ROUTE103", id) + + for _ = 1, 600 do + if not (Space.vm and Space.vm:isRunning()) and not Message.isOpen() and not Warp.isBusy() then break end + if Message.isOpen() then U.tap(game, "a") end + U.wait(2) + end + + local romWhite = RomText.box(whiteKey, { playerName = "BRENDAN" }) + check(type(romWhite) == "string" and romWhite:find("whited out", 1, true) ~= nil, + tag .. whiteKey .. " resolves from the ROM cache") + + local function setup(mons, money) + Map.load(nil, game, route, { x = 10, y = 10, facing = "down" }) + session.x, session.y, session.facing = 10, 10, "down" + Field.unlock() + U.wait(20) + session.party = {} + for i, row in ipairs(mons) do + Party.giveMon(session, C:require("species", row.species), 5) + local mon = session.party[i] + if row.psn then + mon.hp = 1 + mon.status = "PSN" + end + end + session.money = money + session.vars[Sem.var(session, "poisonSteps")] = 3 + session.poisonSteps = 3 + end + + local function step() + local sx, sy = Player.cellX, Player.cellY + for _, dir in ipairs({ "down", "left", "right", "up" }) do + U.hold(game, dir, 20) + U.wait(10) + if Player.cellX ~= sx or Player.cellY ~= sy then return true end + end + return false + end + + local function pump(name, shotName, expectWhiteout) + local seen = { faint = 0, white = false, rom = false } + local lastFaint + for _ = 1, 1500 do + local p = Message.isOpen() and Message.currentPage and Message.currentPage() or "" + if p:find("fainted", 1, true) and p ~= lastFaint then + seen.faint = seen.faint + 1 + lastFaint = p + end + if p:find("whited out", 1, true) and not seen.white then + seen.white = true + seen.rom = romWhite:find(p, 1, true) ~= nil + Message.skipReveal() + U.wait(4) + if shotName then U.shot(game, DIR .. "/" .. shotName) end + end + local settled = not Message.isOpen() and not Warp.isBusy() and not StepEvents.busy() + and not (Space.vm and Space.vm:isRunning()) + if settled and (not expectWhiteout or Map.current ~= route) then break end + if Message.isOpen() and Message.isTyping() then Message.skipReveal() end + if Message.isOpen() and Message.isWaiting() then U.tap(game, "a") end + U.wait(2) + end + print("INFO " .. name .. " map=" .. tostring(Map.current) .. " money=" .. tostring(session.money)) + return seen + end + + local Fade = require("src.ui.game3.fade") + local function fadedIn() + for _ = 1, 300 do + if not Fade.active and (Fade.t or 0) <= 0 then return true end + U.wait(2) + end + return false + end + + local function healed() + for _, mon in ipairs(session.party) do + if (mon.hp or 0) <= 0 or (mon.hp or 0) ~= (mon.maxHp or mon.hp) or mon.status then return false end + end + return true + end + + local okRun, err = pcall(function() + setup({ { species = "SPECIES_TORCHIC", psn = true }, { species = "SPECIES_MUDKIP" } }, 3000) + check(Map.current == route, tag .. "survive case on " .. tostring(route)) + check(step(), tag .. "survive case took a poisoned step") + local s = pump("survive", nil, false) + check(s.faint == 1, tag .. "survive case shows one fainted message (" .. s.faint .. ")") + check(not s.white, tag .. "survive case shows no whited out text") + check(Map.current == route, tag .. "survive case stays on " .. tostring(route)) + check(session.money == 3000, tag .. "survive case money unchanged (" .. tostring(session.money) .. ")") + local m1 = session.party[1] + check(m1 and (m1.hp or 0) == 0 and not m1.status, tag .. "fainted mon has 0 HP and poison cleared") + check(not Field.isLocked or not Field.isLocked(), tag .. "survive case releases the field") + + setup({ { species = "SPECIES_TORCHIC", psn = true } }, 3000) + local oldale = C:require("heal_locations", "HEAL_LOCATION_OLDALE_TOWN") + Field.setRespawn(oldale) + local oldaleMap = HealLocations.get(oldale).map + check(step(), tag .. "whiteout case took a poisoned step") + local w = pump("whiteout", "2736_01_" .. id .. "_whited_out_text.png", true) + check(w.faint == 1, tag .. "fainted message shown before whiteout") + check(w.white, tag .. "whited out message shown") + check(w.rom, tag .. "whited out page comes from ROM " .. whiteKey) + check(Map.current == oldaleMap, tag .. "respawned at last heal location " .. tostring(oldaleMap) + .. " (" .. tostring(Map.current) .. ")") + check(session.money == 1500, tag .. "money halved 3000 -> 1500 (" .. tostring(session.money) .. ")") + check(healed(), tag .. "party healed") + check(fadedIn(), tag .. "screen fades in from black after respawn (fade t=" .. tostring(Fade.t) .. ")") + U.wait(30) + U.shot(game, DIR .. "/2736_02_" .. id .. "_after_respawn.png") + + setup({ { species = "SPECIES_TORCHIC", psn = true } }, 0) + Field.setRespawn(oldale) + local target = oldaleMap + if isEm then + local lavaridge = C:require("heal_locations", "HEAL_LOCATION_LAVARIDGE_TOWN") + target = HealLocations.get(lavaridge).map + Flags.setFlag(Space.store, nil, C:require("flags", "FLAG_WHITEOUT_TO_LAVARIDGE"), true) + end + check(step(), tag .. "zero money case took a poisoned step") + local z = pump("zero_money", isEm and "2736_03_emerald_lavaridge_whited_out.png" or nil, true) + check(z.white and z.rom, tag .. "zero money whited out text from ROM") + check(Map.current == target, tag .. (isEm and "FLAG_WHITEOUT_TO_LAVARIDGE respawns at " or "respawns at ") + .. tostring(target) .. " (" .. tostring(Map.current) .. ")") + check(session.money == 0, tag .. "zero money stays 0 (" .. tostring(session.money) .. ")") + check(healed(), tag .. "party healed after zero money whiteout") + check(fadedIn(), tag .. "screen fades in after zero money respawn") + end) + check(okRun, tag .. "no crash " .. tostring(okRun and "" or err)) + return finish() +end diff --git a/tests/drivers/game3_rs_anim_ports_2735.lua b/tests/drivers/game3_rs_anim_ports_2735.lua new file mode 100644 index 00000000..405c3e3f --- /dev/null +++ b/tests/drivers/game3_rs_anim_ports_2735.lua @@ -0,0 +1,243 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or ".bazinga/BSA/10-07-26-00-userreported/shots/game3_rs_anim_ports_2735" + +return function(game) + local fails = 0 + local function result(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then fails = fails + 1 end + end + + for _ = 1, 600 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + game:_handleBootAction({ action = "new_game", name = "RUBY" }) + U.wait(180) + + local Runtime = require("src.core.game3.runtime") + local Party = require("src.core.game3.party") + local BattleBridge = require("src.core.game3.battle_bridge") + local Anim = require("src.core.game3.battle.anim") + local Ui = require("src.core.game3.battle.ui") + local AnimSprites = require("src.core.game3.battle.anim_sprites") + local AnimTasks = require("src.core.game3.battle.anim_tasks") + local AnimCallbacks = require("src.core.game3.battle.anim_callbacks") + local Secondary = require("src.core.game3.battle.effects.secondary") + + local session = Runtime.getSession() + session.party = {} + Party.giveMon(session, 6, 36) + local ok, err = BattleBridge.startWild(Runtime._mod, game, { species = 41, level = 30 }, { fade = false }) + result(ok == true, "wild battle started " .. tostring(err or "")) + if not ok then love.event.quit(1) return end + local ready = false + for _ = 1, 3000 do + if Ui._mode == "menu" and not Anim.busy() then ready = true break end + if Ui.dialogPending and Ui.dialogPending() then U.tap(game, "a") else U.wait(1) end + end + result(ready, "battle reached the action menu") + if not ready then love.event.quit(1) return end + U.wait(20) + + local fallbacks, stubs, spawned = 0, 0, {} + local get, spawn = AnimCallbacks.get, AnimTasks.spawn + AnimCallbacks.get = function(name) + local fn = get(name) + if fn == AnimCallbacks.SimpleFadeOut and name then fallbacks = fallbacks + 1 end + return fn + end + AnimTasks.spawn = function(name, ...) + spawned[tostring(name)] = true + if not AnimTasks.REGISTRY[tostring(name)] then stubs = stubs + 1 end + return spawn(name, ...) + end + + local function sprites(cb) + local out = {} + AnimSprites.forEachActive(function(s) + if s._cbName == cb and s.visible ~= false and not s.invisible then out[#out + 1] = s end + end) + return out + end + local function pos(s) return s.x + (s.ox or 0), s.y + (s.oy or 0) end + local function sideOpts(side) + local other = side == "player" and "enemy" or "player" + return { attackerSide = side, targetSide = other, isReversed = side == "enemy", + attackerSpecies = side == "player" and 6 or 41, targetSpecies = side == "player" and 41 or 6 } + end + local function reset() + for _, s in ipairs({ "player", "enemy" }) do + local p = Anim.present(s) + if p then p.visible, p.ox, p.oy, p.sx, p.sy, p.rotation = true, 0, 0, 1, 1, 0 end + end + for _ = 1, 20 do U.wait(1) end + end + local function run(label, launch, onFrame) + fallbacks, stubs, spawned = 0, 0, {} + local launched = launch() + local f = 0 + while f < 900 do + onFrame(f) + U.wait(1) + f = f + 1 + if f > 4 and not Anim.busy() then break end + end + result(launched ~= false, label .. " launched") + result(fallbacks == 0, label .. " used no SimpleFadeOut fallback callbacks (" .. fallbacks .. ")") + result(stubs == 0, label .. " spawned no unbound tasks (" .. stubs .. ")") + result(f < 900, label .. " finished in " .. f .. " frames") + reset() + end + + local function purplePixels(path) + local f = io.open(path, "rb") + if not f then return -1 end + local bytes = f:read("*a") + f:close() + local img = love.image.newImageData(love.filesystem.newFileData(bytes, "shot.png")) + local w, h = img:getDimensions() + local n = 0 + for y = 0, math.floor(h * 0.6) do + for x = math.floor(w * 0.45), w - 1 do + local r, g, b = img:getPixel(x, y) + r, g, b = r * 255, g * 255, b * 255 + if b >= 150 and b - g >= 40 and b - r >= 20 then n = n + 1 end + end + end + return n + end + local function targetDrawn() + local p = Anim.present("enemy") + return p.visible ~= false and not p.invisible and not p.battlerInvisible and not p.blinkHidden + and (p.alpha or 1) == 1 and (p.scale or 1) == 1 + end + local function rockLayers() + local monZ = require("src.core.game3.battle.anim_coords").monBehindZ("enemy") + 5 + local front, behind, xFront = 0, 0, false + AnimSprites.forEachActive(function(s) + if s._cbName == "RockTomb" then + if s.z > monZ then front = front + 1 elseif s.z < monZ then behind = behind + 1 end + elseif s._cbName == "RedX" then + xFront = s.z > monZ + end + end) + return front, behind, xFront + end + + local vm + local rockFall, xOk, xShot, rockShot = 0, nil, false, false + local rockStart = {} + local rockPx, xPx, xDrawn = -1, -1, false + local frontRocks, behindRocks, xFront = 0, 0, false + run("ROCK_TOMB", function() return Anim.launchMove(317, sideOpts("player")) end, function() + vm = Anim.vm() + for _, s in ipairs(sprites("RockTomb")) do + local _, y = pos(s) + rockStart[s] = rockStart[s] or y + rockFall = math.max(rockFall, y - rockStart[s]) + if not rockShot and y - rockStart[s] > 20 then + local path = DIR .. "/2733_01_rock_tomb_rocks_falling.png" + rockShot = U.still(game, path) + rockPx = purplePixels(path) + end + end + local xs = sprites("RedX") + if #xs > 0 then + local x, y = pos(xs[1]) + local tx, ty = vm:battlerCenter("target") + xOk = math.abs(x - tx) <= 8 and math.abs(y - ty) <= 16 + if not xShot then + U.wait(4) + local path = DIR .. "/2733_02_rock_tomb_red_x_on_target.png" + xDrawn = targetDrawn() + frontRocks, behindRocks, xFront = rockLayers() + xShot = U.still(game, path) + xPx = purplePixels(path) + end + end + end) + result(rockFall >= 32, "ROCK_TOMB rocks drop onto the target (fell " .. rockFall .. " px)") + result(xOk == true, "ROCK_TOMB red X sits on the target picture") + result(xDrawn, "ROCK_TOMB target picture is still drawn under the red X") + result(frontRocks == 3 and behindRocks == 1 and xFront, + "ROCK_TOMB red X and three rocks layer in front of the target, one rock behind (" .. frontRocks .. "/" .. behindRocks .. ")") + result(rockPx > 1000 and xPx >= 0 and xPx < rockPx * 0.15, + "ROCK_TOMB red X and rock pile cover the target like the cart (" .. xPx .. " of " .. rockPx .. " purple px)") + + local ringOk, noteMoved, uShot = nil, false, false + local noteStart = {} + run("UPROAR", function() return Anim.launchMove(253, sideOpts("enemy")) end, function() + vm = Anim.vm() + local rings = sprites("UproarRing") + if #rings > 0 and ringOk == nil then + local x, y = pos(rings[1]) + local ax, ay = vm:battlerCenter("attacker") + ringOk = math.abs(x - ax) <= 8 and math.abs(y - ay) <= 16 + end + for _, s in ipairs(sprites("JaggedMusicNote")) do + local x, y = pos(s) + noteStart[s] = noteStart[s] or { x, y } + if math.abs(x - noteStart[s][1]) + math.abs(y - noteStart[s][2]) >= 12 then noteMoved = true end + end + if not uShot and #rings > 0 and noteMoved then + uShot = U.still(game, DIR .. "/2734_01_uproar_ring_notes_on_user.png") + end + end) + result(ringOk == true, "UPROAR ring is centered on the user") + result(noteMoved, "UPROAR notes fly outward") + + run("SUPERSONIC", function() return Anim.launchMove(48, sideOpts("enemy")) end, function() end) + local ducks, dShot = 0, false + run("STATUS_CONFUSION", function() + local o = sideOpts("player") + o.targetSide = "player" + return Anim.launchStatus("CONFUSION", o) + end, function(f) + local n = #sprites("ConfusionDuck") + ducks = math.max(ducks, n) + if not dShot and n > 0 and f >= 12 then + dShot = U.still(game, DIR .. "/2735_01_confusion_ducks_over_head.png") + end + end) + result(ducks >= 2, "STATUS_CONFUSION ducks circle the head (" .. ducks .. ")") + + local needleFrom, needleTo, nShot = nil, nil, false + run("LEECH_LIFE", function() return Anim.launchMove(141, sideOpts("enemy")) end, function() + local n = sprites("LeechLifeNeedle") + if #n > 0 then + local x, y = pos(n[1]) + needleFrom = needleFrom or { x, y } + needleTo = { x, y } + if not nShot and math.abs(x - needleFrom[1]) + math.abs(y - needleFrom[2]) >= 6 then + nShot = U.still(game, DIR .. "/2735_02_leech_life_needle_flying.png") + end + end + end) + local travel = needleFrom and (math.abs(needleTo[1] - needleFrom[1]) + math.abs(needleTo[2] - needleFrom[2])) or 0 + result(travel >= 8, "LEECH_LIFE needle travels before the drain (" .. travel .. " px)") + + local sShot = false + run("STATS_CHANGE", function() + local o = sideOpts("enemy") + o.targetSide = "enemy" + o.animArg = Secondary.statAnimArg("attack", -1) + return Anim.launchGeneral("STATS_CHANGE", o) + end, function(f) + if not sShot and f == 20 then sShot = U.still(game, DIR .. "/2735_03_stats_change_growl_glow.png") end + end) + result(spawned.StatsChange == true, "STATS_CHANGE runs the StatsChange task") + + local hShot = false + run("HARDEN_STATS_CHANGE", function() + local o = sideOpts("player") + o.targetSide = "player" + o.animArg = Secondary.statAnimArg("defense", 1) + return Anim.launchGeneral("STATS_CHANGE", o) + end, function(f) + if not hShot and f == 20 then hShot = U.still(game, DIR .. "/2735_04_stats_change_harden_glow.png") end + end) + + AnimCallbacks.get, AnimTasks.spawn = get, spawn + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/game3_rs_move_menu_fit_2729.lua b/tests/drivers/game3_rs_move_menu_fit_2729.lua new file mode 100644 index 00000000..8f318cab --- /dev/null +++ b/tests/drivers/game3_rs_move_menu_fit_2729.lua @@ -0,0 +1,103 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/drv2729" + +return function(game) + local fails = 0 + local function result(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then fails = fails + 1 end + end + local function finish() love.event.quit(fails == 0 and 0 or 1) end + + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + game:_handleBootAction({ action = "new_game", name = "TAI", gender = 0 }) + U.wait(240) + + local Runtime = require("src.core.game3.runtime") + local Party = require("src.core.game3.party") + local BattleBridge = require("src.core.game3.battle_bridge") + local Battle = require("src.core.game3.battle") + local Ui = require("src.core.game3.battle.ui") + local version = os.getenv("POKEPORT_VERSION") or "sapphire" + local C = require("src.core.game3.constants").of(version) + local session = Runtime.getSession() + result(session ~= nil, "field session exists") + if not session then finish() return end + local function sp(name) return C.species.byName["SPECIES_" .. name] end + local function mv(name) return C.moves.byName["MOVE_" .. name] end + + session.party = {} + Party.giveMon(session, sp("TREECKO"), 11, "TREECKO") + local m = session.party[1] + m.moves = { mv("POUND"), mv("LEER"), mv("ABSORB"), mv("QUICK_ATTACK") } + m.pp = { 35, 30, 20, 30 } + m.maxPp = { 35, 30, 20, 30 } + + local ok, err = BattleBridge.startWild(Runtime._mod, game, { species = sp("WURMPLE"), level = 3 }, {}) + result(ok == true, "battle started " .. tostring(err or "")) + if not ok then finish() return end + local lastTap, f = 0, 0 + local function at_command() + return Battle.isActive() and Battle._phase == "command" and Ui._mode == "menu" + end + for _ = 1, 6000 do + if at_command() then break end + f = f + 1 + if Ui.dialogPending and Ui.dialogPending() and f - lastTap >= 14 then + lastTap = f + U.tap(game, "a") + else + U.wait(1) + end + end + result(at_command(), "reached command menu") + if not at_command() then finish() return end + U.wait(30) + local BattleChromeA = require("src.ui.game3.battle_chrome") + local menu = BattleChromeA.menuFrameRects("menu", "rs") + result(menu and menu[1][1] - 1 == 17, "action menu frame left border at tile 17") + U.still(game, DIR .. "/2729_00_action_menu.png") + U.tap(game, "a") + U.wait(30) + result(Ui._mode == "moves", "move menu open") + U.tap(game, "down") + U.wait(6) + U.tap(game, "right") + U.wait(10) + result(Ui._moveIndex == 4, "cursor on QUICK ATTACK") + + local BattleChrome = require("src.ui.game3.battle_chrome") + local Moves = require("src.core.game3.battle.moves") + local P = require("src.core.game3.profile").forSession(session) + local F = require(P.font.module) + local drawn + local orig = BattleChrome.drawMenuFrames + BattleChrome.drawMenuFrames = function(mode, layout) + drawn = BattleChrome.menuFrameRects(mode, layout) + return orig(mode, layout) + end + for _ = 1, 2000 do + if drawn then break end + U.wait(1) + end + BattleChrome.drawMenuFrames = orig + local left = drawn and drawn[1] + result(left ~= nil, "move menu frame drawn") + if left then + local limit = (left[1] + left[3]) * 8 + result(limit == 168, "left move frame content ends at x=168 (got " .. limit .. ")") + for i = 1, 4 do + local label = Moves.displayName(m.moves[i]) + local x = 8 + 80 * ((i - 1) % 2) + local right = x + F.measure(label) + result(right <= limit, string.format("move %d %s right edge %d within %d", i, label, right, limit)) + end + end + U.still(game, DIR .. "/2729_01_quick_attack_move_menu.png") + Battle.abort("run") + U.wait(30) + finish() +end diff --git a/tests/drivers/game3_transition_shade_2731.lua b/tests/drivers/game3_transition_shade_2731.lua new file mode 100644 index 00000000..8d1ba29a --- /dev/null +++ b/tests/drivers/game3_transition_shade_2731.lua @@ -0,0 +1,156 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or ".bazinga/BSA/10-07-26-00-userreported/shots/game3_transition_shade_2731" +local VERSION = os.getenv("POKEPORT_VERSION") or "emerald" + +local function loadPng(path) + local f = path and io.open(path, "rb") + if not f then return nil end + local s = f:read("*a") + f:close() + return love.image.newImageData(love.filesystem.newFileData(s, "x.png")) +end + +local function diffCount(a, b) + if not (a and b) then return -1 end + local w, h = math.min(a:getWidth(), b:getWidth()), math.min(a:getHeight(), b:getHeight()) + local n = 0 + for y = 0, h - 1, 4 do + for x = 0, w - 1, 4 do + local r1, g1, b1 = a:getPixel(x, y) + local r2, g2, b2 = b:getPixel(x, y) + if math.abs(r1 - r2) + math.abs(g1 - g2) + math.abs(b1 - b2) > 0.06 then n = n + 1 end + end + end + return n +end + +local function grayShare(img) + if not img then return 0 end + local w, h = img:getWidth(), img:getHeight() + local n, g = 0, 0 + for y = math.floor(h * 0.3), math.floor(h * 0.7), 4 do + for x = math.floor(w * 0.3), math.floor(w * 0.7), 4 do + local r, gg, b = img:getPixel(x, y) + n = n + 1 + if math.max(r, gg, b) - math.min(r, gg, b) < 0.08 and r > 0.2 and r < 0.55 then g = g + 1 end + end + end + return n > 0 and g / n or 0 +end + +return function(game) + local fails = 0 + local function result(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then fails = fails + 1 end + end + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + game:_handleBootAction({ action = "new_game", name = "NICK", gender = 0 }) + U.wait(240) + local Runtime = require("src.core.game3.runtime") + local Party = require("src.core.game3.party") + local BattleBridge = require("src.core.game3.battle_bridge") + local Battle = require("src.core.game3.battle") + local BT = require("src.core.game3.battle_transition") + local Map = require("src.core.game3.map") + local MapIds = require("src.core.game3.map_ids") + local C = require("src.core.game3.constants").of(VERSION) + local session = Runtime.getSession() + local Ids = BT.ids() + local rse = VERSION ~= "firered" and VERSION ~= "leafgreen" + local W = rse and require("src.core.game3.field_weather_rse") or nil + + local cases + if rse then + cases = { + { map = "MAP_ROUTE104", x = 20, y = 40, tid = "POKEBALLS_TRAIL", shade = false }, + { map = "MAP_PETALBURG_WOODS", x = 14, y = 32, tid = "POKEBALLS_TRAIL", shade = true }, + { map = "MAP_PETALBURG_WOODS", x = 14, y = 32, tid = "ANGLED_WIPES", shade = true }, + { map = "MAP_PETALBURG_WOODS", x = 14, y = 32, tid = "SLICE", shade = true }, + { map = "MAP_PETALBURG_WOODS", x = 14, y = 32, tid = "WHITE_BARS_FADE", shade = true }, + { map = "MAP_PETALBURG_WOODS", x = 14, y = 32, pick = "WHITE_BARS_FADE", lead = 5, foe = 6, shade = true }, + { map = "MAP_ROUTE120", x = 22, y = 61, tid = "POKEBALLS_TRAIL", rain = true }, + { map = "MAP_ROUTE120", x = 22, y = 61, tid = "WHITE_BARS_FADE", rain = true }, + } + else + cases = { + { map = "MAP_ROUTE1", x = 10, y = 20, tid = "POKEBALLS_TRAIL" }, + } + end + + for i, c in ipairs(cases) do + local mapId = MapIds.forConst(c.map, VERSION) + local tag = string.format("%s %s", c.map, c.tid or ("pick " .. c.pick)) + local stem = string.format("%s/2731_%02d_%s_%s_%s", DIR, i, VERSION, c.map:lower():gsub("^map_", ""), + (c.tid or c.pick):lower()) + local ok, err = pcall(function() Map.load(nil, game, mapId, { x = c.x, y = c.y, facing = "down" }) end) + result(ok and mapId ~= nil, "load " .. tag .. " " .. tostring(err or "")) + U.wait(120) + if c.rain then + W.setNextWeather(W.WEATHER.RAIN) + end + U.wait(240) + session.party = {} + Party.giveMon(session, C.species.byName.SPECIES_MUDKIP, c.lead or 20, "MUDKIP") + if W then + local snap = W.snapshot() + print(string.format("[driver] %s weather %s colorMapIndex %s", tag, tostring(snap.curr), tostring(snap.colorMapIndex))) + if c.shade or c.rain then + result(snap.colorMapIndex ~= 0, tag .. " weather color map active") + end + end + local base = stem .. "_base.png" + U.still(game, base) + local opts = {} + if c.tid then opts.transitionId = Ids.ID[c.tid] end + BattleBridge.startWild(Runtime._mod, game, { species = C.species.byName.SPECIES_WURMPLE, level = c.foe or 5 }, opts) + if c.pick then + result(BT._transitionId == Ids.ID[c.pick], tag .. " level compare picked " .. c.pick + .. " (got " .. tostring(BT._transitionId) .. ")") + end + local peakShot + local mainShots = {} + local frames, mainAt = 0, nil + while frames < 900 do + U.wait(1) + frames = frames + 1 + if BT._phase == "intro" and BT._intro and BT._intro.blend >= 14 and not peakShot then + peakShot = stem .. "_gray_flash.png" + U.still(game, peakShot) + end + if BT._phase == "main" and not mainAt then mainAt = frames end + if mainAt and (frames - mainAt == 8 or frames - mainAt == 20 or frames - mainAt == 32) then + local p = string.format("%s_main_%02d.png", stem, frames - mainAt) + U.still(game, p) + mainShots[#mainShots + 1] = p + end + if Battle.isActive() then break end + end + local b = loadPng(base) + local peakImg = loadPng(peakShot) + local dPeak = diffCount(b, peakImg) + local dMain, best = -1, nil + for _, p in ipairs(mainShots) do + local d = diffCount(b, loadPng(p)) + if d > dMain then dMain, best = d, p end + end + for _, p in ipairs(mainShots) do + if p ~= best then os.remove(p) end + end + local gray = grayShare(peakImg) + print(string.format("[driver] %s introDiff %d mainDiff %d grayShare %.2f", tag, dPeak, dMain, gray)) + result(dPeak > 2000, tag .. " intro flash visible (diff " .. dPeak .. ")") + result(gray > 0.8, string.format("%s intro flash is gray (%.2f)", tag, gray)) + result(dMain > 2000, tag .. " main transition visible (diff " .. dMain .. ")") + Battle.abort("run") + for _ = 1, 600 do + if not Battle.isActive() then break end + U.wait(1) + end + U.wait(60) + end + love.event.quit(fails == 0 and 0 or 1) +end diff --git a/tests/drivers/rs_bag_berry_2727.lua b/tests/drivers/rs_bag_berry_2727.lua new file mode 100644 index 00000000..09533325 --- /dev/null +++ b/tests/drivers/rs_bag_berry_2727.lua @@ -0,0 +1,135 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/drv2727" + +local function until_(n, fn) + for _ = 1, n do + if fn() then return true end + U.wait(1) + end + return fn() +end + +local function loadShot(path) + local f = io.open(path, "rb") + if not f then return nil end + local bytes = f:read("*a") + f:close() + return love.image.newImageData(love.filesystem.newFileData(bytes, "shot.png")) +end + +local function hex(r, g, b) + return string.format("%02x%02x%02x", math.floor(r * 255 + 0.5), math.floor(g * 255 + 0.5), math.floor(b * 255 + 0.5)) +end + +local function cellColors(img, cellX, cellY) + local scale = img:getWidth() / 240 + local seen = {} + for y = 0, 7 do for x = 0, 7 do + local px, py = math.floor((cellX + x + 0.5) * scale), math.floor((cellY + y + 0.5) * scale) + seen[hex(img:getPixel(px, py))] = true + end end + return seen +end + +return function(game) + local pass = true + local function check(cond, label) + print((cond and "PASS " or "FAIL ") .. label) + if not cond then pass = false end + end + until_(900, function() return game.phase == "boot" and game.boot end) + local ok, err = xpcall(function() + local Runtime = require("src.core.game3.runtime") + local Warp = require("src.core.game3.warp") + local Message = require("src.ui.game3.message") + pcall(function() game:_handleBootAction({ action = "new_game", name = "MAY", gender = 1 }) end) + U.wait(60) + until_(600, function() + if Message.isOpen and Message.isOpen() then U.tap(game, "b") end + return not (Warp.isBusy() or (Message.isOpen and Message.isOpen())) + end) + local s = Runtime.getSession() + local C = require("src.core.game3.constants").active(s) + local Bag = require("src.core.game3.bag") + Bag.add(s.bag, C:require("items", "ITEM_POTION"), 3) + Bag.add(s.bag, C:require("items", "ITEM_LIECHI_BERRY"), 1) + Bag.add(s.bag, C:require("items", "ITEM_RAZZ_BERRY"), 10) + Bag.add(s.bag, C:require("items", "ITEM_TM39_ROCK_TOMB"), 1) + Bag.add(s.bag, C:require("items", "ITEM_HM01_CUT"), 1) + local BagMenu = require("src.ui.game3.bag_menu") + local Font = require("src.ui.game3.frlg_font") + local realDraw = Font.draw + local calls = {} + Font.draw = function(str, x, y, opts) + calls[#calls + 1] = {s = str, x = x, y = y} + return realDraw(str, x, y, opts) + end + local function find(str, y) + for i = #calls, 1, -1 do + local c = calls[i] + if c.s == str and (y == nil or c.y == y) then return c.x, c.y end + end + end + local function rightEdge(str, y) + local x = find(str, y) + return x and x + Font.measure(str, {font = "normal"}) + end + + local function capture(gender, tag, pocket, idx) + s.gender, s.playerGender = gender, gender + BagMenu.show(s, { session = s, pocket = pocket }) + until_(120, function() return not BagMenu._open end) + U.wait(10) + calls = {} + local path = string.format("%s/2727_%s_%s_%s.png", DIR, idx, tag, pocket:lower()) + U.still(game, path) + local img = loadShot(path) + BagMenu.close() + U.wait(20) + return img + end + + for _, who in ipairs({{1, "female", "0"}, {0, "male", "1"}}) do + local gender, tag, base = who[1], who[2], who[3] + local img = capture(gender, tag, "BERRY_POUCH", base .. "1") + local lx, ly = find("36") + check(lx == 120, tag .. " LIECHI No36 number at x=120") + check(ly ~= nil and find("×", ly) == 208, tag .. " LIECHI multiply sign at x=208") + check(ly ~= nil and rightEdge("1", ly) == 232, tag .. " LIECHI count right-aligned to 232") + local _, ry = find("10") + check(ry ~= nil and rightEdge("10", ry) == 232 and find("×", ry) == 208, tag .. " RAZZ x10 count right-aligned to 232") + if img then + local selectedCell, stripeLeak, idleStripe = 40 + 3 * 8, false, false + for p = 0, 4 do + local cx = 40 + p * 8 + local seen = cellColors(img, cx, 72) + if cx ~= selectedCell and (seen["6bb5d6"] or seen["297ba5"]) then idleStripe = true end + if seen["297ba5"] then stripeLeak = true end + end + if gender == 1 then + check(not stripeLeak and not idleStripe, "female indicator dots have no blue stripe pixels") + else + check(idleStripe, "male indicator dots sit on blue stripes") + end + else check(false, tag .. " berry shot readable") end + + capture(gender, tag, "TM_CASE", base .. "2") + local tx, ty = find("39") + check(tx == 120, tag .. " TM39 number at x=120") + check(ty ~= nil and find("ROCK TOMB", ty) == 136, tag .. " TM39 name at x=136") + check(ty ~= nil and find("×", ty) == 214, tag .. " TM multiply sign at x=214") + check(ty ~= nil and rightEdge("1", ty) == 232, tag .. " TM count right-aligned to 232") + local _, hy = find("CUT") + check(hy ~= nil and find("1", hy) == 129, tag .. " HM01 number at x=129") + check(hy ~= nil and find("×", hy) == nil, tag .. " HM row has no quantity") + + capture(gender, tag, "ITEMS", base .. "3") + check(find("×", 16) == 214, tag .. " item multiply sign at x=214") + check(rightEdge("3", 16) == 232, tag .. " item count right-aligned to 232") + end + Font.draw = realDraw + end, debug.traceback) + if not ok then print("FAIL driver error: " .. tostring(err)); pass = false end + love.event.quit(pass and 0 or 1) + U.wait(10) +end diff --git a/tests/drivers/save_editor_unown_form_2726.lua b/tests/drivers/save_editor_unown_form_2726.lua new file mode 100644 index 00000000..b0782a31 --- /dev/null +++ b/tests/drivers/save_editor_unown_form_2726.lua @@ -0,0 +1,146 @@ +local U = require("tests.drivers.util") +return function(game) + U.wait(20) + package.path = package.path .. ";./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua" + local App, Paint, Kit, path, tmp, oldPaint + local dim, safe, osName + local failures = 0 + local function pass(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + end + local ok, err = xpcall(function() + App = require("tools.save-editor.App") + local Gen, Ops, MonOps = require("Gen"), require("Ops"), require("MonOps") + local Touch, History = require("TouchEditor"), require("History") + local Serializer = require("src.core.SaveSerializer") + local version = require("src.core.GameVersion").get() + local dir = assert(os.getenv("POKEPORT_SHOT_DIR")) + Kit, Paint = require("Kit"), require("src.ui.kit.Button") + dim, safe, osName = love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS + local controls = {} + oldPaint = Paint.draw + Paint.draw = function(owner, x, y, w, h, label, opts, hot, focused) + if owner == Kit then controls[opts.id or label] = { x = x, y = y, w = w, h = h, label = label } end + return oldPaint(owner, x, y, w, h, label, opts, hot, focused) + end + tmp = os.tmpname() + path = tmp .. "-unown-2726.lua" + local file = assert(io.open(path, "wb")) + file:write(Serializer.encode(Gen.newGame(version))); file:close() + App.load(path, { version = version, embedded = true }) + local S = App.getState() + assert(S.save and not S.loadError and not S.missingCache, S.status) + local g = Gen.ofState(S) + local mon = MonOps.create(S.data, g == 3 and MonOps.SPECIES_UNOWN_G3 or "UNOWN", 25, g) + if g == 3 then + mon.otId, mon.otSecretId = S.save.player and S.save.player.trainerId or 0, S.save.player and S.save.player.secretId or 0 + end + S.save.party = { mon } + Ops.selectParty(S, 1) + assert(Ops.setUnownForm(S, mon, g == 3 and 5 or 6), "seed form F") + S.dirty = false + local W, H, platform = 390, 844, "Android" + local function frame(name) + controls = {} + love.graphics.getDimensions = function() return W, H end + love.window.getSafeArea = function() return 0, 0, W, H end + love.system.getOS = function() return platform end + local canvas = love.graphics.newCanvas(W, H, { msaa = 0 }) + love.graphics.push("all") + love.graphics.setCanvas(canvas) + love.graphics.origin() + love.graphics.setScissor() + love.graphics.setShader() + require("src.render.GameViewport").reset() + love.graphics.clear(0, 0, 0, 1) + App.draw() + love.graphics.setCanvas() + love.graphics.pop() + if name then + local pixels = canvas:newImageData() + local bytes = pixels:encode("png"):getString() + pixels:release() + local out = assert(io.open(dir .. "/" .. version .. "-" .. name .. ".png", "wb")) + out:write(bytes); out:close() + end + canvas:release() + love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS = dim, safe, osName + end + local function tap(id) + local r = assert(controls[id], "missing control " .. id) + App.mousepressed(r.x + r.w / 2, r.y + r.h / 2, 1) + frame() + frame() + end + local function current() return Ops.unownForm(S, S.save.party[1]) end + local function letterName() return Ops.unownFormName(S, current()) end + local function pick(letter) + local p = assert(S.editPopup, "form popup open") + p.query, p.index = "", nil + for i, o in ipairs(p.options) do if o[1] == letter then p.index = i end end + assert(p.index, "option present") + Touch.commit(S, Kit) + frame() + end + + S.tab, S.monSection, S.mobileInspector = "party", "main", false + frame(); frame("2726_01_roster_unown_f_phone") + pass(letterName() == "F", version .. " roster seeded with UNOWN F") + + S.mobileInspector = true + frame(); frame() + local r = controls["choice-form"] + for _ = 1, 40 do + if r and r.y >= H * 0.3 and r.y + r.h <= H * 0.7 then break end + S.inspectorScroll = (S.inspectorScroll or 0) + 40 + frame() + r = controls["choice-form"] + end + frame("2726_02_form_picker_row_phone") + pass(r ~= nil, version .. " phone inspector shows the Unown form picker") + pass(r and r.label == "Unown form: F", version .. " picker reads the current letter (" .. tostring(r and r.label) .. ")") + tap("choice-form") + pass(S.editPopup and S.editPopup.id == "form", version .. " tapping the picker opens the touch chooser") + pass(S.editPopup and #S.editPopup.options == (g == 3 and 28 or 26), version .. " chooser lists every form") + frame("2726_03_form_chooser_open_phone") + local target = g == 3 and 27 or 17 + pick(target) + pass(current() == target, version .. " chooser sets the form to " .. letterName()) + pass(S.dirty, version .. " form edit marks the save dirty") + frame() + frame("2726_04_form_set_" .. letterName():gsub("%?", "qmark") .. "_phone") + pass(History.undo(S) and letterName() == "F", version .. " undo restores F") + pass(History.redo(S) and current() == target, version .. " redo reapplies") + Ops.selectParty(S, 1) + + W, H, platform = 1100, 720, "Linux" + S.inspectorScroll = 0 + frame(); frame() + for _ = 1, 40 do + r = controls["choice-form"] + if r and r.y >= 0 and r.y + r.h <= H then break end + S.inspectorScroll = (S.inspectorScroll or 0) + 80 + frame() + end + frame("2726_05_desktop_docked_picker_and_roster") + pass(controls["choice-form"] ~= nil, version .. " desktop docked inspector shows the picker") + + assert(App.save(), "save") + App.unload() + App.load(path, { version = version, embedded = true }) + S = App.getState() + mon = S.save.party[1] + pass(Ops.unownForm(S, mon) == target, version .. " reopened save keeps the form") + end, debug.traceback) + if App then App.unload() end + if Paint and oldPaint then Paint.draw = oldPaint end + if dim then love.graphics.getDimensions, love.window.getSafeArea, love.system.getOS = dim, safe, osName end + if path then + os.remove(path) + for _, backup in ipairs(require("tests.fs_io").globPrefix(path .. ".bak-")) do os.remove(backup) end + end + if tmp then os.remove(tmp) end + if not ok then print("FAIL save_editor_unown_form_2726: " .. tostring(err)) end + love.event.quit((ok and failures == 0) and 0 or 1) +end diff --git a/tests/engine/android_old_shell_restart_bug2738.lua b/tests/engine/android_old_shell_restart_bug2738.lua new file mode 100644 index 00000000..08cadc98 --- /dev/null +++ b/tests/engine/android_old_shell_restart_bug2738.lua @@ -0,0 +1,128 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq +love = love or require("tests.love_stub") + +local HostShell = require("src.core.HostShell") + +local quits = {} +love.event = { + quit = function(...) + quits[#quits + 1] = { n = select("#", ...), arg = (...) } + end, +} + +local osName = "Android" +local restartApp = nil +love.system = love.system or {} +love.system.getOS = function() return osName end + +local exits = {} +local realExit = os.exit +os.exit = function(code) + exits[#exits + 1] = code + if code ~= nil and type(code) ~= "number" and type(code) ~= "boolean" then + error("bad argument #1 to 'exit' (number expected, got " .. type(code) .. ")") + end +end + +local function oldShellStep(a) + if love.system and love.system.getOS() == "Android" then + os.exit(a or 0) + end + return a or 0 +end + +local function driveOldShell() + local errs = {} + for _, q in ipairs(quits) do + local ok, err = pcall(oldShellStep, q.arg) + if not ok then errs[#errs + 1] = tostring(err) end + end + return errs +end + +local function reset() + for i = #quits, 1, -1 do quits[i] = nil end + for i = #exits, 1, -1 do exits[i] = nil end +end + +_G.POKEPORT_LOOP_RESTART = nil +love.system.restartApp = nil +HostShell.restart() +eq(#quits, 1, "Android restart on an older shell still quits once") +eq(quits[1] and quits[1].n, 0, "with a bare quit(), never quit(\"restart\")") +local errs = driveOldShell() +eq(#errs, 0, "the old stepper's os.exit(a or 0) never errors: " .. table.concat(errs, "; ")) +for _, code in ipairs(exits) do + check(type(code) == "number", "old stepper exits with a number, got " .. type(code)) +end +check(HostShell.canRestart ~= nil and not HostShell.canRestart(), + "an older Android shell cannot restart in-process") + +reset() +local restartCalls = 0 +love.system.restartApp = function() restartCalls = restartCalls + 1 return true end +HostShell.restart() +eq(restartCalls, 1, "an older shell with the restartApp bridge relaunches through it") +eq(#quits, 0, "and sends no quit event") + +reset() +love.system.restartApp = function() return false end +HostShell.restart() +eq(#quits, 1, "a failed restartApp falls back to one quit") +eq(#driveOldShell(), 0, "which the old stepper exits on cleanly") + +for _, mod in ipairs({ "src.core.Game", "src.core.Game3" }) do + reset() + love.system.restartApp = nil + local ok, M = pcall(require, mod) + if ok and type(M) == "table" and type(M.restartWithMods) == "function" then + local stub = setmetatable({}, { __index = function() return function() end end }) + pcall(M.restartWithMods, stub) + for _, q in ipairs(quits) do + check(type(q.arg) ~= "string", mod .. ":restartWithMods sends no string quit on an older shell") + end + eq(#driveOldShell(), 0, mod .. ":restartWithMods survives the old stepper") + end +end + +reset() +local ManagerState = require("src.mods.ManagerState") +ManagerState.restartGame({ game = {} }) +for _, q in ipairs(quits) do + check(type(q.arg) ~= "string", "ManagerState:restartGame sends no string quit on an older shell") +end +eq(#driveOldShell(), 0, "ManagerState:restartGame survives the old stepper") + +reset() +_G.POKEPORT_LOOP_RESTART = true +check(HostShell.canRestart and HostShell.canRestart(),"a shell whose love.run sets the flag restarts in-process") +HostShell.restart() +eq(quits[1] and quits[1].arg, "restart", "and HostShell.restart sends quit(\"restart\")") + +_G.POKEPORT_LOOP_RESTART = nil +osName = "iOS" +check(HostShell.canRestart and HostShell.canRestart(),"iOS keeps its bare-quit restart") +osName = "OS X" +check(HostShell.canRestart and HostShell.canRestart(),"desktop keeps quit(\"restart\")") + +local f = assert(io.open("main.lua", "rb")) +local mainSrc = f:read("*a") +f:close() +local runBody = mainSrc:match("\nfunction love%.run%(%)(.-)\nend\n") or "" +local flagAt = runBody:find("_G.POKEPORT_LOOP_RESTART = true", 1, true) +local loadAt = runBody:find("love.load(", 1, true) +check(flagAt ~= nil and loadAt ~= nil and flagAt < loadAt, + "love.run advertises restart support before love.load chainloads a payload") +local returnBody = mainSrc:match("local function returnToLauncher%(opts%)(.-)\nend\n") or "" +local gateAt = returnBody:find("canRestart()", 1, true) +local inProcAt = returnBody:find("rebuildLauncherInProcess(opts)", 1, true) +local restartAt = returnBody:find('require("src.core.HostShell").restart()', 1, true) +check(gateAt ~= nil and inProcAt ~= nil and restartAt ~= nil and gateAt < restartAt + and inProcAt < restartAt, + "EXIT GAME rebuilds the launcher in-process when the shell cannot restart") + +os.exit = realExit +T.finish("android_old_shell_restart_bug2738") diff --git a/tests/engine/chipaudio_prewarm_queue_2732.lua b/tests/engine/chipaudio_prewarm_queue_2732.lua new file mode 100644 index 00000000..e2a82493 --- /dev/null +++ b/tests/engine/chipaudio_prewarm_queue_2732.lua @@ -0,0 +1,167 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local S = require("tests.harness").suite("chipaudio prewarm queue 2732") +local check, eq = S.check, S.eq + +love = require("tests.love_stub") +local ffi = require("ffi") + +local SoundData = {} +SoundData.__index = SoundData +function SoundData:setSample(index, a, b) + if b == nil then + self.buf[index * self.channels] = a * 32767 + else + self.buf[index * self.channels + (a - 1)] = b * 32767 + end +end +function SoundData:getFFIPointer() return self.buf end +function SoundData:getSampleCount() return self.samples end +function SoundData:getSampleRate() return self.rate end +function SoundData:getChannelCount() return self.channels end +love.sound.newSoundData = function(samples, rate, _, channels) + return setmetatable({ samples = samples, rate = rate, channels = channels, + buf = ffi.new("int16_t[?]", samples * channels) }, SoundData) +end +love.audio = { newSource = function(sd) return { sd = sd } end } + +local clock = 0 +local clockStep = 0 +love.timer.getTime = function() + clock = clock + clockStep + return clock +end + +local channels = {} +local function channel(name) + local ch = channels[name] + if ch then return ch end + local items = {} + ch = { items = items } + function ch:push(v) items[#items + 1] = v end + function ch:pop() return table.remove(items, 1) end + function ch:clear() for i = #items, 1, -1 do items[i] = nil end end + function ch:getCount() return #items end + channels[name] = ch + return ch +end +love.thread = { + newThread = function() + return { start = function() end, getError = function() return nil end } + end, + getChannel = channel, +} + +local ChipSynth = require("src.core.ChipSynth") +local ChipAsm = require("src.audio.ChipAsm") +local ChipAudio = require("src.core.ChipAudio") +local FixedStep = require("src.core.FixedStep") + +local beep = ChipAsm.sfx{ channels = { { hw = 1, program = { + { squareNote = { len = 4, volume = 15, fade = 1, frequency = 0x600 } }, +} } } } +local data = { audio = { sfx = { Beep = beep }, cries = {} } } + +local function workerStep(limit) + local cmdCh, fxCh = channel("chipaudio_cmd"), channel("chipaudio_fx") + local n = 0 + while n < (limit or math.huge) do + local req = cmdCh:pop() + if not req then break end + if req.cmd == "effect" then + n = n + 1 + fxCh:push({ key = req.key, epoch = req.epoch, + sd = ChipSynth.renderEffectData({ audio = req.audio }, req.header, + req.options or {}) }) + end + end + return n +end + +for pitch = 1, 5 do ChipAudio.prewarmSfx(data, "Beep", pitch) end +local st = ChipAudio._effectStateForTest() +eq(st.inFlight, 2, "two prewarms handed to the worker at once") +eq(st.queued, 3, "the rest wait on the main thread") +eq(#channel("chipaudio_cmd").items, 2, "only two effect commands pushed") + +ChipAudio.resetSyncStats() +check(ChipAudio.newSfx(data, "Beep", 5) ~= nil, "a queued sfx still plays") +eq(ChipAudio._effectStateForTest().queued, 2, + "playing a queued sfx drops its prewarm") +eq(ChipAudio.stats().syncRenders, 1, "and is counted as a sync render") + +workerStep() +ChipAudio.pumpEffects() +st = ChipAudio._effectStateForTest() +eq(st.ready, 2, "finished prewarms are ready") +eq(st.inFlight, 2, "the queue refills the worker as results land") +workerStep() +ChipAudio.pumpEffects() +st = ChipAudio._effectStateForTest() +eq(st.ready, 4, "all four left are ready") +eq(st.queued + st.inFlight, 0, "nothing left queued") + +ChipAudio.resetSyncStats() +check(ChipAudio.newSfx(data, "Beep", 1) ~= nil, "prewarmed sfx plays") +eq(ChipAudio.stats().syncRenders, 0, "a prewarmed play renders nothing") +eq(ChipAudio.stats().prewarmHits, 1, "and is counted as a hit") + +ChipAudio.prewarmPinned(function() + for pitch = 50, 66 do ChipAudio.prewarmSfx(data, "Beep", pitch) end +end) +for _ = 1, 20 do + workerStep() + ChipAudio.pumpEffects() +end +st = ChipAudio._effectStateForTest() +eq(st.pinned, 17, "a session set of 17 is pinned") +eq(st.ready, 20, "pinned prewarms are ready") + +for pitch = 100, 199 do + ChipAudio.prewarmSfx(data, "Beep", pitch) + workerStep() + ChipAudio.pumpEffects() +end +for _ = 1, 10 do + workerStep() + ChipAudio.pumpEffects() +end +st = ChipAudio._effectStateForTest() +eq(st.unpinnedReady, 64, "unpinned ready prewarms are capped") +eq(st.ready, 64 + 17, "pinned ones sit outside the cap") +ChipAudio.resetSyncStats() +for pitch = 50, 66 do ChipAudio.newSfx(data, "Beep", pitch) end +eq(ChipAudio.stats().syncRenders, 0, + "every pinned session sound survives a 100-sound battle prewarm") +ChipAudio.newSfx(data, "Beep", 199) +eq(ChipAudio.stats().syncRenders, 0, "the newest unpinned prewarm survives the cap") +ChipAudio.newSfx(data, "Beep", 100) +eq(ChipAudio.stats().syncRenders, 1, "the oldest unpinned one was evicted") +eq(ChipAudio._effectStateForTest().pinned, 17, "the pin budget is unchanged") + +ChipAudio.prewarmPinned(function() + for pitch = 400, 440 do ChipAudio.prewarmSfx(data, "Beep", pitch) end +end) +eq(ChipAudio._effectStateForTest().pinned, 24, "pins stop at the pin budget") +for _ = 1, 30 do + workerStep() + ChipAudio.pumpEffects() +end +check(ChipAudio._effectStateForTest().ready <= 64 + 24, + "ready PCM never exceeds both budgets together") + +FixedStep:init(function() end) +ChipAudio.resetSyncStats() +clockStep = 0.001 +ChipAudio.newSfx(data, "Beep", 300) +check(not FixedStep.suppressCatchup, "a short sync render keeps the catch-up") +clockStep = 0.05 +ChipAudio.newSfx(data, "Beep", 301) +check(FixedStep.suppressCatchup, "a sync render over a frame drops the catch-up") +eq(FixedStep.accum, 0, "and clears the accumulator") +check(ChipAudio.stats().syncRenderMsMax >= 16, "its duration is recorded") +check(ChipAudio.stats().line:find("sync=2/", 1, true) ~= nil, + "stats.line carries the counters") +clockStep = 0 + +S.finish() diff --git a/tests/engine/game3_object_spawn_elevation_2737_test.lua b/tests/engine/game3_object_spawn_elevation_2737_test.lua new file mode 100644 index 00000000..24d71060 --- /dev/null +++ b/tests/engine/game3_object_spawn_elevation_2737_test.lua @@ -0,0 +1,98 @@ +-- pokeemerald/src/event_object_movement.c:7737, pokeemerald/src/event_object_movement.c:4931 + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq +love = love or require("tests.love_stub") + +local GameVersion = require("src.core.GameVersion") +GameVersion.set("emerald") +local Collision = require("src.core.game3.collision") +local Objects = require("src.core.game3.objects") +local FieldView = require("src.core.game3.field_view") + +local W, H = 19, 9 +local elev = {} +for y = 0, H - 1 do + for x = 0, W - 1 do elev[y * W + x] = 3 end +end +elev[5 * W + 17] = 4 +elev[5 * W + 2] = 15 +elev[6 * W + 4] = 0 +local layout = { width = W, height = H, pair = "spawn_elev_test" } +function layout:elevAt(x, y) return elev[y * W + x] end +function layout:collAt() return 0 end +function layout:midAt() return 0x200 end +function layout:collArray() + local c = {} + for i = 1, W * H do c[i] = 0 end + return c +end +local mapDef = { midLayout = layout, pair = layout.pair, warps = {} } +Collision.bindMap({ save = { position = {} } }, "SPAWN_ELEV_TEST", mapDef) + +local defs = { + { localId = 1, x = 17, y = 5, elevation = 3, graphicsId = 7, movementType = 0x0A }, + { localId = 2, x = 2, y = 5, elevation = 3, graphicsId = 7, movementType = 0x0A }, + { localId = 3, x = 4, y = 6, elevation = 3, graphicsId = 7, movementType = 0x0A }, +} +local pool = Objects.spawnFromDefs(defs, mapDef, nil) +local stone, multi, trans = pool.byId[1], pool.byId[2], pool.byId[3] +check(stone and multi and trans, "objects spawned") +eq(stone.elevation, 3, "template elevation before first update") + +local savedById, savedOrder, savedTracks = Objects._byId, Objects._order, Objects._tracks +Objects._byId, Objects._order, Objects._tracks = pool.byId, pool.order, {} +Objects.update({ save = { position = {} } }) + +eq(stone.elevation, 4, "spawn_elevation_follows_metatile") +eq(stone.currentElevation, 4, "spawn current elevation follows metatile") +eq(multi.elevation, 3, "multi-level tile keeps template elevation") +eq(trans.elevation, 3, "transition tile keeps previous elevation") +eq(trans.currentElevation, 0, "transition tile sets current elevation 0") +check(not stone.spawnElevation, "spawn update runs once") + +local under, over = FieldView.applyDrawOrder({ + { kind = "npc", eventObject = stone, elevation = stone.elevation, y = 5 * 16 }, +}, {}, {}, 0) +eq(#over, 1, "mr_stone_draws_over_bg1") +eq(#under, 0, "mr stone not under top layer") + +local savedMapId, savedDefs = Objects._mapId, Objects._defs +Objects._mapId, Objects._defs = "SPAWN_ELEV_TEST", defs +local snap = Objects.snapshot() +eq(#snap.list, 0, "snapshot has no row for a spawn-resolved elevation") +Objects._mapId, Objects._defs = savedMapId, savedDefs +Objects._byId, Objects._order, Objects._tracks = savedById, savedOrder, savedTracks + +local NW, NH = 5, 5 +local nelev = {} +for y = 0, NH - 1 do + for x = 0, NW - 1 do nelev[y * NW + x] = 3 end +end +nelev[1 * NW + 1] = 4 +local nlayout = { width = NW, height = NH, pair = "spawn_elev_neighbor" } +function nlayout:elevAt(x, y) return nelev[y * NW + x] end +function nlayout:collAt() return 0 end +function nlayout:midAt() return 0x200 end +function nlayout:collArray() + local c = {} + for i = 1, NW * NH do c[i] = 0 end + return c +end +local nmapDef = { midLayout = nlayout, pair = nlayout.pair, warps = {} } +local npool = Objects.spawnFromDefs({ + { localId = 1, x = 1, y = 1, elevation = 3, graphicsId = 7, movementType = 0x0A }, + { localId = 2, x = 3, y = 3, elevation = 3, graphicsId = 7, movementType = 0x0A }, +}, nmapDef, nil) +local raised, flat = npool.byId[1], npool.byId[2] +eq(raised.elevation, 3, "neighbor template elevation before first tick") +eq(layout:elevAt(1, 1), 3, "active map cell under the neighbor NPC is elevation 3") +Objects.tickPool(npool, { save = { position = {} } }, nil) +eq(raised.elevation, 4, "neighbor_pool_spawn_elevation_follows_own_metatile") +eq(raised.currentElevation, 4, "neighbor pool current elevation follows own metatile") +eq(flat.elevation, 3, "neighbor pool flat tile keeps template elevation") +check(not raised.spawnElevation, "neighbor pool spawn update runs once") +GameVersion.set("firered") +T.finish("game3_object_spawn_elevation_2737_test") diff --git a/tests/engine/game3_rs_anim_names_test.lua b/tests/engine/game3_rs_anim_names_test.lua new file mode 100644 index 00000000..4a2b10a0 --- /dev/null +++ b/tests/engine/game3_rs_anim_names_test.lua @@ -0,0 +1,78 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq + +local Names = require("src.import.gba.rs.anim_names") +local Emerald = require("src.import.gba.anim_names_emerald") +local AnimCallbacks = require("src.core.game3.battle.anim_callbacks") +local AnimTasks = require("src.core.game3.battle.anim_tasks") +local Versions = require("src.import.gba.versions") + +local function count(t) + local n = 0 + for _ in pairs(t or {}) do n = n + 1 end + return n +end + +eq(count(Names.templates), 329, "329 RS sprite templates") +eq(count(Names.callbacks), 269, "269 RS sprite callbacks") +eq(count(Names.tasks), 208, "208 RS visual/sound tasks") + +local missCb, missTask = {}, {} +for key, name in pairs(Names.callbacks) do + local bound = AnimCallbacks.get(name) ~= AnimCallbacks.SimpleFadeOut + or AnimTasks.REGISTRY["_noGfx_" .. name] ~= nil + if not bound then missCb[#missCb + 1] = key .. ">" .. name end +end +for key, name in pairs(Names.tasks) do + if not AnimTasks.REGISTRY[name] then missTask[#missTask + 1] = key .. ">" .. name end +end +table.sort(missCb) +table.sort(missTask) +eq(table.concat(missCb, " "), "", "every RS callback resolves to a runtime callback") +eq(table.concat(missTask, " "), "", "every RS task resolves to a runtime task") + +local emTemplates = {} +for _, v in pairs(Emerald.templates) do emTemplates[v] = true end +local seen, dup = {}, {} +for key, name in pairs(Names.templates) do + if seen[name] then dup[#dup + 1] = name end + seen[name] = key + check(emTemplates[name], "RS template " .. key .. " carries an Emerald template name (" .. name .. ")") +end +eq(#dup, 0, "RS template canonical names are unique") + +local spot = { + callbacks = { + sub_80DC8F4 = "LeechLifeNeedle", sub_80DC068 = "RedX", sub_80DD8E8 = "RockTomb", + sub_80D1318 = "UproarRing", sub_80D2D68 = "JaggedMusicNote", AnimConfusionDuck = "ConfusionDuck", + sub_80CA9A8 = "sub_80CA9A8", + }, + tasks = { + sub_807BB88 = "StatsChange", sub_80D2CF8 = "UproarDistortion", sub_80E26BC = "ShakeBattleTerrain", + sub_80CE7E0 = "DoubleTeam", sub_812B2B8 = "sub_812B2B8", + }, + templates = { + gBattleAnimSpriteTemplate_83DA8F4 = "gRedXSpriteTemplate", + gBattleAnimSpriteTemplate_83DADA8 = "gRockTombRockSpriteTemplate", + gBattleAnimSpriteTemplate_83D79A4 = "gUproarRingSpriteTemplate", + gBattleAnimSpriteTemplate_83D7CC8 = "gJaggedMusicNoteSpriteTemplate", + gBattleAnimSpriteTemplate_83DAB10 = "gLeechLifeNeedleSpriteTemplate", + }, +} +for section, rows in pairs(spot) do + for key, want in pairs(rows) do eq(Names[section][key], want, section .. " " .. key) end +end + +local V = Versions.forGame("sapphire") +for i, n in pairs({ [0] = "PSN", "CONFUSION", "BRN", "INFATUATION", "SLP", "PRZ", "FRZ", "CURSED", "NIGHTMARE" }) do + eq(V.BATTLE_ANIM_STATUS_NAMES[i], "STATUS_" .. n, "RS status anim " .. i) +end +eq(V.BATTLE_ANIM_GENERAL_NAMES[7], "HELD_ITEM_EFFECT", "RS general anim 7 uses the runtime name") +eq(V.BATTLE_ANIM_GENERAL_NAMES[9], "FOCUS_BAND", "RS general anim 9 uses the runtime name") +eq(count(V.ANIM_CALLBACK_NAMES), 269, "RS callback address map built from syms") +eq(count(V.ANIM_TASK_NAMES), 208, "RS task address map built from syms") +eq(count(V.ANIM_TEMPLATE_NAMES), 329, "RS template address map built from syms") + +T.finish("game3_rs_anim_names_test") diff --git a/tests/engine/game3_rs_bag_row_layout_2727.lua b/tests/engine/game3_rs_bag_row_layout_2727.lua new file mode 100644 index 00000000..74e1c035 --- /dev/null +++ b/tests/engine/game3_rs_bag_row_layout_2727.lua @@ -0,0 +1,113 @@ +#!/usr/bin/env luajit + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local eq = T.eq + +local draws, images = {}, {} +_G.love = _G.love or {} +love.graphics = { + setColor = function() end, + rectangle = function() end, + newQuad = function() return {} end, + draw = function(img) images[#images + 1] = img.path end, +} + +local manifest = { + layout = "rs", + layers = {bg = {png = "bg.png", variants = {female = "bg_female.png"}}}, + sprites = {male = {anims = {}}, female = {anims = {}}}, + labels = {{male = "l_m", female = "l_f"}, {male = "l_m", female = "l_f"}, {male = "l_m", female = "l_f"}, + {male = "l_m", female = "l_f"}, {male = "l_m", female = "l_f"}}, + indicators = {idle = "indicator_0.png", selected = "indicator_1.png", + idleFemale = "indicator_0_female.png", selectedFemale = "indicator_1_female.png"}, + hm = "hm.png", number = "number.png", select = "select.png", +} +package.loaded["src.ui.game3.rse.scene_kit"] = { + manifest = function() return manifest end, + image = function(path) + if path and path:find("indicator", 1, true) then + return {path = path, getDimensions = function() return 8, 8 end} + end + end, + playSe = function() end, resetCaches = function() end, +} +package.loaded["src.ui.game3.frlg_font"] = { + STDPAL = {{1, 1, 1, 1}, {1, 0, 0, 1}, [8] = {0, 0, 0, 1}}, + COLOR = {NORMAL = {}}, + sync = function() end, + measure = function(s) return #s * 6 end, + draw = function(s, x, y, opts) draws[#draws + 1] = {s = s, x = x, y = y, w = opts and opts.maxWidth} end, +} +package.loaded["src.ui.game3.chrome"] = {stdFrame = function() end, dialogueFrame = function() end} +package.loaded["src.ui.game3.rs.menu_cursor"] = {draw = function() end} +package.loaded["src.core.game3.rom_text"] = {plain = function(key) return key end, overrides = {}} +package.loaded["src.core.game3.trig"] = {sin = function() return 0 end} +package.loaded["src.core.game3.bag"] = {} +local HM = {HM01 = 1} +local TM = {TM39 = 39, HM01 = 1} +local BERRY = {LIECHI = 36} +package.loaded["src.core.game3.items_data"] = { + isHm = function(id) return HM[id] ~= nil end, + tmNumber = function(id) return TM[id] end, + berryNumber = function(id) return BERRY[id] end, + toNumericId = function(id) return id end, +} +package.loaded["src.core.game3.pokemon"] = { + moveFromTmItem = function(id) return id end, + moveName = function(id) return ({TM39 = "ROCK TOMB", HM01 = "CUT"})[id] end, +} + +local M = require("src.ui.game3.rs.bag_menu") + +local function render(pocket, rows, gender, pocketIdx) + draws, images = {}, {} + local Bag = { + open = true, mode = "list", scroll = 0, cursor = 1, pocketIdx = pocketIdx or 1, + _session = {gender = gender}, + list = function() return rows end, + currentPocket = function() return pocket end, + } + Bag._rsBag = {pos = {}, frame = 0, gridPos = 1, lastMode = "list"} + M.draw(Bag) + return draws +end + +local function at(list, s, y) + for _, d in ipairs(list) do if d.s == s and (y == nil or d.y == y) then return d end end +end + +T.suite("rs bag row layout") + +local tm = render("TM_CASE", {{id = "TM39", name = "TM39", qty = 1, description = ""}, + {id = "HM01", name = "HM01", qty = 1, description = ""}}, 0, 4) +eq(at(tm, "39", 16) and at(tm, "39", 16).x, 120, "TM number x") +eq(at(tm, "ROCK TOMB", 16) and at(tm, "ROCK TOMB", 16).x, 136, "TM name x") +eq(at(tm, "ROCK TOMB", 16) and at(tm, "ROCK TOMB", 16).w, 78, "TM name width") +eq(at(tm, "×", 16) and at(tm, "×", 16).x, 214, "TM multiply sign x") +eq(at(tm, "1", 16) and at(tm, "1", 16).x, 226, "TM count right-aligned to 232") +eq(at(tm, "1", 32) and at(tm, "1", 32).x, 129, "HM number x") +eq(at(tm, "CUT", 32) and at(tm, "CUT", 32).x, 136, "HM name x") +eq(at(tm, "×", 32), nil, "HM row has no quantity") + +local berry = render("BERRY_POUCH", {{id = "LIECHI", name = "LIECHI", qty = 10, description = ""}}, 1, 5) +eq(at(berry, "36", 16) and at(berry, "36", 16).x, 120, "berry number x") +eq(at(berry, "LIECHI", 16) and at(berry, "LIECHI", 16).w, 72, "berry name width") +eq(at(berry, "×", 16) and at(berry, "×", 16).x, 208, "berry multiply sign x") +eq(at(berry, "10", 16) and at(berry, "10", 16).x, 220, "berry count right-aligned to 232") + +local items = render("ITEMS", {{id = "POTION", name = "POTION", qty = 5, description = ""}}, 0, 1) +eq(at(items, "POTION", 16) and at(items, "POTION", 16).x, 112, "item name x") +eq(at(items, "×", 16) and at(items, "×", 16).x, 214, "item multiply sign x") +eq(at(items, "5", 16) and at(items, "5", 16).x, 226, "item count right-aligned to 232") + +render("ITEMS", {}, 1, 2) +local fem = table.concat(images, ",") +eq(fem, "indicator_0_female.png,indicator_1_female.png,indicator_0_female.png,indicator_0_female.png,indicator_0_female.png", + "female indicator dots use female bake") +render("ITEMS", {}, 0, 2) +eq(table.concat(images, ","), "indicator_0.png,indicator_1.png,indicator_0.png,indicator_0.png,indicator_0.png", + "male indicator dots use male bake") + +T.finish("rs bag row layout") diff --git a/tests/engine/game3_rs_battle_menu_frames_2729.lua b/tests/engine/game3_rs_battle_menu_frames_2729.lua new file mode 100644 index 00000000..1a81c8a8 --- /dev/null +++ b/tests/engine/game3_rs_battle_menu_frames_2729.lua @@ -0,0 +1,62 @@ +#!/usr/bin/env luajit + +package.path = "./?.lua;./?/init.lua;" .. package.path +require("tests.fixture_data.game3_map_sections").install() + +local T = require("tests.harness") +local eq = T.eq +require("tests.game3_cache").stubSpeciesNames() + +require("src.core.GameVersion").set("firered") +package.loaded["src.core.game3.audio"] = { + playSe = function() end, playCry = function() end, + playSong = function() end, stopAll = function() end, +} +package.loaded["src.core.game3.rom_text"] = { + plain = function(key) return key end, box = function(key) return key end, + ascii = function(key) return key end, has = function() return true end, + key = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end, + at = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end, + count = function() return 0 end, list = function() return {} end, + lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end, +} + +local drawn = {} +package.loaded["src.ui.game3.chrome"] = { + _frameType = 0, + userFrame = function(_, x, y, w, h) drawn[#drawn + 1] = { x, y, w, h } end, +} + +local BattleChrome = require("src.ui.game3.battle_chrome") +local Ui = require("src.core.game3.battle.ui") +local nativeLayout = "rs" +package.loaded["src.core.game3.profile"] = { + forSession = function() return { font = { nativeLayout = nativeLayout } } end, +} +BattleChrome._manifest = { layout = "rse" } + +local function frames(mode) + drawn = {} + Ui.drawMenuFrames(mode) + return drawn +end + +local function same(a, b) + if #a ~= #b then return false end + for i = 1, #a do + for j = 1, 4 do if a[i][j] ~= b[i][j] then return false end end + end + return true +end + +nativeLayout = "rs" +local mv = frames("moves") +eq(same(mv, { { 1, 15, 20, 4 }, { 23, 15, 6, 4 } }), true, "RS move menu frames follow menu_map.bin") +eq((mv[1] and (mv[1][1] + mv[1][3]) * 8) or -1, 168, "RS left move box content ends at x=168") +eq(same(frames("menu"), { { 18, 15, 11, 4 } }), true, "RS action menu frame follows menu_map.bin") + +nativeLayout = nil +eq(same(frames("moves"), { { 1, 15, 18, 4 }, { 21, 15, 8, 4 } }), true, "Emerald move menu frames unchanged") +eq(same(frames("menu"), { { 16, 15, 13, 4 } }), true, "Emerald action menu frame unchanged") + +T.finish("game3 RS battle menu frames #2729") diff --git a/tests/engine/game3_rse_field_poison_script_2736.lua b/tests/engine/game3_rse_field_poison_script_2736.lua new file mode 100644 index 00000000..5edcd094 --- /dev/null +++ b/tests/engine/game3_rse_field_poison_script_2736.lua @@ -0,0 +1,108 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq +love = love or require("tests.love_stub") + +local noop = function() end +local profileField +local started = {} +local vm = { active = false } + +package.loaded["src.core.game3.pokemon"] = { + FRIENDSHIP_EVENT_WALKING = 0, + FRIENDSHIP_EVENT_FAINT_OUTSIDE_BATTLE = 1, + adjustFriendship = noop, + currentMapSec = function() return 0 end, + displayMonName = function(mon) return mon.name or "MON" end, +} +package.loaded["src.core.game3.rom_text"] = { box = function(key) return key end } +package.loaded["src.core.game3.field_semantics"] = { + var = function(_, name) return name end, + getVar = function() return 0 end, + setVar = noop, +} +package.loaded["src.core.game3.field_modules"] = { enabled = function() return false end } +package.loaded["src.core.game3.profile"] = { + family = function() return profileField and "rse" or "frlg" end, + forSession = function() return { field = profileField } end, +} +package.loaded["src.core.game3.capabilities"] = { gate = function() return false end } +package.loaded["src.core.game3.rs.rematch"] = { enabled = function() return false end } +package.loaded["src.core.game3.faraway_island"] = { updateStepCounter = noop } +package.loaded["src.core.game3.mystery_gift"] = { incrementNewsStepCounter = noop } +package.loaded["src.core.game3.rse.init"] = { call = noop, isRse = function() return profileField ~= nil end } +package.loaded["src.core.game3.player"] = { cellX = 0, cellY = 0 } +package.loaded["src.core.game3.forced_movement"] = { + isForced = function() return false end, + isForcedMovementTile = function() return false end, +} +package.loaded["src.core.game3.se_ids"] = { SE_FIELD_POISON = 1 } +package.loaded["src.core.game3.audio"] = { playSe = noop, fadeOutBgm = noop } +package.loaded["src.core.game3.special_scene_rse"] = { countSSTidalStep = function() return false end } +package.loaded["src.core.game3.safari"] = { takeStep = function() return false end } +package.loaded["src.core.game3.daycare"] = { step = function() return nil end } +package.loaded["src.core.game3.scripting.space"] = { + vm = vm, + scriptKey = function(name) return name end, + startScript = function(key) + started[#started + 1] = key + vm.active = true + return true + end, +} + +local StepEvents = require("src.core.game3.step_events") + +local function poisonedSession() + return { + vars = { poisonSteps = 3 }, + party = { + { name = "TORCHIC", hp = 1, status = "PSN" }, + { name = "MUDKIP", hp = 20 }, + }, + } +end + +local function types() + local out = {} + for i, ev in ipairs(StepEvents._queue) do out[i] = ev.type end + return table.concat(out, ",") +end + +for _, case in ipairs({ + { name = "emerald", field = require("src.core.game3.profiles.emerald.field"), script = "EventScript_FieldPoison" }, + { name = "rs", field = require("src.core.game3.profiles.rs.field"), script = "gUnknown_081A14B8" }, +}) do + StepEvents.flush() + started = {} + vm.active = false + profileField = case.field + eq(case.field.fieldPoisonScript, case.script, case.name .. " profile names the cart field poison script") + local session = poisonedSession() + StepEvents.onStepTaken(session, {}) + local mon = session.party[1] + eq(mon.hp, 0, case.name .. ": poison step takes the last HP") + eq(mon.status, "PSN", case.name .. ": status is left for the script's faint check") + eq(types(), "field_poison_script", case.name .. ": only the field poison script is queued") + StepEvents.update(1 / 60, {}) + eq(started[1], case.script, case.name .. ": the step starts " .. case.script) + check(StepEvents.busy(), case.name .. ": step events stay busy while the script runs") + StepEvents.update(1 / 60, {}) + check(StepEvents.busy(), case.name .. ": still busy on the next frame") + vm.active = false + for _ = 1, 8 do StepEvents.update(1 / 60, {}) end + check(not StepEvents.busy(), case.name .. ": step events release once the script ends") +end + +StepEvents.flush() +started = {} +profileField = nil +local session = poisonedSession() +StepEvents.onStepTaken(session, {}) +eq(types(), "poison_faint,poison_white_out", "firered keeps the Lua faint + white out events") +eq(session.party[1].status, nil, "firered clears status at faint time") +eq(#started, 0, "firered starts no script") +StepEvents.flush() + +T.finish("game3_rse_field_poison_script_2736") diff --git a/tests/engine/game3_transition_world_canvas_2731_test.lua b/tests/engine/game3_transition_world_canvas_2731_test.lua new file mode 100644 index 00000000..118fc14d --- /dev/null +++ b/tests/engine/game3_transition_world_canvas_2731_test.lua @@ -0,0 +1,95 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path +local T = require("tests.harness") +local check = T.check +love = require("tests.love_stub") +local G = love.graphics + +local canvasA = G.newCanvas(240, 160) +local canvasB = G.newCanvas(240, 160) +canvasA.name, canvasB.name = "A", "B" + +local function noop() end +local Renderer = { canvas = G.newCanvas(240, 160), worldCanvas = canvasA } +function Renderer:init() end +function Renderer:setUISize() end +function Renderer:beginFrame() end +function Renderer:worldViewSize() return 240, 160 end +function Renderer:beginWorldPass() + self.worldActive = true + G.setCanvas(self.worldCanvas) +end +function Renderer:exchangeWorldCanvas(current, replacement) + if self.worldCanvas ~= current then return false end + self.worldCanvas = replacement + return true +end +function Renderer:endWorldPass() G.setCanvas(self.canvas) end +function Renderer:endFrame() end + +local seen = {} +local Transition = { + isActive = function() return true end, + drawWorld = function(canvas) + seen.handed = canvas + seen.bound = G.getCanvas() + end, +} + +local stubs = { + ["src.render.Renderer"] = Renderer, + ["src.render.PaletteFX"] = { mode = "gbc" }, + ["src.render.Tilt"] = { active = function() return false end }, + ["src.ui.game3.help_system"] = { isOpen = function() return false end }, + ["src.core.game3.battle"] = { isActive = function() return false end }, + ["src.core.game3.bg"] = { hasVisible = function() return false end }, + ["src.core.game3.oam"] = { + resetFrame = noop, setLayer = function() return "world" end, animateSprites = noop, + buildOamBuffer = noop, flush = noop, flushPriority = noop, + }, + ["src.core.game3.field_view"] = { + draw = function(_, _, _, opts) + local cur = G.getCanvas() + if opts.exchangeCanvas and opts.exchangeCanvas(cur, canvasB) then + G.setCanvas(canvasB) + end + end, + }, + ["src.core.game3.battle_transition"] = Transition, +} +local saved = {} +for name, mod in pairs(stubs) do + saved[name] = package.loaded[name] + package.loaded[name] = mod +end + +package.loaded["src.core.game3.display"] = nil +local Display = require("src.core.game3.display") +Display.setUiRenderer(noop) +Display.planesBroken = false +Display.present({ options = {} }, 240, 160) + +check(not Display.planesBroken, "plane present ran without falling back") +check(Renderer.worldCanvas == canvasB, "field pass exchanged the world canvas") +check(seen.handed == canvasB, "drawWorld is handed the exchanged world canvas") +check(seen.bound == canvasB, "world canvas still bound after the field pass pop") + +for name in pairs(stubs) do package.loaded[name] = saved[name] end +package.loaded["src.core.game3.display"] = nil + +local BT = require("src.core.game3.battle_transition") +local Pal = require("src.core.game3.pal_fade") +local drawnOn = {} +local rect = G.rectangle +G.rectangle = function() drawnOn[#drawnOn + 1] = G.getCanvas() end +BT._active, BT._opts, BT._fx = true, {}, nil +BT._pal = Pal.new() +BT._pal.slots[0].y = 8 +G.setCanvas(canvasA) +BT.drawWorld(canvasB, 240, 160) +G.rectangle = rect +check(#drawnOn > 0, "intro overlay drew") +check(drawnOn[1] == canvasB, "drawWorld draws the intro overlay into the canvas it is handed") +check(G.getCanvas() == canvasA, "drawWorld restores the caller's canvas") +BT._active, BT._pal, BT._worldDrawn = false, nil, false + +T.finish() diff --git a/tests/engine/gen2_sfx_prewarm_bug2732.lua b/tests/engine/gen2_sfx_prewarm_bug2732.lua new file mode 100644 index 00000000..494bbe95 --- /dev/null +++ b/tests/engine/gen2_sfx_prewarm_bug2732.lua @@ -0,0 +1,248 @@ +-- engine/battle_anims/anim_commands.asm:1191 +package.path = "./?.lua;./?/init.lua;" .. package.path + +local S = require("tests.harness").suite("gen2 sfx prewarm 2732") +local check, eq = S.check, S.eq + +love = require("tests.love_stub") +local ffi = require("ffi") + +local cache = os.getenv("CRYSTAL_CACHE") +if not cache then + local home = os.getenv("HOME") or "" + cache = home .. "/Library/Application Support/LOVE/crystal-dev/crystal" +end +local gen = cache .. "/data/generated/" +local audioFile = io.open(gen .. "audio.lua", "r") +local animFile = io.open(gen .. "battle_anims.lua", "r") +local progFile = io.open(cache .. "/assets/generated/audio/programs.bin", "rb") +if not (audioFile and animFile and progFile) then + for _, f in ipairs({ audioFile or false, animFile or false, progFile or false }) do + if f then f:close() end + end + print("[skip] no crystal cache at " .. cache) + os.exit(0) +end +audioFile:close() +animFile:close() +local progBytes = progFile:read("*a") +progFile:close() + +local audio = assert(loadfile(gen .. "audio.lua"))() +local anims = assert(loadfile(gen .. "battle_anims.lua"))() +local constants = loadfile(gen .. "constants.lua") +constants = constants and constants() or nil +if audio.runtime ~= true or not audio.sfx or not audio.sfx.Sfx_Item then + print("[skip] crystal cache has no runtime audio") + os.exit(0) +end +love.filesystem.write(audio.programFile, progBytes) + +local SoundData = {} +SoundData.__index = SoundData +function SoundData:setSample(index, a, b) + if b == nil then + self.buf[index * self.channels] = a * 32767 + else + self.buf[index * self.channels + (a - 1)] = b * 32767 + end +end +function SoundData:getSample(index, channel) + return self.buf[index * self.channels + ((channel or 1) - 1)] / 32767 +end +function SoundData:getFFIPointer() return self.buf end +function SoundData:getSampleCount() return self.samples end +function SoundData:getSampleRate() return self.rate end +function SoundData:getBitDepth() return 16 end +function SoundData:getChannelCount() return self.channels end +love.sound.newSoundData = function(samples, rate, _, channels) + return setmetatable({ samples = samples, rate = rate, channels = channels, + buf = ffi.new("int16_t[?]", samples * channels) }, SoundData) +end + +local Source = {} +Source.__index = Source +function Source:play() self.plays = (self.plays or 0) + 1 end +function Source:stop() self.playing = false end +function Source:isPlaying() return self.playing == true end +function Source:setVolume() end +function Source:setPitch(p) self.pitch = p end +function Source:getPitch() return self.pitch or 1 end +function Source:getDuration() return self.sd.samples / self.sd.rate end +function Source:tell() return 0 end +love.audio = { + newSource = function(sd) return setmetatable({ sd = sd }, Source) end, +} +love.timer.getTime = os.clock + +local channels = {} +local function channel(name) + local ch = channels[name] + if ch then return ch end + local items = {} + ch = {} + function ch:push(v) items[#items + 1] = v end + function ch:pop() return table.remove(items, 1) end + function ch:clear() for i = #items, 1, -1 do items[i] = nil end end + function ch:getCount() return #items end + channels[name] = ch + return ch +end +love.thread = { + newThread = function() + return { start = function() end, getError = function() return nil end } + end, + getChannel = channel, +} + +local ChipSynth = require("src.core.ChipSynth") +local ChipAudio = require("src.core.ChipAudio") +local Sound = require("src.core.Sound") +local UI = require("src.ui.gen2.BattleState") + +local inWorker = false +local mainRenders = 0 +local realRender = ChipSynth.renderEffectData +ChipSynth.renderEffectData = function(...) + if not inWorker then mainRenders = mainRenders + 1 end + return realRender(...) +end + +local function workerStep() + local cmdCh, fxCh = channel("chipaudio_cmd"), channel("chipaudio_fx") + local did = false + inWorker = true + while true do + local req = cmdCh:pop() + if not req then break end + if req.cmd == "effect" then + did = true + local sd = ChipSynth.renderEffectData({ audio = req.audio }, req.header, + req.options or {}) + fxCh:push({ key = req.key, epoch = req.epoch, sd = sd }) + end + end + inWorker = false + return did +end + +local function pump() + if ChipAudio.pumpEffects then ChipAudio.pumpEffects() end + ChipAudio.update() +end + +local function drain() + for _ = 1, 400 do + pump() + if not workerStep() then + pump() + local st = ChipAudio._effectStateForTest() + if (st.queued or 0) == 0 and (st.inFlight or 0) == 0 then return end + end + end +end + +local function resetStats() + mainRenders = 0 + if ChipAudio.resetSyncStats then ChipAudio.resetSyncStats() end +end + +local data = { audio = audio, gen2BattleAnims = anims, + gen2Constants = constants } + +if jit and jit.off then jit.off() end +local before = os.clock() +realRender(data, audio.sfx.Sfx_Item, { frequencyOffset = 0, frameTicks = 0x100 }) +local syncMs = (os.clock() - before) * 1000 +check(Sound.prewarmSfx(data, "Sfx_Item"), "Sfx_Item prewarm queued") +drain() +resetStats() +before = os.clock() +check(Sound.play(data, "Sfx_Item") ~= nil, "Sfx_Item plays") +local warmMs = (os.clock() - before) * 1000 +print(("Sfx_Item first play, jit off: sync render %.1f ms, prewarmed %.1f ms") + :format(syncMs, warmMs)) +eq(mainRenders, 0, "prewarmed Sfx_Item: no main-thread render") + +local enemy = { species = "RATTATA", moves = { + { id = "TACKLE" }, { id = "TAIL_WHIP" }, { id = "BITE" } } } +local player = { species = "CYNDAQUIL", moves = { + { id = "EMBER" }, { id = "SMOKESCREEN" }, { id = "QUICK_ATTACK" }, + { id = "LEER" } } } +local battle = { wild = true, enemy = enemy, player = player, + party = { player }, enemyParty = { enemy } } + +local s = UI.new({ data = data, options = {}, save = {} }, { battle = battle }) +drain() +resetStats() + +local played = {} +local realStereo = Sound.playStereo +Sound.playStereo = function(d, name) + played[#played + 1] = name + return realStereo(d, name) +end +for _, turn in ipairs({ { player, "player" }, { enemy, "enemy" } }) do + for _, move in ipairs(turn[1].moves) do + if s:animForMove(move.id, turn[2]) then + for _ = 1, 2000 do + if not s.anim:step() then break end + end + end + s:playHitSound(10) + end +end +Sound.playStereo = realStereo + +check(#played >= 5, "the turn's anims played their sounds (" .. #played .. ")") +eq(mainRenders, 0, "zero main-thread ChipSynth renders during the turn") +local stats = ChipAudio.stats() +eq(stats.syncRenders, 0, "zero main-thread sfx renders during the turn" + .. (stats.syncRenderLast and (" (last " .. stats.syncRenderLast .. ")") or "")) +check((stats.prewarmHits or 0) > 0, "plays took prewarmed PCM") +check(stats.line and stats.line:find("sync=0/", 1, true) ~= nil, + "stats.line carries the sync counters") + +resetStats() +check(Sound.play(data, "Sfx_GetTm") ~= nil, "cold Sfx_GetTm plays") +eq(ChipAudio.stats().syncRenders, 1, "a cold play is counted as a sync render") +check((ChipAudio.stats().syncRenderMsMax or 0) > 0, "its render time is recorded") + +local Game2 = require("src.core.Game2") +Game2.prewarmSessionSfx({ data = data }) +drain() +local function mon(species, moves) + local m = { species = species, moves = {} } + for _, id in ipairs(moves) do m.moves[#m.moves + 1] = { id = id } end + return m +end +local party = { + mon("TYPHLOSION", { "FLAMETHROWER", "THUNDERPUNCH", "EARTHQUAKE", "SWIFT" }), + mon("FERALIGATR", { "SURF", "ICE_PUNCH", "CRUNCH", "SLASH" }), + mon("MEGANIUM", { "RAZOR_LEAF", "BODY_SLAM", "SYNTHESIS", "REFLECT" }), + mon("AMPHAROS", { "THUNDER", "FIRE_PUNCH", "THUNDER_WAVE", "LIGHT_SCREEN" }), + mon("PIDGEOT", { "FLY", "WING_ATTACK", "MIRROR_MOVE", "QUICK_ATTACK" }), + mon("GYARADOS", { "HYDRO_PUMP", "HYPER_BEAM", "DRAGON_RAGE", "BITE" }), +} +local enemyParty = { + mon("ALAKAZAM", { "PSYCHIC", "RECOVER", "REFLECT", "FUTURE_SIGHT" }), + mon("EXEGGUTOR", { "SOLARBEAM", "PSYCHIC", "SLEEP_POWDER", "EXPLOSION" }), + mon("SLOWBRO", { "SURF", "AMNESIA", "PSYCHIC", "CURSE" }), + mon("JYNX", { "ICE_BEAM", "LOVELY_KISS", "PSYCHIC", "DREAM_EATER" }), + mon("MR__MIME", { "BARRIER", "PSYBEAM", "SUBSTITUTE", "ENCORE" }), + mon("XATU", { "PSYCHIC", "FLY", "CONFUSE_RAY", "NIGHT_SHADE" }), +} +local trainer = { player = party[1], enemy = enemyParty[1], + party = party, enemyParty = enemyParty } +local names = UI.battleSfxNames(data, anims, trainer) +check(#names + #Game2.SESSION_SFX > 64, "6v6 list plus session set exceeds the cap (" + .. #names .. " + " .. #Game2.SESSION_SFX .. ")") +for _, name in ipairs(names) do Sound.prewarmSfx(data, name) end +drain() +for _, name in ipairs({ "Sfx_KeyItem", "Sfx_ReadText2", "Sfx_Menu", "Sfx_Save" }) do + resetStats() + check(Sound.play(data, name) ~= nil, name .. " plays") + eq(mainRenders, 0, name .. " stays prewarmed after a 6v6 battle prewarm") +end + +S.finish() diff --git a/tests/engine/host_restart_android_bug575.lua b/tests/engine/host_restart_android_bug575.lua index 8eba0487..dca731dd 100644 --- a/tests/engine/host_restart_android_bug575.lua +++ b/tests/engine/host_restart_android_bug575.lua @@ -25,6 +25,7 @@ local restartCalls = 0 love.system = love.system or {} love.system.getOS = function() return osName end love.system.restartApp = function() restartCalls = restartCalls + 1 return true end +_G.POKEPORT_LOOP_RESTART = true HostShell.restart() eq(restartCalls, 0, "Android never schedules the blocked restartApp relaunch") diff --git a/tests/engine/hostshell_restart_single_spawn_2740.lua b/tests/engine/hostshell_restart_single_spawn_2740.lua new file mode 100644 index 00000000..991b2dc5 --- /dev/null +++ b/tests/engine/hostshell_restart_single_spawn_2740.lua @@ -0,0 +1,85 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq +_G.love = require("tests.love_stub") + +local spawns = 0 +package.loaded["src.core.WinApi"] = { + spawn = function() spawns = spawns + 1 return true end, + dirOf = function(p) return p:match("^(.*)\\") or p end, + modulePath = function() return "C:\\Games\\gen1recomp.exe" end, +} +package.loaded["src.core.SessionLifecycle"] = setmetatable({}, { + __index = function() return function() end end, +}) + +love.system.getOS = function() return "Windows" end +love.filesystem.isFused = function() return true end +love.filesystem.getSource = function() return "C:\\Games\\gen1recomp.exe" end + +local queue = {} +love.event = love.event or {} +love.event.quit = function(...) queue[#queue + 1] = { n = select("#", ...), arg = (...) } end + +local loaded, err = pcall(dofile, "main.lua") +check(loaded, "main.lua loads headless: " .. tostring(err)) + +local HostShell = require("src.core.HostShell") + +local function upvalue(fn, name) + for i = 1, 200 do + local n = debug.getupvalue(fn, i) + if n == nil then return nil end + if n == name then return i end + end +end + +local function setLocal(name, value) + local fn = love.quit + local i = upvalue(fn, name) + if i then + debug.setupvalue(fn, i, value) + return true + end + local ret = upvalue(fn, "returnToLauncher") + local _, rtl = debug.getupvalue(fn, ret) + i = upvalue(rtl, name) + if i then debug.setupvalue(rtl, i, value) return true end + return false +end + +local function runQuits() + local exited = false + while #queue > 0 and not exited do + table.remove(queue, 1) + if not love.quit() then exited = true end + end + return exited +end + +local function reset() + spawns = 0 + for i = #queue, 1, -1 do queue[i] = nil end + HostShell.restarting = false + setLocal("quitToLauncher", false) + setLocal("processEnded", false) + love.filesystem.remove("relaunch_to_launcher.txt") +end + +reset() +check(setLocal("Game", {}), "a game session is on screen") +HostShell.restart() +eq(spawns, 1, "restartWithMods spawns the new process") +check(runQuits(), "the old process exits") +eq(spawns, 1, "and love.quit does not spawn a second instance") +check(love.filesystem.getInfo("relaunch_to_launcher.txt") ~= nil, + "the relaunch still lands in the launcher") + +reset() +setLocal("Game", {}) +love.event.quit() +check(runQuits(), "closing a game window ends this process") +eq(spawns, 1, "and relaunches the launcher exactly once") + +T.finish("hostshell_restart_single_spawn_2740") diff --git a/tests/engine/launcher_settings_persist_2740.lua b/tests/engine/launcher_settings_persist_2740.lua new file mode 100644 index 00000000..e3bed5c1 --- /dev/null +++ b/tests/engine/launcher_settings_persist_2740.lua @@ -0,0 +1,175 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq +love = love or require("tests.love_stub") + +love.data = love.data or {} +love.data.hash = function(_, data) return "h" .. #data .. ":" .. data:sub(1, 24) end +love.data.encode = function(_, _, digest) return (digest:gsub(".", function(c) + return ("%02x"):format(c:byte()) +end)) end + +local SaveData = require("src.core.SaveData") +local Serializer = require("src.core.SaveSerializer") + +local function disk() + return Serializer.decode(love.filesystem.read("options.lua") or "") or {} +end + +local function externalWrite(patch) + local tree = disk() + for k, v in pairs(patch) do tree[k] = v end + love.filesystem.write("options.lua", Serializer.encode(tree)) +end + +SaveData.saveOptions(SaveData.defaultOptions()) +local stale = SaveData.loadOptions() +externalWrite({ autoReimport = true, splashVideo = false, themeVideoBg = false, + reduceMotion = true }) + +stale.textSpeed = 1 +SaveData.saveLiveOptions({ version = "red", options = stale }) +local after = disk() +eq(after.autoReimport, true, "Gen 1 in-game save keeps autoReimport") +eq(after.splashVideo, false, "Gen 1 in-game save keeps Splash Video OFF") +eq(after.themeVideoBg, false, "Gen 1 in-game save keeps Theme Video BG OFF") +eq(after.reduceMotion, true, "Gen 1 in-game save keeps Reduce Motion") +eq(after.textSpeed, 1, "and the in-game change still lands") + +local Game3 = require("src.core.Game3") +SaveData.saveOptions(SaveData.defaultOptions()) +local stale3 = SaveData.loadOptions() +externalWrite({ autoReimport = true, splashVideo = false, themeVideoBg = false }) +stale3.musicVol = 3 +Game3.writeOptions({ options = stale3 }) +after = disk() +eq(after.autoReimport, true, "Gen 3 writeOptions keeps autoReimport") +eq(after.splashVideo, false, "Gen 3 writeOptions keeps Splash Video OFF") +eq(after.themeVideoBg, false, "Gen 3 writeOptions keeps Theme Video BG OFF") +eq(after.musicVol, 3, "and the Gen 3 change still lands") + +local LauncherSettings = require("src.import.LauncherSettings") + +local function findRow(model, label) + for _, section in ipairs(model.sections) do + for _, row in ipairs(section.rows) do + if row.label == label then return row end + end + end +end + +SaveData.saveOptions({ lastVersion = "red", splashVideo = true, reduceMotion = false }) +local model = LauncherSettings.open(nil, "red") +externalWrite({ lastVersion = "blue", autoReimport = true }) +local splash = findRow(model, "Splash Video") +check(splash.step(), "Splash Video row steps") +model.save() +after = disk() +eq(after.splashVideo, false, "the settings panel writes its own change") +eq(after.lastVersion, "blue", "and keeps a launcher key another writer changed") +eq(after.autoReimport, true, "and keeps autoReimport another writer set") + +local realWrite = love.filesystem.write +love.filesystem.write = function(name, ...) + if tostring(name):match("^options%.lua") then return false, "sharing violation" end + return realWrite(name, ...) +end +local motion = findRow(model, "Reduce Motion") +eq(motion.value(), "OFF", "Reduce Motion starts OFF") +motion.step() +local saved = model.save() +love.filesystem.write = realWrite +check(saved == false, "a failed options write reports failure") +eq(motion.value(), "OFF", "and the row shows what the file holds, not the lost value") +check(type(model.saveError) == "string", "and the panel carries a visible save error") +eq(disk().reduceMotion, false, "the file still holds OFF") + +local RomSources = require("src.import.RomSources") +local RomArchive = require("src.import.RomArchive") + +local rom = ("R"):rep(1048576) +local romSha = love.data.encode("string", "hex", love.data.hash("sha1", rom)) + +local mounts = {} +local fs = love.filesystem +local baseMount, baseUnmount, baseItems, baseInfo, baseRead = + fs.mount, fs.unmount, fs.getDirectoryItems, fs.getInfo, fs.read +fs.newFileData = function(data, name) return { data = data, name = name } end +fs.mount = function(fd, point) + if type(point) == "string" and point:match("^_rom_") then + mounts[point] = fd + return true + end + return baseMount(fd, point) +end +fs.unmount = function(fd) + for point, m in pairs(mounts) do if m == fd then mounts[point] = nil return true end end + return baseUnmount(fd) +end +local function inner(path) + local point, rest = tostring(path):match("^(_rom_[%w_]+)/?(.*)$") + if not point or not mounts[point] then return nil end + return point, rest +end +fs.getDirectoryItems = function(path) + local point, rest = inner(path) + if point then + if rest == "" then + return { point:match("probe") and "probe" or "Pokemon Red.gb" } + end + return {} + end + return baseItems(path) +end +fs.getInfo = function(path, ...) + local point, rest = inner(path) + if point then + if rest == "" then return { type = "directory" } end + return { type = "file", size = rest == "probe" and 2 or #rom } + end + return baseInfo(path, ...) +end +fs.read = function(path, ...) + local point, rest = inner(path) + if point then return rest == "probe" and "ok" or rom end + return baseRead(path, ...) +end +RomArchive._resetForTests() + +local tmp = os.tmpname() +local zipPath = tmp .. "-red.zip" +local f = assert(io.open(zipPath, "wb")) +f:write("PK\3\4" .. ("z"):rep(4096)) +f:close() + +RomSources.remember("red", { sha1 = romSha, path = zipPath, at = 1 }) +local cand = RomSources.candidate("red", false) +check(cand ~= nil and not cand.pick and cand.path == zipPath, + "a zip source whose inner ROM matches the record is a re-import candidate") + +local rawPath = tmp .. "-red.gb" +f = assert(io.open(rawPath, "wb")) +f:write(rom) +f:close() +RomSources.remember("red", { sha1 = romSha, path = rawPath, at = 1 }) +cand = RomSources.candidate("red", false) +check(cand ~= nil and not cand.pick and cand.path == rawPath, "a raw source still matches") + +os.remove(rawPath) +cand = RomSources.candidate("red", false) +check(cand ~= nil and cand.pick == true, "a moved or deleted source asks for the ROM instead of nothing") +eq(cand and cand.missing, "missing", "and says the file is gone") + +f = assert(io.open(rawPath, "wb")) +f:write(("X"):rep(1048576)) +f:close() +cand = RomSources.candidate("red", false) +check(cand ~= nil and cand.pick == true and cand.missing == "changed", + "a source that no longer matches asks for the ROM too") + +os.remove(rawPath) +os.remove(zipPath) +os.remove(tmp) + +T.finish("launcher_settings_persist_2740") diff --git a/tests/engine/save_editor_unown_form_2726.lua b/tests/engine/save_editor_unown_form_2726.lua new file mode 100644 index 00000000..2b899ad8 --- /dev/null +++ b/tests/engine/save_editor_unown_form_2726.lua @@ -0,0 +1,220 @@ +package.path = package.path .. ";./?.lua;./?/init.lua;./tools/save-editor/?.lua;./tools/save-editor/panels/?.lua" +love = require("tests.love_stub") +local T = require("tests.harness") +local bit = require("bit") +local Version = require("src.core.GameVersion") +local Serializer = require("src.core.SaveSerializer") +local K = require("tests.save_compat._codec") +local D = require("tests.save_compat._gen3_decode") +local G3 = require("tests.fixtures.save.gen3_build") +local G2 = require("tests.fixtures.save.gen2_build") +local Gen2Save = require("src.save_convert.Gen2Save") +local PokemonG3 = require("src.core.game3.pokemon") +local Unown = require("src.core.gen2.Unown") +local Gen, Ops, MonOps = require("Gen"), require("Ops"), require("MonOps") +local State = require("State") +local Legality = require("Legality") + +local function shinyOf(p, tid, sid) + return bit.bxor(bit.bxor(tid, sid), bit.bxor(math.floor(p / 65536), p % 65536)) < 8 +end + +do + local rng = 2726 + local function rand(n) + rng = (rng * 1103515245 + 12345) % 2147483648 + return rng % n + end + local bad = 0 + for _ = 1, 120 do + local pid = rand(65536) * 65536 + rand(65536) + local tid, sid = rand(65536), rand(65536) + local nature = pid % 25 + for letter = 0, 27 do + for _, shiny in ipairs({ false, true }) do + local p = MonOps.unownPid(pid, letter, nature, shiny, tid, sid) + if not (p and PokemonG3.unownLetter(p) == letter and p % 25 == nature and shinyOf(p, tid, sid) == shiny) then + bad = bad + 1 + if bad <= 5 then + T.check(false, ("unownPid pid=%08x tid=%d sid=%d letter=%d shiny=%s -> %s") + :format(pid, tid, sid, letter, tostring(shiny), tostring(p))) + end + end + end + end + end + T.eq(bad, 0, "gen3 unownPid keeps nature and shininess for every letter") + for tsv89 = 0, 3 do + for letter = 0, 27 do + local p = MonOps.unownPid(0x12345678, letter, 7, true, tsv89 * 256, 0) + T.check(p and PokemonG3.unownLetter(p) == letter, ("shiny letter %d reachable with tsv bits %d"):format(letter, tsv89)) + end + end + local rewritten = 0 + for _ = 1, 300 do + local pid = rand(65536) * 65536 + rand(65536) + local tid, sid = rand(65536), rand(65536) + if MonOps.unownPid(pid, PokemonG3.unownLetter(pid), pid % 25, shinyOf(pid, tid, sid), tid, sid) ~= pid then + rewritten = rewritten + 1 + end + local low = rand(65536) + local spid = bit.bxor(low, bit.bxor(tid, sid), rand(8)) * 65536 + low + if MonOps.unownPid(spid, PokemonG3.unownLetter(spid), spid % 25, true, tid, sid) ~= spid then + rewritten = rewritten + 1 + end + end + T.eq(rewritten, 0, "gen3 unownPid leaves a PID that already fits unchanged (plain and shiny)") +end + + +local function g3state(version, mons) + Version.set(version) + local w = G3.base(version) + G3.setParty(w, mons) + local s = State.new() + s.save, s.version = assert(K.import(3, version, G3.emit(w))), version + s.save.pcItems = s.save.pcItems or {} + Gen.ensureBoxes(s.save) + return s +end + +local UNOWN_F = 0x00000105 +assert(PokemonG3.unownLetter(UNOWN_F) == 5) + +do + local s = g3state("firered", { G3.mon({ species = 201, pid = UNOWN_F, tid = 0x1234, sid = 0x5678 }) }) + local mon = s.save.party[1] + T.eq(PokemonG3.unownLetter(mon.personality), 5, "fixture unown is F") + local natureBefore = mon.personality % 25 + MonOps.setNature(s.data, mon, (natureBefore + 3) % 25, 3) + T.eq(PokemonG3.unownLetter(mon.personality), 5, "setNature keeps unown letter") + T.eq(mon.personality % 25, (natureBefore + 3) % 25, "setNature still sets the nature") + MonOps.setShiny(s.data, mon, true, 3) + T.eq(PokemonG3.unownLetter(mon.personality), 5, "setShiny(true) keeps unown letter") + T.check(shinyOf(mon.personality, 0x1234, 0x5678), "setShiny(true) makes the PID shiny") + MonOps.setShiny(s.data, mon, false, 3) + T.eq(PokemonG3.unownLetter(mon.personality), 5, "setShiny(false) keeps unown letter") + T.check(not shinyOf(mon.personality, 0x1234, 0x5678), "setShiny(false) clears shiny") + MonOps.setAbility(s.data, mon, 0, 3) + T.eq(PokemonG3.unownLetter(mon.personality), 5, "setAbility keeps unown letter") + local before = mon.personality + MonOps.setAbility(s.data, mon, 0, 3) + T.eq(mon.personality, before, "setAbility to the current slot keeps the Unown PID") + MonOps.setShiny(s.data, mon, false, 3) + T.eq(mon.personality, before, "setShiny to the current state keeps the Unown PID") + MonOps.setNature(s.data, mon, before % 25, 3) + T.eq(mon.personality, before, "setNature to the current nature keeps the Unown PID") +end + +for _, v in ipairs({ "firered", "emerald" }) do + local fam = v == "emerald" and "emerald" or "frlg" + local s = g3state(v, { G3.mon({ species = 201, pid = UNOWN_F, tid = 0x1234, sid = 0x5678, ability = 1, + ivs = { 31, 2, 17, 4, 25, 6 }, moves = { 237, 0, 0, 0 }, pp = { 15, 0, 0, 0 } }) }) + local mon = s.save.party[1] + local before = assert(D.deep(assert(K.export(3, v, Serializer.decode(Serializer.encode(s.save)), nil)), fam)).party[1] + T.eq(Ops.setUnownForm(s, mon, 27), true, v .. " setUnownForm accepted ?") + T.eq(s.status, "UNOWN is now ?", v .. " status names the form") + T.eq(#s.undoStack, 1, v .. " one history entry") + T.eq(PokemonG3.unownLetter(mon.personality), 27, v .. " editor mon is ?") + local out = assert(K.export(3, v, Serializer.decode(Serializer.encode(s.save)), nil)) + local after = assert(D.deep(out, fam)).party[1] + T.check(after.checksumOk, v .. " substruct checksum valid after PID change") + T.eq(after.species, 201, v .. " species decrypts as UNOWN") + T.eq(PokemonG3.unownLetter(after.pid), 27, v .. " written PID spells ?") + T.eq(after.pid % 25, UNOWN_F % 25, v .. " nature kept") + T.eq(after.shiny, false, v .. " still not shiny") + T.eq(after.ability, 1, v .. " abilityNum bit kept") + T.eq(table.concat(after.ivs, ","), table.concat(before.ivs, ","), v .. " IVs kept") + T.eq(table.concat(after.moves, ","), table.concat(before.moves, ","), v .. " moves kept") + T.eq(after.exp, before.exp, v .. " exp kept") + local back = assert(K.import(3, v, out)) + T.eq(PokemonG3.unownLetter(back.party[1].personality), 27, v .. " re-import reads ?") + T.eq(Ops.setUnownForm(s, mon, 27), false, v .. " same form is a no-op") +end + +do + local s = g3state("firered", { G3.mon({ species = 4 }) }) + T.eq(Ops.setUnownForm(s, s.save.party[1], 3), false, "non-unown refuses a form") + T.eq(s.status, "Only an Unown has a form", "and says why") +end + +local UNOWN_DEF = { + id = "UNOWN", name = "UNOWN", dex = 201, index = 201, types = { "PSYCHIC" }, + baseStats = { hp = 48, attack = 72, defense = 48, speed = 48, specialAttack = 72, specialDefense = 48 }, + catchRate = 225, baseExp = 61, growthRate = "MEDIUM_FAST", genderRatio = 255, +} +local g2data = { + pokemon = { UNOWN = UNOWN_DEF, CYNDAQUIL = { id = "CYNDAQUIL", name = "CYNDAQUIL", dex = 155, index = 155 } }, + items = K.gen2Data.items, maps = K.gen2Data.maps, moves = {}, +} + +local function g2state(version, dvs) + Version.set(version) + local bytes = G2.build({ version = version, party = { G2.mon({ species = 201, nick = "UNOWN", dvs = dvs }) } }) + local s = State.new() + s.data = g2data + s.save, s.version = assert(Gen2Save.decode(bytes, version, g2data)), version + Gen.ensureBoxes(s.save) + return s +end + +local function dvsOf(t) return { attack = t[1], defense = t[2], speed = t[3], special = t[4] } end + +for _, v in ipairs({ "gold", "crystal" }) do + local start = { 9, 4, 13, 6 } + for letter = 1, 26 do + local s = g2state(v, start) + local mon = s.save.party[1] + T.eq(mon.species, "UNOWN", v .. " fixture decodes as UNOWN") + local hp = mon.dvs.hp + local ok = Ops.setUnownForm(s, mon, letter) + if letter == Unown.letterFromDVs(dvsOf(start)) then + T.eq(ok, false, v .. " setting the current letter is a no-op") + else + T.eq(ok, true, v .. " set letter " .. letter) + end + T.eq(Unown.letterFromDVs(mon.dvs), letter, v .. " DVs spell letter " .. letter) + T.eq(Unown.monLetter(mon), letter, v .. " stored letter " .. letter) + T.eq(mon.dvs.hp, hp, v .. " HP DV kept for " .. letter) + for i, k in ipairs({ "attack", "defense", "speed", "special" }) do + T.eq(mon.dvs[k] % 2, start[i] % 2, v .. " bit 0 of " .. k .. " kept") + T.eq(math.floor(mon.dvs[k] / 8), math.floor(start[i] / 8), v .. " bit 3 of " .. k .. " kept") + end + local report = Legality.mon(s, mon) + local issues = Legality.highlights(report, mon) + T.check(issues.fields.form == nil and issues.fields.shiny == nil, v .. " no form/shiny issue for " .. letter) + local out = assert(Gen2Save.encode(s.save, v, nil, g2data)) + local L = Gen2Save.layoutFor(v) + T.eq(Gen2Save.checksumValid(out, L), true, v .. " checksum valid for " .. letter) + local dvHi, dvLo = out:byte(L.wPartyMons + 0x15 + 1), out:byte(L.wPartyMons + 0x16 + 1) + T.eq(Unown.letterFromDVs(dvsOf({ math.floor(dvHi / 16), dvHi % 16, math.floor(dvLo / 16), dvLo % 16 })), letter, + v .. " DV bytes at +0x15 spell " .. letter) + local back = assert(Gen2Save.decode(out, v, g2data)) + T.eq(Unown.monLetter(back.party[1]), letter, v .. " re-read letter " .. letter) + end + + local s = g2state(v, { 2, 10, 10, 10 }) + local mon = s.save.party[1] + T.eq(Unown.monLetter(mon), 9, v .. " shiny fixture is I") + T.eq(require("src.battle.gen2.Mon").vanillaShiny(mon.dvs), true, v .. " and shiny") + Ops.setUnownForm(s, mon, 22) + T.eq(Unown.monLetter(mon), 22, v .. " shiny I -> V") + T.eq(mon.shiny, true, v .. " V stays shiny") + Ops.setUnownForm(s, mon, 1) + T.eq(Unown.monLetter(mon), 1, v .. " shiny V -> A") + T.eq(mon.shiny, false, v .. " A is not shiny") + T.eq(s.status, "UNOWN is now A (no longer shiny: only I and V can be shiny)", v .. " status warns about shininess") + local out, why = Gen2Save.encode(s.save, v, nil, g2data) + T.check(out ~= nil, v .. " a former shiny now A still writes: " .. tostring(why)) + + for _, letter in ipairs({ 9, 22 }) do + local s2 = g2state(v, { 9, 8, 12, 14 }) + local m2 = s2.save.party[1] + T.eq(require("src.battle.gen2.Mon").vanillaShiny(m2.dvs), false, v .. " plain fixture not shiny") + Ops.setUnownForm(s2, m2, letter) + T.eq(Unown.monLetter(m2), letter, v .. " plain -> " .. letter) + T.eq(m2.shiny, false, v .. " plain never becomes shiny at " .. letter) + end +end + +T.finish() diff --git a/tests/quick.list b/tests/quick.list index 4488e44d..6764250e 100644 --- a/tests/quick.list +++ b/tests/quick.list @@ -4,6 +4,7 @@ tests/engine/amnesia_rest_share_anim_bug1881.lua tests/engine/android_asymmetric_display.lua tests/engine/android_exit_to_launcher_test.lua tests/engine/android_host_extension_test.lua +tests/engine/android_old_shell_restart_bug2738.lua tests/engine/android_secondary_present.lua tests/engine/android_shortcuts_payload_test.lua tests/engine/applying_attack_sfx_wait_bug1998.lua @@ -114,7 +115,9 @@ tests/engine/game3_em_bike_doors_2637.lua tests/engine/game3_em_catch_prompt_2637.lua tests/engine/game3_frontier_obedience_2638.lua tests/engine/game3_post_dex_background_2637.lua +tests/engine/game3_rs_anim_names_test.lua tests/engine/game3_rs_bag_cursor_width_2709.lua +tests/engine/game3_rs_bag_row_layout_2727.lua tests/engine/game3_rs_item_use_text_2699_test.lua tests/engine/game3_rs_starter_choose_scale_2703.lua tests/engine/game3_rs_summary_memo_location_2707.lua @@ -389,6 +392,7 @@ tests/engine/host_shell_bridge_request.lua tests/engine/host_shell_fetch_errors.lua tests/engine/host_shell_postlog.lua tests/engine/host_shell_request_headers.lua +tests/engine/hostshell_restart_single_spawn_2740.lua tests/engine/hostshell_spawn_self.lua tests/engine/hostshell_win_http_nonascii.lua tests/engine/hostshell_win_restart_2721.lua @@ -446,6 +450,7 @@ tests/engine/launcher_page_scroll.lua tests/engine/launcher_patch_notes.lua tests/engine/launcher_return_pointer_bug2079.lua tests/engine/launcher_secret_emerald_test.lua +tests/engine/launcher_settings_persist_2740.lua tests/engine/launcher_skins_options_reads_bug2038.lua tests/engine/launcher_skins_tab.lua tests/engine/launcher_skins_ux.lua @@ -620,6 +625,7 @@ tests/engine/save_convert_toggle_objects.lua tests/engine/save_editor_berry_powder_2698.lua tests/engine/save_editor_buena_points_2640.lua tests/engine/save_editor_custom_maps.lua +tests/engine/save_editor_unown_form_2726.lua tests/engine/save_export_map_window_bug1691.lua tests/engine/save_export_portable_bug752.lua tests/engine/save_export_same_map_moved_2468.lua diff --git a/tools/gen_anim_names.py b/tools/gen_anim_names.py index 2800e322..f7c6703a 100644 --- a/tools/gen_anim_names.py +++ b/tools/gen_anim_names.py @@ -2,21 +2,32 @@ """Generate src/import/gba/anim_names_.lua for an RSE-family pret build. usage: tools/gen_anim_names.py --game emerald [--pret ../pokeemerald] [--ref ../pokefirered] [--check] + tools/gen_anim_names.py --game ruby [--pret ../pokeruby] [--ref ../pokeemerald] [--check] """ import argparse +import difflib import hashlib import importlib.util import os import re import sys +from collections import Counter ROM_LO = 0x08000000 ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) GAMES = { "emerald": {"repo": "pokeemerald", "elf": "pokeemerald.elf", "rom": "pokeemerald.gba"}, + "ruby": {"repo": "pokeruby", "elf": "pokeruby.elf", "rom": "pokeruby.gba"}, } REF = {"repo": "pokefirered", "elf": "pokefirered.elf", "rom": "pokefirered.gba"} +RS_REF = {"repo": "pokeemerald", "elf": "pokeemerald.elf", "rom": "pokeemerald.gba"} +RS_NATIVE_GLOB = ("rs_callbacks.lua", "rs_early_callbacks.lua", "rs_tasks.lua", "rs_sound_tasks.lua") +PTR_FIELDS = { + 0x02: ((1, "template"),), 0x03: ((1, "task"),), 0x1F: ((1, "task"),), + 0x0E: ((1, "branch"),), 0x13: ((1, "branch"),), 0x24: ((1, "branch"),), + 0x21: ((4, "branch"),), 0x12: ((2, "branch"),), 0x11: ((1, "branch"), (5, "branch")), +} FIXED = { 0x00: 3, 0x01: 3, 0x04: 2, 0x05: 1, 0x06: 1, 0x07: 1, 0x08: 1, 0x09: 3, @@ -247,6 +258,305 @@ def render_indexed(name, d, indent=" "): return rows +def decode_ops(bd, o, errors): + ops, pos = [], o + for _ in range(8192): + op = bd.u8(pos) + n = op_len(bd, pos) + raw = bytearray(bd.b[pos:pos + n]) + ptrs = [] + for at, kind in PTR_FIELDS.get(op, ()): + ptrs.append((kind, bd.u32(pos + at))) + raw[at:at + 4] = b"\0\0\0\0" + ops.append((bytes(raw), ptrs)) + if op in (0x08, 0x0F, 0x13): + return ops + pos += n + errors.append("script at 0x%X did not terminate" % o) + return ops + + +def parse_lua_maps(path, sections): + out, cur = {s: {} for s in sections}, None + with open(path) as f: + for line in f: + m = re.match(r"^ (\w+) = \{$", line) + if m: + cur = m.group(1) if m.group(1) in out else None + continue + if line.startswith(" }"): + cur = None + continue + if cur: + m = re.match(r'^ (?:\["([^"]+)"\]|(\w+)) = "([^"]*)",$', line) + if m: + out[cur][m.group(1) or m.group(2)] = m.group(3) + return out + + +def native_names(): + names = set() + base = os.path.join(ROOT, "src", "core", "game3", "battle", "anim_port") + for fn in RS_NATIVE_GLOB: + with open(os.path.join(base, fn)) as f: + for m in re.finditer(r"\b(sub_[0-9A-Fa-f]{7})\s*=", f.read()): + names.add(m.group(1)) + return names + + +def status_names(pret, count): + out = {} + with open(os.path.join(pret, "include", "constants", "battle.h")) as f: + for l in f: + m = re.match(r"#define\s+B_ANIM_(STATUS_\w+)\s+(0x[0-9A-Fa-f]+|\d+)", l) + if m and int(m.group(2), 0) < count: + out[int(m.group(2), 0)] = m.group(1) + return out + + +def best(counter): + top = counter.most_common(2) + if len(top) > 1 and top[0][1] == top[1][1]: + return None + return top[0][0] + + +def main_rs(args): + info = GAMES[args.game] + pret = args.pret or os.path.join(os.path.dirname(ROOT), info["repo"]) + refp = args.ref or os.path.join(os.path.dirname(ROOT), RS_REF["repo"]) + out = args.out or os.path.join(ROOT, "src", "import", "gba", "rs", "anim_names.lua") + symmod = load_syms_module() + rs = Build(pret, info, symmod) + em = Build(refp, RS_REF, symmod) + errors = [] + em_names = parse_lua_maps(os.path.join(ROOT, "src", "import", "gba", "anim_names_emerald.lua"), + ("templates", "callbacks", "tasks")) + + rs_tables, em_tables = table_labels(pret), table_labels(refp) + roots, queue, counts = [], [], {} + for key, tname in TABLES: + rbase, ebase = rs.sym_off(tname), em.sym_off(tname) + rn = len([l for l, _ in rs_tables[tname] if l != "Move_COUNT"]) + en = len([l for l, _ in em_tables[tname] if l != "Move_COUNT"]) + counts[key] = rn + for i in range(rn): + ro = rs.off(rs.u32(rbase + i * 4)) + if ro is None: + errors.append("%s[%d] bad pointer" % (tname, i)) + continue + roots.append(ro) + if i < en: + eo = em.off(em.u32(ebase + i * 4)) + if eo is not None: + queue.append((ro, eo)) + + tmpl_ptrs, task_ptrs = walk(rs, roots, errors) + + def func_size(bd, ptr): + r = bd.func_row(ptr) + return r and r["size"] + + def close(x, y): + return x is not None and y is not None and abs(x - y) <= max(8, max(x, y) // 10) + + def same_size(pa, pb): + if pa[0] == "template": + ra, rb = rs.data_row(pa[1]), em.data_row(pb[1]) + if not ra or not rb: + return False + return close(func_size(rs, rs.u32(ra["off"] + 20)), func_size(em, em.u32(rb["off"] + 20))) + return close(func_size(rs, pa[1]), func_size(em, pb[1])) + + votes = {"template": {}, "task": {}} + seen, drift = set(), 0 + while queue: + pair = queue.pop() + if pair in seen: + continue + seen.add(pair) + a, b = decode_ops(rs, pair[0], errors), decode_ops(em, pair[1], errors) + ta, tb = [x[0] for x in a], [x[0] for x in b] + if ta != tb: + drift += 1 + sm = difflib.SequenceMatcher(None, ta, tb, autojunk=False) + matched, loose_a, loose_b = [], [], [] + for tag, i1, i2, j1, j2 in sm.get_opcodes(): + if tag == "equal" or (tag == "replace" and i2 - i1 == j2 - j1 + and all(ta[i1 + k][0] == tb[j1 + k][0] for k in range(i2 - i1))): + matched += [(i1 + k, j1 + k) for k in range(i2 - i1)] + else: + loose_a += [i for i in range(i1, i2) if ta[i][0] in (0x02, 0x03, 0x1F)] + loose_b += [j for j in range(j1, j2) if tb[j][0] in (0x02, 0x03, 0x1F)] + ca, cb = Counter(ta[i] for i in loose_a), Counter(tb[j] for j in loose_b) + matched += [(i, j) for i in loose_a for j in loose_b if ta[i] == tb[j] and ca[ta[i]] == 1 and cb[tb[j]] == 1 + and same_size(a[i][1][0], b[j][1][0])] + for ia, ib in matched: + for (ka, pa), (_, pb) in zip(a[ia][1], b[ib][1]): + if ka == "branch": + ro, eo = rs.off(pa), em.off(pb) + if ro is not None and eo is not None: + queue.append((ro, eo)) + else: + votes[ka].setdefault(pa, Counter())[pb] += 1 + + natives = native_names() + ambiguous, resized = [], [] + + def rs_func_key(r): + return rs.func_key(r) if r.get("obj") else r["name"] + + def em_func_canon(ptr, section): + r = em.func_row(ptr) + return r and em_names[section].get(em.func_key(r)) + + tasks, task_src = {}, {} + for ptr in sorted(task_ptrs): + r = rs.func_row(ptr) + if not r: + errors.append("task pointer 0x%08X has no ELF function" % ptr) + continue + key = rs_func_key(r) + canon = None + if ptr in votes["task"] and r["name"] not in natives: + eptr = best(votes["task"][ptr]) + if eptr is None: + ambiguous.append("task %s: %s" % (key, ", ".join( + "%s x%d" % (em.func_row(p)["name"], c) for p, c in votes["task"][ptr].most_common()))) + else: + canon = em_func_canon(eptr, "tasks") + if canon and func_size(rs, ptr) != func_size(em, eptr): + resized.append("task %s %d -> %s %d" % (key, func_size(rs, ptr), canon, func_size(em, eptr))) + tasks[key] = canon or short_task(r["name"]) + task_src[key] = canon is not None + + cb_votes, tmpl_canon = {}, {} + templates, callbacks, cb_src = {}, {}, {} + for ptr in sorted(tmpl_ptrs): + r = rs.data_row(ptr) + if not r: + errors.append("template pointer 0x%08X has no ELF symbol" % ptr) + continue + key = rs.data_key(r) if r.get("obj") else r["name"] + cbptr = rs.u32(r["off"] + 20) + cbr = rs.func_row(cbptr) + if not cbr: + errors.append("template %s callback 0x%08X has no ELF function" % (key, cbptr)) + continue + eptr = None + if ptr in votes["template"]: + eptr = best(votes["template"][ptr]) + if eptr is None: + ambiguous.append("template %s: %s" % (key, ", ".join( + "%s x%d" % (em.data_row(p)["name"], c) for p, c in votes["template"][ptr].most_common()))) + if eptr is not None: + er = em.data_row(eptr) + tmpl_canon[key] = (em_names["templates"].get(em.data_key(er)), sum(votes["template"][ptr].values())) + ecb = em.u32(er["off"] + 20) + cb_votes.setdefault(cbptr, Counter())[ecb] += votes["template"][ptr][eptr] + templates[key] = key + callbacks.setdefault(rs_func_key(cbr), None) + + by_canon = {} + for key, (canon, n) in tmpl_canon.items(): + if canon: + by_canon.setdefault(canon, []).append((n, key)) + shared, tmpl_src = [], set() + for canon, rows in by_canon.items(): + rows.sort(key=lambda x: (-x[0], x[1])) + if canon in templates and canon not in [k for _, k in rows]: + shared.append("%s (RS template of that name exists): %s" % (canon, ", ".join(k for _, k in rows))) + continue + templates[rows[0][1]] = canon + tmpl_src.add(rows[0][1]) + if len(rows) > 1: + shared.append("%s <- %s; kept RS name for %s" % (canon, rows[0][1], ", ".join(k for _, k in rows[1:]))) + + for ptr in sorted({rs.u32(rs.data_row(p)["off"] + 20) for p in tmpl_ptrs if rs.data_row(p)}): + r = rs.func_row(ptr) + if not r: + continue + key = rs_func_key(r) + canon = None + if ptr in cb_votes and r["name"] not in natives: + eptr = best(cb_votes[ptr]) + if eptr is None: + ambiguous.append("callback %s: %s" % (key, ", ".join( + "%s x%d" % (em.func_row(p)["name"], c) for p, c in cb_votes[ptr].most_common()))) + else: + canon = em_func_canon(eptr, "callbacks") + if canon and func_size(rs, ptr) != func_size(em, eptr): + resized.append("callback %s %d -> %s %d" % (key, func_size(rs, ptr), canon, func_size(em, eptr))) + callbacks[key] = canon or short_cb(r["name"]) + cb_src[key] = canon is not None + + tags = tag_names(pret) + if sorted(tags) != list(range(len(tags))): + errors.append("ANIM_TAG_ indices are not contiguous") + status = status_names(pret, counts["status"]) + shared_names = {} + for key, tname in TABLES[2:]: + lst = [re.match(r"B_ANIM_(\w+)", c) for _, c in em_tables[tname]] + if len(lst) != counts[key] or not all(lst): + errors.append("%s does not line up with %s" % (tname, RS_REF["repo"])) + shared_names[key] = dict(enumerate(m.group(1) for m in lst if m)) + if sorted(status) != list(range(counts["status"])): + errors.append("B_ANIM_STATUS_ constants do not cover gBattleAnims_StatusConditions") + + rs_only_cb = sorted(k for k, v in callbacks.items() if not cb_src.get(k) and k not in natives) + rs_only_task = sorted(k for k, v in tasks.items() if not task_src.get(k) and k not in natives) + rs_only_tmpl = sorted(k for k in templates if k not in tmpl_src) + print("%s: %d script roots, %d paired scripts (%d drifted), %d templates, %d callbacks, %d tasks" % ( + args.game, len(roots), len(seen), drift, len(templates), len(callbacks), len(tasks))) + print("aligned: callbacks=%d tasks=%d templates=%d natives=%d" % ( + sum(1 for v in cb_src.values() if v), sum(1 for v in task_src.values() if v), + len(templates) - len(rs_only_tmpl), len(natives))) + print("rs-only callbacks: %s" % (", ".join(rs_only_cb) or "-")) + print("rs-only tasks: %s" % (", ".join(rs_only_task) or "-")) + for s in ambiguous: + print(" ambiguous " + s) + for s in shared: + print(" shared " + s) + for s in resized: + print(" resized " + s) + if errors: + print("ERRORS (%d):" % len(errors)) + for e in errors[:80]: + print(" " + e) + sys.exit(1) + + lines = [ + "-- tools/gen_anim_names.py --game ruby from %s, %s/data/battle_anim_scripts.s aligned with %s/data/battle_anim_scripts.s" + % (info["elf"], info["repo"], RS_REF["repo"]), + "return {", + ' game = "rs",', + ] + lines += render_map("templates", templates) + lines += render_map("callbacks", callbacks) + lines += render_map("tasks", tasks) + lines += [" rsOnly = {"] + lines += render_list("callbacks", rs_only_cb, " ") + lines += render_list("tasks", rs_only_task, " ") + lines += render_list("templates", rs_only_tmpl, " ") + lines += [" },"] + lines += render_indexed("tagNames", tags) + lines += render_indexed("statusNames", status) + lines += render_indexed("generalNames", shared_names["general"]) + lines += render_indexed("specialNames", shared_names["special"]) + lines += ["}", ""] + text = "\n".join(lines) + if args.check: + cur = open(out).read() if os.path.exists(out) else "" + if cur != text: + print("STALE %s" % out) + sys.exit(1) + print("OK %s" % out) + return + with open(out, "w") as f: + f.write(text) + print("wrote %s" % out) + + def main(): ap = argparse.ArgumentParser() ap.add_argument("--game", default="emerald", choices=sorted(GAMES)) @@ -255,6 +565,8 @@ def main(): ap.add_argument("--out") ap.add_argument("--check", action="store_true") args = ap.parse_args() + if args.game == "ruby": + return main_rs(args) info = GAMES[args.game] pret = args.pret or os.path.join(os.path.dirname(ROOT), info["repo"]) diff --git a/tools/save-editor/InspectorBody.lua b/tools/save-editor/InspectorBody.lua index c484701e..cfa0f2cc 100644 --- a/tools/save-editor/InspectorBody.lua +++ b/tools/save-editor/InspectorBody.lua @@ -152,8 +152,16 @@ local function drawSection(S, Kit, x, y, w, h, report, issues) cy = cy + row + 2 * gap hint(id) end - local function choice(id, label, value, options, apply) - cy = cy + Touch.choice(S, Kit, id, label, value, options, cx, cy, inner, apply, nil, issues.fields[id]) + 2 * gap + local function choice(id, label, value, options, apply, help) + cy = cy + Touch.choice(S, Kit, id, label, value, options, cx, cy, inner, apply, help, issues.fields[id]) + 2 * gap + end + local function unownForm(help) + local letter = Ops.unownForm(S, mon) + if letter == nil then return false end + choice("form", "Unown form", letter, Ops.unownFormOptions(S), function(v) + return Ops.setUnownForm(S, mon, v) + end, help) + return true end local function props(list) for _, d in ipairs(list) do @@ -308,13 +316,18 @@ local function drawSection(S, Kit, x, y, w, h, report, issues) button("Shiny: " .. (shiny and "ON" or "OFF"), function() Ops.setShiny(S, mon, not shiny) end, "warn", "Changes shininess and personality. Review Checks after editing.", "shiny") + unownForm("Keeps nature and shininess. Changes the personality value.") elseif g == 2 then local d = P.find(S, "pokerus") choice("pokerus", "Pokérus", mon.pokerus or 0, Named.property(S, d), function(v) return Ops.setPokerus(S, mon, v) end) - text("Gender, shininess and Unown form follow DVs.") - for _, id in ipairs({ "gender", "shiny", "form" }) do hint(id) end + local isUnown = unownForm("Rewrites the middle bits of the DVs. Only I and V can be shiny.") + text(isUnown and "Gender and shininess follow DVs. Picking a form rewrites the middle DV bits." + or "Gender and shininess follow DVs.") + hint("gender") + hint("shiny") + if not isUnown then hint("form") end end button("Fix all errors", function() Ops.fixMonErrors(S, mon) diff --git a/tools/save-editor/MonOps.lua b/tools/save-editor/MonOps.lua index 734c7b91..1e4c5ec4 100644 --- a/tools/save-editor/MonOps.lua +++ b/tools/save-editor/MonOps.lua @@ -126,12 +126,81 @@ function MonOps.recalc(data, mon, gen) mon.hp = math.max(0, math.min(mon.hp or mon.stats.hp, mon.stats.hp)) end +MonOps.SPECIES_UNOWN_G3 = 201 +MonOps.UNOWN_FORMS_G3 = 28 + +local function spliceLetterBits(half, lo2, hi2) + local bit = require("bit") + return bit.bor(bit.band(half, 0xFCFC), lo2, hi2 * 256) +end + +-- include/pokemon.h:364 GET_UNOWN_LETTER, src/pokemon.c:2318 CreateMonWithGenderNatureLetter +function MonOps.unownPid(pid, letter, nature, shiny, tid, sid) + local bit = require("bit") + local PokemonG3 = require("src.core.game3.pokemon") + pid = (tonumber(pid) or 0) % 4294967296 + letter = math.floor(tonumber(letter) or 0) % MonOps.UNOWN_FORMS_G3 + nature = nature ~= nil and tonumber(nature) or nil + local tsv = bit.bxor((tonumber(tid) or 0) % 65536, (tonumber(sid) or 0) % 65536) + local oldLow, oldHigh = pid % 65536, math.floor(pid / 65536) + local function accept(high, low) + local p = high * 65536 + low + if p == 0 then return nil end + if nature ~= nil and p % 25 ~= nature then return nil end + local isShiny = bit.bxor(tsv, bit.bxor(high, low)) < 8 + if shiny ~= nil and isShiny ~= (shiny and true or false) then return nil end + if PokemonG3.unownLetter(p) ~= letter then return nil end + return p + end + local same = accept(oldHigh, oldLow) + if same then return same end + local tsv89 = math.floor(tsv / 256) % 4 + for d = 0, 4095 do + local low0 = bit.bxor(oldLow, (d % 64) * 4 + math.floor(d / 64) * 1024) + if shiny then + for b1 = 0, 3 do + local b3 = bit.bxor(b1, tsv89) + for b0 = 0, 3 do + for b2 = 0, 3 do + if (b3 * 64 + b2 * 16 + b1 * 4 + b0) % MonOps.UNOWN_FORMS_G3 == letter then + local low = spliceLetterBits(low0, b0, b1) + local base = bit.bxor(low, tsv) + for k2 = 0, 1 do + local high = bit.bor(bit.band(bit.bxor(base, k2 * 4), 0xFFFC), b2) + local p = accept(high, low) + if p then return p end + end + end + end + end + end + else + for v = letter, 255, MonOps.UNOWN_FORMS_G3 do + local b0, b1 = v % 4, math.floor(v / 4) % 4 + local b2, b3 = math.floor(v / 16) % 4, math.floor(v / 64) + local p = accept(spliceLetterBits(oldHigh, b2, b3), spliceLetterBits(low0, b0, b1)) + if p then return p end + end + end + end + return nil +end + +local function unownReq(mon) + if tonumber(mon.speciesId or mon.species) ~= MonOps.SPECIES_UNOWN_G3 then return nil end + return require("src.core.game3.pokemon").unownLetter(mon.personality) +end + function MonOps.generatePid(species, otId, otSecretId, reqs) reqs = reqs or {} local PokemonG3 = require("src.core.game3.pokemon") local bit = require("bit") local spId = tonumber(species) or (PokemonG3.speciesFromName and PokemonG3.speciesFromName(tostring(species))) or 1 + if spId == MonOps.SPECIES_UNOWN_G3 and reqs.unownLetter ~= nil then + local p = MonOps.unownPid(reqs.basePid, reqs.unownLetter, reqs.nature, reqs.shiny, otId, otSecretId) + if p then return p end + end otId = bit.band(tonumber(otId) or 0, 0xFFFF) otSecretId = bit.band(tonumber(otSecretId) or 0, 0xFFFF) local trainerXor = bit.bxor(otId, otSecretId) @@ -224,6 +293,8 @@ function MonOps.setNature(data, mon, natureId, gen) ability = currentAbility, gender = mon.gender, shiny = isShiny, + unownLetter = unownReq(mon), + basePid = mon.personality, }) MonOps.recalc(data, mon, gen) end @@ -241,6 +312,8 @@ function MonOps.setAbility(data, mon, abilitySlot, gen) ability = abilitySlot, gender = mon.gender, shiny = isShiny, + unownLetter = unownReq(mon), + basePid = mon.personality, }) mon.abilityNum = abilitySlot local PokemonG3 = require("src.core.game3.pokemon") @@ -261,6 +334,8 @@ function MonOps.setGender(data, mon, gender, gen) ability = currentAbility, gender = gender, shiny = isShiny, + unownLetter = unownReq(mon), + basePid = mon.personality, }) local PokemonG3 = require("src.core.game3.pokemon") mon.gender = PokemonG3.gender(mon.speciesId or mon.species, mon.personality) @@ -281,6 +356,8 @@ function MonOps.setShiny(data, mon, shiny, gen) ability = currentAbility, gender = mon.gender, shiny = shiny, + unownLetter = unownReq(mon), + basePid = mon.personality, }) mon.isShiny = shiny MonOps.recalc(data, mon, gen) @@ -534,6 +611,20 @@ function MonOps.syncHpDv(dvs) return dvs end +local function deriveGen2(data, mon) + local Mon = require("src.battle.gen2.Mon") + local Unown = require("src.core.gen2.Unown") + mon.dvs.hp = Mon.hpDV(mon.dvs) + local def = data and data.pokemon and data.pokemon[mon.species] + if def then + mon.gender = Mon.gender(def, mon.dvs, { species = mon.species, level = mon.level }) + end + mon.shiny = Mon.isShiny(mon.dvs, { species = mon.species, def = def, level = mon.level }) + if mon.species == Unown.SPECIES then + mon.unownLetter = Unown.letterFromDVs(mon.dvs) + end +end + function MonOps.setDv(data, mon, key, value, gen) if type(mon) ~= "table" then return end mon.dvs = mon.dvs or { attack = 15, defense = 15, speed = 15, special = 15, hp = 15 } @@ -552,21 +643,58 @@ function MonOps.setDv(data, mon, key, value, gen) mon.ivs.spa = math.min(31, (mon.dvs.special or 0) * 2 + 1) mon.ivs.spd = math.min(31, (mon.dvs.special or 0) * 2 + 1) elseif gen == 2 or (mon.stats and mon.stats.specialAttack) then - local Mon = require("src.battle.gen2.Mon") - mon.dvs.hp = Mon.hpDV(mon.dvs) - local def = data and data.pokemon and data.pokemon[mon.species] - if def then - mon.gender = Mon.gender(def, mon.dvs, { species = mon.species, level = mon.level }) - mon.shiny = Mon.isShiny(mon.dvs, { species = mon.species, def = def, level = mon.level }) - local Unown = require("src.core.gen2.Unown") - if mon.species == Unown.SPECIES then - mon.unownLetter = Unown.letterFromDVs(mon.dvs) - end - end + deriveGen2(data, mon) end MonOps.recalc(data, mon, gen) end +local DV_KEYS = { "attack", "defense", "speed", "special" } + +-- engine/gfx/load_pics.asm:1 GetUnownLetter, engine/gfx/color.asm:8 CheckShininess +function MonOps.unownDvs(dvs, letter, keepShiny) + local Mon = require("src.battle.gen2.Mon") + letter = math.floor(tonumber(letter) or 1) + if letter < 1 or letter > 26 then return nil end + local lo, hi = (letter - 1) * 10, math.min(255, letter * 10 - 1) + local best, bestKey + for p = lo, hi do + local out = {} + for i, k in ipairs(DV_KEYS) do + local d = tonumber(dvs[k]) or 0 + local mid = math.floor(p / 4 ^ (4 - i)) % 4 + out[k] = d - (math.floor(d / 2) % 4) * 2 + mid * 2 + end + local cost = 0 + for _, k in ipairs(DV_KEYS) do cost = cost + math.abs(out[k] - (tonumber(dvs[k]) or 0)) end + local key = (Mon.vanillaShiny(out) == (keepShiny and true or false) and 0 or 1000) + cost + if not bestKey or key < bestKey then best, bestKey = out, key end + end + return best +end + +function MonOps.setUnownForm(data, mon, letter, gen) + if type(mon) ~= "table" then return nil end + if gen == 3 then + local Summary = require("src.core.game3.summary_data") + local p = MonOps.unownPid(mon.personality, letter, (tonumber(mon.personality) or 0) % 25, + Summary.isShiny(mon), mon.otId, mon.otSecretId) + if not p then return nil end + mon.personality = p + mon.isShiny = nil + MonOps.recalc(data, mon, gen) + return true + end + local Mon = require("src.battle.gen2.Mon") + mon.dvs = mon.dvs or { attack = 0, defense = 0, speed = 0, special = 0 } + local wasShiny = mon.shiny == true or Mon.vanillaShiny(mon.dvs) + local out = MonOps.unownDvs(mon.dvs, letter, wasShiny) + if not out then return nil end + for k, v in pairs(out) do mon.dvs[k] = v end + deriveGen2(data, mon) + MonOps.recalc(data, mon, gen) + return true, wasShiny and not mon.shiny +end + local EV_KEYS = { "hp", "atk", "def", "spe", "spa", "spd" } function MonOps.setEv(data, mon, key, value, gen) diff --git a/tools/save-editor/Ops.lua b/tools/save-editor/Ops.lua index b9fd735c..e1994a6f 100644 --- a/tools/save-editor/Ops.lua +++ b/tools/save-editor/Ops.lua @@ -2241,6 +2241,54 @@ function Ops.setShiny(S, mon, shiny) return Ops.mark(S, ("%s %s"):format(mon.species, shiny and "is now shiny!" or "is no longer shiny")) end +Ops.UNOWN_FORM_NAMES_G3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!?" + +function Ops.unownForm(S, mon) + if not mon or mon.isEgg or mon.egg then return nil end + local g = Gen.ofState(S) + if g == 3 and tonumber(mon.speciesId or mon.species) == MonOps.SPECIES_UNOWN_G3 then + return require("src.core.game3.pokemon").unownLetter(mon.personality) + elseif g == 2 then + return require("src.core.gen2.Unown").monLetter(mon) + end + return nil +end + +function Ops.unownFormName(S, letter) + if letter == nil then return nil end + if Gen.ofState(S) == 3 then + local i = letter + 1 + return Ops.UNOWN_FORM_NAMES_G3:sub(i, i) + end + return require("src.core.gen2.Unown").name(letter) +end + +function Ops.unownFormOptions(S) + local out = {} + if Gen.ofState(S) == 3 then + for i = 0, MonOps.UNOWN_FORMS_G3 - 1 do out[#out + 1] = { i, Ops.unownFormName(S, i) } end + else + for i = 1, 26 do out[#out + 1] = { i, Ops.unownFormName(S, i) } end + end + return out +end + +function Ops.setUnownForm(S, mon, letter) + local current = Ops.unownForm(S, mon) + if current == nil then return Ops.say(S, "Only an Unown has a form") end + letter = math.floor(tonumber(letter) or -1) + local name = Ops.unownFormName(S, letter) + if not name or name == "" then return Ops.say(S, "Unknown Unown form") end + if letter == current then return Ops.say(S, ("UNOWN is already %s"):format(name)) end + local g = Gen.ofState(S) + local ok, lostShiny = MonOps.setUnownForm(S.data, mon, letter, g) + if not ok then return Ops.say(S, ("UNOWN %s cannot keep this nature and shininess"):format(name)) end + if lostShiny then + return Ops.mark(S, ("UNOWN is now %s (no longer shiny: only I and V can be shiny)"):format(name)) + end + return Ops.mark(S, ("UNOWN is now %s"):format(name)) +end + function Ops.setPokerus(S, mon, value) if not mon then return false end local want = clamp(math.floor(value), 0, 255) diff --git a/tools/save-editor/panels/MonEditor.lua b/tools/save-editor/panels/MonEditor.lua index ec1e38e1..54dec590 100644 --- a/tools/save-editor/panels/MonEditor.lua +++ b/tools/save-editor/panels/MonEditor.lua @@ -11,17 +11,28 @@ local MonEditor = {} -- species, cached for the process: the old panel called newImage every frame, -- which re-decoded a PNG sixty times a second. local spriteCache = {} -function MonEditor.sprite(S, species) + +function MonEditor.spriteKey(S, species, mon) if not species then return nil end - if spriteCache[species] ~= nil then return spriteCache[species] or nil end + local letter = mon and require("Ops").unownForm(S, mon) + if letter == nil then return species end + return tostring(species) .. ":" .. letter +end + +function MonEditor.sprite(S, species, mon) + if not species then return nil end + local key = MonEditor.spriteKey(S, species, mon) + local letter = key ~= species and require("Ops").unownForm(S, mon) or nil + if spriteCache[key] ~= nil then return spriteCache[key] or nil end if Gen.ofState(S) == 3 then local okP, Pokemon = pcall(require, "src.core.game3.pokemon") if okP and Pokemon then local spId = tonumber(species) or (Pokemon.speciesFromName and Pokemon.speciesFromName(tostring(species))) if spId then - local pic = (Pokemon.frontPic and Pokemon.frontPic(spId)) or (Pokemon.icon and Pokemon.icon(spId)) + local picId = letter ~= nil and Pokemon.picSpecies(spId, mon.personality) or spId + local pic = (Pokemon.frontPic and Pokemon.frontPic(picId)) or (Pokemon.icon and Pokemon.icon(picId)) if pic and pic.image then - spriteCache[species] = pic.image + spriteCache[key] = pic.image return pic.image end end @@ -29,19 +40,22 @@ function MonEditor.sprite(S, species) end local def = S.data and S.data.pokemon and (S.data.pokemon[species] or (type(species) == "number" and S.data.pokemon[species])) local path = def and def.spriteFront + if letter ~= nil and Gen.ofState(S) == 2 then + path = require("src.core.gen2.Unown").formSprite(S.data.pokemon, letter) or path + end if not path or not love.graphics.newImage then - spriteCache[species] = false + spriteCache[key] = false return nil end local ok, img = pcall(love.graphics.newImage, path) - spriteCache[species] = ok and img or false + spriteCache[key] = ok and img or false return ok and img or nil end -- Draw a species sprite fitted into a box, or a dashed placeholder when the -- cache has no art for it (a modded species, or a headless run). -function MonEditor.drawSprite(S, Kit, species, x, y, size) - local img = MonEditor.sprite(S, species) +function MonEditor.drawSprite(S, Kit, species, x, y, size, mon) + local img = MonEditor.sprite(S, species, mon) if img and love.graphics.draw and img.getDimensions then local iw, ih = img:getDimensions() if iw > 0 and ih > 0 then diff --git a/tools/save-editor/panels/Party.lua b/tools/save-editor/panels/Party.lua index 7be6e4dd..0981219b 100644 --- a/tools/save-editor/panels/Party.lua +++ b/tools/save-editor/panels/Party.lua @@ -84,7 +84,7 @@ local function drawRoster(S, Kit, x, y, listW, h) local rpad = 12 * s local icon = 44 * s - MonEditor.drawSprite(S, Kit, mon.species, cx + rpad, ry + (rowH - icon) / 2, icon) + MonEditor.drawSprite(S, Kit, mon.species, cx + rpad, ry + (rowH - icon) / 2, icon, mon) -- right cluster first, so the name knows how much room it has left local rightW = 52 * s