further performance udpdates

This commit is contained in:
bryanthaboi
2026-10-05 15:45:58 -04:00
parent 6613f5bc42
commit 34377eef5a
29 changed files with 920 additions and 123 deletions
+7
View File
@@ -15,6 +15,7 @@ local MapIds = require("src.core.game3.map_ids")
local Profile = require("src.core.game3.profile")
local Runtime = require("src.core.game3.runtime")
local Audio = require("src.core.game3.audio")
local Warm = require("src.core.game3.warm")
local Options = require("src.core.game3.options")
local Dataset = require("src.core.game3.dataset")
local Display = require("src.core.game3.display")
@@ -117,6 +118,10 @@ function Game3:_enterField(session, reason, opts)
-- pokefirered/src/fieldmap.c:100
Runtime.start(nil, self, session, { reason = reason or "new_game" })
Map._nextEnterVia = nil
pcall(function() require("src.core.game3.prewarm").session(session) end)
local Syms = package.loaded["src.import.gba.syms"]
if Syms and Syms.reset then Syms.reset() end
collectgarbage("collect")
if fieldCallback then require(fieldCallback).execute() end
if reason == "continue" then
if lazyReq("src.core.game3.profile").family(session) == "rse" then
@@ -755,6 +760,8 @@ function Game3:update(dt)
local okP, errP = pcall(function() lazyReq("src.render.Pipelines").update(dt) end)
if not okP then s9log("pipelines", errP) end
pcall(function() lazyReq("src.core.DiscordPresence").update(dt) end)
local okW, errW = pcall(Warm.step)
if not okW then s9log("warm", errW) end
Game3.stepGC()
end
+2 -2
View File
@@ -23,8 +23,8 @@ local function applyVolume(src)
fading = state.fade ~= nil,
optionScale = volumeScale,
}
local ok, Game = pcall(require, "src.core.Game")
if ok and Game then
local Game = package.loaded["src.core.Game"]
if type(Game) == "table" then
local ow = Game.overworld
if ow and ow.player then
ctx.x, ctx.y = ow.player.cellX, ow.player.cellY
+120 -48
View File
@@ -9,6 +9,7 @@ local Mix = require("src.core.game3.m4a_mix")
local Player = require("src.core.game3.m4a_player")
local SE = require("src.core.game3.se_ids")
local Song = require("src.core.game3.song_ids")
local Warm = require("src.core.game3.warm")
local ffiOk, ffi = pcall(require, "ffi")
local Audio = {}
@@ -817,6 +818,7 @@ Audio.SE_LOOP_MAX_SEC = 2.5
Audio.SE_ONESHOT_MAX_SEC = 30
function Audio._seRawClear()
Audio._cryCache, Audio._cryCacheN = nil, 0
Audio._seRaw = {}
Audio._seRawFrames = 0
Audio._seRawTick = 0
@@ -919,6 +921,55 @@ function Audio._seSourceFor(id, rawL, rawR, master, pan, mono, loop)
return src
end
local function bake_se(id, opts, memoable)
local slot = { voices = {} }
-- SE must run the M4A sequencer (SE_SELECT is CGB pulse, not voice0 PCM).
local ok = Player.start(Audio._pack, Audio._cache, slot, id, { forceSeq = true })
if not ok then
warn_once("se:" .. tostring(id), "SE " .. tostring(id) .. " missing")
return nil
end
local loop = opts.loop
if loop == nil then
-- SE_LOW_HEALTH and any track with GOTO before FINE are hardware loops.
loop = (id == SE.SE_LOW_HEALTH) or Audio._songHasGoto(slot)
end
local loopBody = loop and opts.loop == nil and id ~= SE.SE_LOW_HEALTH
-- pokefirered/src/battle_anim_special.c:1200
local cut = ((loop and not loopBody) or id == SE.SE_EXP)
local maxSec = opts.maxSec
or (cut and Audio.SE_LOOP_MAX_SEC or Audio.SE_ONESHOT_MAX_SEC)
local rawL, rawR, loopStart = Player.bakeSlot(slot, {
raw = true,
maxSec = maxSec,
stopOnGoto = loop and true or false,
loopBody = loopBody,
})
if not cut and opts.maxSec == nil and type(rawL) == "table"
and #rawL >= math.floor(Mix.SAMPLE_RATE * maxSec) then
warn_once("selen:" .. tostring(id),
"SE " .. tostring(id) .. " hit the " .. tostring(maxSec) .. "s bake ceiling")
end
if memoable then Audio._seRawPut(id, loop and true or false, rawL, rawR, loopStart) end
return loop and true or false, rawL, rawR, loopStart
end
function Audio.prewarmSe(id, priority)
id = SE.resolve(id)
if id == nil or not Audio.isReady() then return false end
if Audio._seRaw and Audio._seRaw[id] then return false end
local info = Audio.songInfo(id)
if info and info.kind == "fanfare" then return false end
return Warm.add("se:" .. tostring(id), function()
if Audio._seRaw and Audio._seRaw[id] then return end
local loop, rawL, rawR = bake_se(id, {}, true)
if loop == nil or loop then return end
Audio._seSourceFor(id, rawL, rawR, Audio._sfxVolume or 1, Audio.normalizePan(nil), Audio._mono, false)
end, priority)
end
function Audio.playSe(id, opts)
opts = opts or {}
id = SE.resolve(id)
@@ -938,42 +989,14 @@ function Audio.playSe(id, opts)
Audio._stopSePlayer(mplay)
local memoable = opts.loop == nil and opts.maxSec == nil
if memoable then Warm.flush("se:" .. tostring(id)) end
local hit = memoable and Audio._seRawGet(id) or nil
local loop, rawL, rawR, loopStart
if hit then
loop, rawL, rawR, loopStart = hit.loop, hit.rawL, hit.rawR, hit.loopStart
else
local slot = { voices = {} }
-- SE must run the M4A sequencer (SE_SELECT is CGB pulse, not voice0 PCM).
local ok = Player.start(Audio._pack, Audio._cache, slot, id, { forceSeq = true })
if not ok then
warn_once("se:" .. tostring(id), "SE " .. tostring(id) .. " missing")
return false
end
loop = opts.loop
if loop == nil then
-- SE_LOW_HEALTH and any track with GOTO before FINE are hardware loops.
loop = (id == SE.SE_LOW_HEALTH) or Audio._songHasGoto(slot)
end
local loopBody = loop and opts.loop == nil and id ~= SE.SE_LOW_HEALTH
-- pokefirered/src/battle_anim_special.c:1200
local cut = ((loop and not loopBody) or id == SE.SE_EXP)
local maxSec = opts.maxSec
or (cut and Audio.SE_LOOP_MAX_SEC or Audio.SE_ONESHOT_MAX_SEC)
rawL, rawR, loopStart = Player.bakeSlot(slot, {
raw = true,
maxSec = maxSec,
stopOnGoto = loop and true or false,
loopBody = loopBody,
})
if not cut and opts.maxSec == nil and type(rawL) == "table"
and #rawL >= math.floor(Mix.SAMPLE_RATE * maxSec) then
warn_once("selen:" .. tostring(id),
"SE " .. tostring(id) .. " hit the " .. tostring(maxSec) .. "s bake ceiling")
end
if memoable then Audio._seRawPut(id, loop and true or false, rawL, rawR, loopStart) end
loop, rawL, rawR, loopStart = bake_se(id, opts, memoable)
if loop == nil then return false end
end
local pan = Audio.normalizePan(opts.pan)
@@ -1045,6 +1068,7 @@ function Audio._buildSeSoundData(L, R, master, pan, mono)
master = master or 1
local gainL, gainR = Audio._seGains(pan)
local gl, gr = master * gainL, master * gainR
local warm = package.loaded["src.core.game3.warm"]
local ptr
if ffiOk and ffi and sd.getFFIPointer then
local okP, p = pcall(sd.getFFIPointer, sd)
@@ -1068,6 +1092,7 @@ function Audio._buildSeSoundData(L, R, master, pan, mono)
sd:setSample(i - 1, 1, l)
sd:setSample(i - 1, 2, r)
end
if warm and i % 4096 == 0 then warm.yield() end
end
return sd
end
@@ -1338,6 +1363,67 @@ function Audio.waitFanfare(cb)
if not Audio._fanfareActive and cb then cb() end
end
Audio.CRY_CACHE_MAX = 24
local function cry_inputs(mode, pan, volume)
mode = tonumber(mode) or 0
local cfg = Audio.config()
-- pokeruby/src/sound.c:364
if cfg.cryModeMax and (mode < 0 or mode > cfg.cryModeMax) then mode = 0 end
local params = Sample.cryParams(mode, volume or cfg.cryDefaultVolume, cfg.cryModeOverrides)
return params, pan and pan ~= 0 and Audio.normalizePan(pan) or nil
end
local function cry_meta(slot)
local cry = slot and slot.info
if not (cry and cry.cryIndex ~= nil) then return nil end
local c = Audio._pack.index.cries[cry.cryIndex]
return c and Audio._pack.samples[c.sampleId], cry.cryIndex
end
local function cry_render(slot, params, pan, mode, volume)
local meta, cryIndex = cry_meta(slot)
if not meta then return nil end
local key = table.concat({ tostring(cryIndex), tostring(mode), tostring(volume), tostring(pan),
Audio._mono and "m" or "s" }, ":")
Warm.flush("cry:" .. key)
local cache = Audio._cryCache or {}
Audio._cryCache = cache
local hit = cache[key]
if hit then return hit.sd, hit.info end
local pcm = Sample.loadPcm(Audio._pack.samplesBin, meta)
if not pcm then return nil end
local sd, info = Sample.renderCry(pcm, Mix.waveRate(meta.freq), params, {
outRate = Mix.SAMPLE_RATE,
pan = pan,
mono = Audio._mono,
})
if sd then
Audio._cryCacheN = (Audio._cryCacheN or 0) + 1
if Audio._cryCacheN > Audio.CRY_CACHE_MAX then
Audio._cryCache, Audio._cryCacheN = {}, 1
cache = Audio._cryCache
end
cache[key] = { sd = sd, info = info }
end
return sd, info
end
function Audio.prewarmCry(species, mode, pan, priority)
species = tonumber(species) or species
if species == nil or not Audio.isReady() then return false end
local params, npan = cry_inputs(mode, pan, nil)
local slot = Player.startCry(Audio._pack, species, { pitch = 1.0 })
local _, cryIndex = cry_meta(slot)
if cryIndex == nil then return false end
local key = table.concat({ tostring(cryIndex), tostring(tonumber(mode) or 0), "nil", tostring(npan),
Audio._mono and "m" or "s" }, ":")
if Audio._cryCache and Audio._cryCache[key] then return false end
return Warm.add("cry:" .. key, function()
cry_render(slot, params, npan, tonumber(mode) or 0, nil)
end, priority)
end
-- pokefirered/src/sound.c:333
function Audio.playCry(species, mode, pan)
species = tonumber(species) or species
@@ -1350,10 +1436,7 @@ function Audio.playCry(species, mode, pan)
noDuck = o.noDuck == true
end
mode = tonumber(mode) or 0
local cfg = Audio.config()
-- pokeruby/src/sound.c:364
if cfg.cryModeMax and (mode < 0 or mode > cfg.cryModeMax) then mode = 0 end
local params = Sample.cryParams(mode, volume or cfg.cryDefaultVolume, cfg.cryModeOverrides)
local params, npan = cry_inputs(mode, pan, volume)
local doubles = params.mode == 1 or noDuck
Audio._cryParams = params
log(string.format("playCry species=%s mode=%d", tostring(species), params.mode))
@@ -1367,25 +1450,14 @@ function Audio.playCry(species, mode, pan)
return false
end
Audio._crySlot = slot
local cry = slot.info
local meta = nil
if cry and cry.cryIndex ~= nil then
local c = Audio._pack.index.cries[cry.cryIndex]
if c then meta = Audio._pack.samples[c.sampleId] end
end
local pcm = meta and Sample.loadPcm(Audio._pack.samplesBin, meta)
if pcm then
if cry_meta(slot) then
Audio._crySource = nil
if not doubles then
Audio._duck = 85 / 256
Audio._duckHold = 2
apply_bgm_gain()
end
local sd, info = Sample.renderCry(pcm, Mix.waveRate(meta.freq), params, {
outRate = Mix.SAMPLE_RATE,
pan = pan and pan ~= 0 and Audio.normalizePan(pan) or nil,
mono = Audio._mono,
})
local sd, info = cry_render(slot, params, npan, mode, volume)
if info then
Audio._cryUntil = (Audio._cryClock or 0) + info.frames
end
+15
View File
@@ -81,6 +81,21 @@ local function frlgSheetKey(env, id)
return primary
end
function BattleBg.resolveOpts(opts)
local terrain = opts.terrain
-- pokefirered/src/battle_main.c:689
if terrain == nil and (opts.mapBehavior ~= nil or opts.mapType ~= nil) then
terrain = BattleBg.resolveFromBehavior(opts.mapBehavior, opts.mapKind, opts.mapType)
end
if terrain == nil and opts.mapKind then
terrain = BattleBg.resolveFromMapKind(opts.mapKind)
end
if terrain == nil then
terrain = BattleBg.TERRAIN.BUILDING
end
return terrain
end
function BattleBg.sheetKey(id)
id = tonumber(id) or BattleBg._terrainId
local env = BattleBg.env()
+1 -11
View File
@@ -679,17 +679,7 @@ function Battle.start(opts)
end
local BattleBg = require("src.core.game3.battle.bg")
local terrain = opts.terrain
-- pokefirered/src/battle_main.c:689
if terrain == nil and (opts.mapBehavior ~= nil or opts.mapType ~= nil) then
terrain = BattleBg.resolveFromBehavior(opts.mapBehavior, opts.mapKind, opts.mapType)
end
if terrain == nil and opts.mapKind then
terrain = BattleBg.resolveFromMapKind(opts.mapKind)
end
if terrain == nil then
terrain = BattleBg.TERRAIN.BUILDING
end
local terrain = BattleBg.resolveOpts(opts)
st.terrain = terrain
-- pokefirered/src/battle_bg.c:714
BattleBg.setTerrain(BattleBg.resolveOverride(terrain, {
+7 -2
View File
@@ -48,8 +48,13 @@ function Moves.loadRomPack(cache)
Moves._romLoaded = true
local root = (Extract.CACHE_ROOT or "data/generated/gba") .. "/pokemon/battle_moves.lua"
cache = cache or require("src.core.game3.dataset").cache()
local src = assert(cache:read(root), "pokemon/battle_moves.lua is not in the cache")
local pack = assert(load(src, "@" .. root, "t", {}))()
local chunk = Moves._romChunk
if not (chunk and Moves._romChunkCache == cache) then
local src = assert(cache:read(root), "pokemon/battle_moves.lua is not in the cache")
chunk = assert(load(src, "@" .. root, "t", {}))
Moves._romChunk, Moves._romChunkCache = chunk, cache
end
local pack = chunk()
Moves._rom = assert(pack and pack.moves, "pokemon/battle_moves.lua has no moves")
Moves._numByName = nil
build_names()
+1 -1
View File
@@ -243,7 +243,7 @@ function Ui.reset(opts)
Ui._oldManSubstate = nil
if Message and Message.isHeld and Message.isHeld() then Message.close() end
if not Ui._headless then
local okC, errC = pcall(BattleChrome.install, nil)
local okC, errC = pcall(BattleChrome.ensureInstalled)
if not okC and not chromeInstallWarned then
chromeInstallWarned = true
print("[game3/battle.ui] BattleChrome.install failed: " .. tostring(errC))
+8 -2
View File
@@ -146,8 +146,8 @@ function BattleBridge.installWhiteoutIntercept(mod, game)
end
BattleBridge._whiteoutHook = onWhiteout
local ok, World = pcall(require, "src.world.gen2.World")
if ok and World then
local World = package.loaded["src.world.gen2.World"]
if type(World) == "table" then
if type(World.whiteOut) == "function" and not World._game3WhiteOut then
local prev = World.whiteOut
World.whiteOut = function(self, ...)
@@ -398,6 +398,9 @@ function BattleBridge.start(mod, game, foe, opts)
if not session then return nil, "no session" end
BattleBridge.installWhiteoutIntercept(mod, game)
if not opts.headless then
pcall(function() require("src.core.game3.prewarm").battle(session, foe) end)
end
if opts.wild and require("src.core.game3.profile").family(session) == "rse" then
-- pokeemerald/src/battle_setup.c:417
require("src.core.game3.rse.init").call("tv", "incrementDailyWildBattles", "IncrementDailyWildBattles", nil)
@@ -659,6 +662,9 @@ function BattleBridge.start(mod, game, foe, opts)
local StayMessage = package.loaded["src.ui.game3.message"]
if StayMessage and StayMessage.closeStay then StayMessage.closeStay() end
end
if not opts.headless then
pcall(function() require("src.core.game3.prewarm").battleStart(startOpts) end)
end
local function doStart()
local ok, err = Battle.start(startOpts)
+34 -13
View File
@@ -376,11 +376,29 @@ function Dataset.mountExtractRoots()
end
local manifestLayouts = {}
local lazyLayouts, LazyDef
function Dataset.invalidateManifestCache()
manifestLayouts = {}
end
lazyLayouts = setmetatable({}, { __mode = "k" })
LazyDef = {
__index = function(def, k)
if k ~= "midLayout" then return nil end
local src = lazyLayouts[def]
if not src then return nil end
lazyLayouts[def] = nil
local blob = src.cache:read(src.rel)
local decoded = blob and require("src.import.gba.native_pack").decodeMidLayout(blob)
if not decoded then return nil end
local layout = require("src.core.game3.layout_native").fromDecoded(decoded, src.mapId, src.pair, blob)
rawset(def, "midLayout", layout)
return layout
end,
}
--- Bind LayoutNative handles onto map defs (FieldView needs midLayout).
function Dataset.attachMidLayouts(maps, cache)
if type(maps) ~= "table" then return 0 end
@@ -397,25 +415,28 @@ function Dataset.attachMidLayouts(maps, cache)
end
local attached = 0
for mapId, def in pairs(maps) do
if def and not def.midLayout then
if def and not rawget(def, "midLayout") and not lazyLayouts[def] then
local info = layouts[mapId]
local rel = nativeRoot .. "/"
.. ((info and info.file) or ("layouts/" .. mapId .. ".mid"))
local blob = cache:read(rel)
if blob then
local decoded = NativePack.decodeMidLayout(blob)
if decoded then
local pair = (info and info.pair) or def.pair
def.midLayout = LayoutNative.fromDecoded(decoded, mapId, pair, blob)
local tw = decoded.trueWidth or decoded.width
local th = decoded.trueHeight or decoded.height
if tw and tw > 0 then def.width = tw end
if th and th > 0 then def.height = th end
if pair then def.pair = pair end
attached = attached + 1
if blob and #blob >= 16 and blob:sub(1, 4) == NativePack.MAGIC_MID then
local pair = (info and info.pair) or def.pair
local tw = blob:byte(11) + blob:byte(12) * 256
local th = blob:byte(13) + blob:byte(14) * 256
if tw > 0 then def.width = tw end
if th > 0 then def.height = th end
if pair then def.pair = pair end
if getmetatable(def) == nil then
lazyLayouts[def] = { cache = cache, rel = rel, mapId = mapId, pair = pair }
setmetatable(def, LazyDef)
else
local decoded = NativePack.decodeMidLayout(blob)
if decoded then def.midLayout = LayoutNative.fromDecoded(decoded, mapId, pair, blob) end
end
attached = attached + 1
end
elseif def and def.midLayout then
elseif def then
attached = attached + 1
end
end
+1 -1
View File
@@ -146,7 +146,7 @@ local function drawFrlgReflections(camX, camY)
for j = 1, #live do
local obj = live[j]
if not obj.hideReflection and obj.graphicsId and not obj.virtualId then
local spr = Ow.getDraw(obj.graphicsId)
local spr = (Ow.peekDraw or Ow.getDraw)(obj.graphicsId)
if spr then
objectPoseOpts.frame = obj.customFrame
local frame, flip = Ow.pose(spr, obj.facing, Objects.walkPhase(obj), obj.stepFlip,
+2 -2
View File
@@ -682,7 +682,7 @@ local ghostPose = {}
local function drawReflection(obj, gid, frame, hflip, x2, y2, camX, camY, ox, oy)
local Ow = package.loaded["src.core.game3.ow_sprites"]
local spr = Ow and Ow.getDraw and Ow.getDraw(gid)
local spr = Ow and (Ow.peekDraw or Ow.getDraw)(gid)
if not (spr and spr.quads and spr.quads[frame]) then return false end
ox, oy = ox or 0, oy or 0
local cx = (obj.moving and obj.targetX or obj.cellX) + ox
@@ -871,7 +871,7 @@ local function drawReflections(camX, camY)
for j = 1, #live do
local eo = live[j]
if not eo.hideReflection and eo.graphicsId and not eo.virtualId then
local spr = Ow.getDraw(eo.graphicsId)
local spr = (Ow.peekDraw or Ow.getDraw)(eo.graphicsId)
if spr then
ghostPose.frame = eo.customFrame
local frame, flip = Ow.pose(spr, eo.facing, O.walkPhase(eo), eo.stepFlip, ghostPose)
+2 -1
View File
@@ -47,7 +47,8 @@ end
local function not_the_time(session)
if require("src.core.game3.profile").family(session) == "rse" then
-- pokeemerald/src/item_use.c:158 DisplayDadsAdviceCannotUseItemMessage
return (RomText.box("gText_DadsAdvice", { playerName = player_name(session) }))
-- pokeruby/src/item_use.c:146
return (RomText.box(is_rs() and "gOtherText_DadsAdvice" or "gText_DadsAdvice", { playerName = player_name(session) }))
end
return (RomText.box("gText_OakForbidsUseOfItemHere", { playerName = player_name(session) }))
end
+2
View File
@@ -590,6 +590,8 @@ function Player.bakeSlot(slot, opts)
else
idle = 0
end
local warm = package.loaded["src.core.game3.warm"]
if warm then warm.yield() end
end
if #L == 0 then
L[1] = 0
+4
View File
@@ -181,6 +181,7 @@ function Sample.renderCryMix(pcm, sampleRate, params, opts)
local gain = (tonumber(params.volume) or Sample.CRY_VOLUME) / 127
local reverse = params.reverse and true or false
local out = {}
local warm = package.loaded["src.core.game3.warm"]
for i = 0, nOut - 1 do
local g = env.gain(math.floor(i * frameRate / outRate)) * gain
local acc = 0
@@ -196,6 +197,7 @@ function Sample.renderCryMix(pcm, sampleRate, params, opts)
if acc > 1 then acc = 1 elseif acc < -1 then acc = -1 end
end
out[i + 1] = acc
if warm and i % 2048 == 2047 then warm.yield() end
end
local v1 = voices[1]
local sampleFrames = v1.samples * frameRate / outRate
@@ -223,6 +225,7 @@ function Sample.renderCry(pcm, sampleRate, params, opts)
gainL = (127 - pan) / 191
gainR = (128 + pan) / 191
end
local warm = package.loaded["src.core.game3.warm"]
for i = 0, #out - 1 do
local v = out[i + 1]
if channels == 2 then
@@ -231,6 +234,7 @@ function Sample.renderCry(pcm, sampleRate, params, opts)
else
sd:setSample(i, v * (gainL + gainR) * 0.5)
end
if warm and i % 2048 == 2047 then warm.yield() end
end
return sd, info
end
+9
View File
@@ -405,6 +405,15 @@ function OwSprites.getDraw(graphicsId)
return OwSprites.get(graphicsId)
end
function OwSprites.peekDraw(graphicsId)
graphicsId = tonumber(graphicsId)
if graphicsId == nil then return nil end
local ov = OwSprites._overrides and OwSprites._overrides[graphicsId]
if ov or OwSprites._loaded[graphicsId] or not OwSprites._stream then return OwSprites.getDraw(graphicsId) end
OwSprites.prefetch(graphicsId, 0)
return nil
end
local function paletteRgb(colors)
if type(colors) ~= "table" then return nil end
local out = {}
+96
View File
@@ -0,0 +1,96 @@
local Warm = require("src.core.game3.warm")
local Prewarm = {}
Prewarm.FIELD_SE = { "SE_SELECT", "SE_WIN_OPEN", "SE_WALL_HIT", "SE_EXIT", "SE_RS_DOOR", "SE_LEDGE",
"SE_SLIDING_DOOR", "SE_PIN" }
Prewarm.BATTLE_SE = { "SE_BALL_OPEN", "SE_BALL_THROW", "SE_FAINT", "SE_NOT_EFFECTIVE", "SE_EFFECTIVE",
"SE_SUPER_EFFECTIVE", "SE_EXP" }
Prewarm.MODULES = {
"src.core.game3.party", "src.core.game3.trainer_sight", "src.core.game3.warp",
"src.core.game3.battle_downgrade", "src.core.game3.battle_bridge", "src.core.game3.battle_transition",
"src.core.game3.battle.ai", "src.core.game3.battle.ai_cmds", "src.core.game3.battle.ai_vm",
"src.core.game3.battle.ai_items", "src.core.game3.battle.link_guard", "src.ui.game3.stat_growth",
"src.ui.game3.map_name_popup",
}
Prewarm.FAMILY_MODULES = {
rse = { "src.core.game3.audio_policy_rse", "src.ui.game3.rse.mapsec", "src.ui.game3.rse.scene_kit",
"src.core.game3.battle_transition_ids_rse", "src.core.game3.battle_transition_rse_a",
"src.core.game3.battle_transition_rse_b", "src.core.game3.battle_transition_rse_frontier" },
frlg = { "src.core.game3.battle_transition_ids_frlg" },
}
Prewarm.FONT_FACES = { "normal", "small", "short", "narrow", "small_narrow" }
Prewarm.CONSTANTS = { "weather", "region_map_sections", "trainer_classes", "moves", "abilities" }
local function se_list(names, priority)
local Audio = package.loaded["src.core.game3.audio"]
if not Audio then return end
for _, name in ipairs(names) do Audio.prewarmSe(name, priority) end
end
function Prewarm.session(session)
local Profile = require("src.core.game3.profile")
local family = Profile.family(session)
Warm.require(Prewarm.MODULES, 6)
Warm.require(Prewarm.FAMILY_MODULES[family] or {}, 6)
local okG, game = pcall(function() return require("src.core.GameVersion").get() end)
if okG and game then
for _, kind in ipairs(Prewarm.CONSTANTS) do
Warm.add("const:" .. tostring(game) .. ":" .. kind, function()
local C = require("src.core.game3.constants").of(game)
local _ = C[kind]
end, 7)
end
end
for _, face in ipairs(Prewarm.FONT_FACES) do
Warm.add("font:" .. face, function()
local Font = require("src.ui.game3.frlg_font")
if Font.face then pcall(Font.face, { font = face }) end
end, 5)
end
Warm.add("data:pic_coords", function() require("src.core.game3.battle.pic_coords").active() end, 7)
Warm.add("data:mon_anim", function() require("src.core.game3.mon_anim_data").get() end, 7)
Warm.add("data:moves", function() require("src.core.game3.battle.moves").loadRomPack() end, 7)
se_list(Prewarm.FIELD_SE, 9)
end
local function species_of(mon)
if type(mon) ~= "table" then return nil end
return tonumber(mon.species or mon.speciesId)
end
local function lead_of(party)
for _, mon in ipairs(type(party) == "table" and party or {}) do
if type(mon) == "table" and not mon.isEgg and not mon.egg and (tonumber(mon.hp) or 1) > 0 then return mon end
end
end
function Prewarm.battle(session, foe)
local Audio = package.loaded["src.core.game3.audio"]
if not Audio then return end
local lead = lead_of(session and session.party)
local IntroSeq = package.loaded["src.core.game3.battle.intro_seq"]
local mode = lead and IntroSeq and IntroSeq.releaseCryMode and IntroSeq.releaseCryMode(lead) or 0
if species_of(lead) then Audio.prewarmCry(species_of(lead), mode, -25, 1) end
local first = foe and ((type(foe.party) == "table" and foe.party[1]) or foe)
if species_of(first) then Audio.prewarmCry(species_of(first), 0, 25, 1) end
se_list(Prewarm.BATTLE_SE, 2)
end
function Prewarm.battleStart(opts)
if type(opts) ~= "table" then return end
Warm.add("battle:chrome", function()
require("src.ui.game3.battle_chrome").ensureInstalled()
local BattleBg = require("src.core.game3.battle.bg")
require("src.ui.game3.battle_chrome").terrain(BattleBg.sheetKey(BattleBg.resolveOpts(opts)))
end, 1)
Warm.add("battle:back", function()
require("src.core.game3.trainer_pic").back(tonumber(opts.playerGender) or 0)
end, 1)
end
return Prewarm
+4 -2
View File
@@ -466,7 +466,8 @@ function Runtime.install(mod)
Runtime._mod = mod
log("Runtime.install — display ownership + START intercept + game.ready")
local ok, World = pcall(lazyReq, "src.world.gen2.World")
local World = package.loaded["src.world.gen2.World"]
local ok = type(World) == "table"
if ok and World and World.step and not World._game3RuntimeStep then
local prev = World.step
World.step = function(self, ...)
@@ -507,7 +508,8 @@ function Runtime.install(mod)
end
-- Own the frame: replace Gen2 drawScene presentation while active.
local ok2, Game2 = pcall(lazyReq, "src.core.Game2")
local Game2 = package.loaded["src.core.Game2"]
local ok2 = type(Game2) == "table"
if ok2 and Game2 and Game2.drawScene and not Game2._game3Display then
local prevDraw = Game2.drawScene
Game2.drawScene = function(self, w, h)
+4 -2
View File
@@ -744,7 +744,8 @@ function Space.install(mod)
end
end
-- Direct Gen2 World:setMap (ferry/warps often skip the facade).
local ok, World = pcall(lazyReq, "src.world.gen2.World")
local World = package.loaded["src.world.gen2.World"]
local ok = type(World) == "table"
if ok and World and World.setMap and not World._game3SetMap then
local prevW = World.setMap
World.setMap = function(self, mapId, ...)
@@ -796,7 +797,8 @@ function Space.install(mod)
-- Gen2 World:busy must see game3 scripts or frozeNpcs clears mid-dialog.
do
local ok, World = pcall(lazyReq, "src.world.gen2.World")
local World = package.loaded["src.world.gen2.World"]
local ok = type(World) == "table"
if ok and World and World.busy and not World._game3Busy then
local prevBusy = World.busy
World.busy = function(self)
+54 -7
View File
@@ -18,6 +18,12 @@ TilesetAnim._enabled = true
local MID_RGBA = 16 * 16 * 4 -- 1024
local nativeMod
local function NativeTileset()
nativeMod = nativeMod or require("src.core.game3.tileset_native")
return nativeMod
end
local function log(msg)
print("[game3/anim] " .. tostring(msg))
end
@@ -167,6 +173,7 @@ local function rse_paste(entry, bank, blob, mids, quads, frame, imageData, lut,
if piece then
local ax, ay = (slot % cols) * 16, math.floor(slot / cols) * 16
local mask = quads and quads[k] or 15
NativeTileset().markDirty(ts, over, slot)
if mask == 15 then
imageData:paste(piece, ax, ay, 0, 0, 16, 16)
else
@@ -234,12 +241,6 @@ end
local rseDirty = {}
local nativeMod
local function NativeTileset()
nativeMod = nativeMod or require("src.core.game3.tileset_native")
return nativeMod
end
-- pokeemerald/src/tileset_anims.c:586
function TilesetAnim.stepRse()
local st = TilesetAnim._rse
@@ -295,6 +296,49 @@ local function load_bank(cache, pair, kind, info)
}
end
local get_frame_piece
local function prewarm(pair, entry)
local Warm = require("src.core.game3.warm")
Warm.add("anim:" .. pair, function()
if TilesetAnim._pairs[pair] ~= entry then return end
NativeTileset().ensureAlt(entry.atlas)
if entry.rse then
for _, bank in ipairs(entry.banks) do
local row = bank and bank.row
if row and row.kind == "palette" then
if row.paletteSlot then NativeTileset().prepareSlot(entry.atlas, tonumber(row.paletteSlot) or 0) end
elseif row then
local frames = tonumber(row.frames) or 0
for frame = 0, frames - 1 do
local n, on = #(row.mids or EMPTY), #(row.overMids or EMPTY)
for k = 1, n do
if bank.under then rse_piece(entry, bank, bank.under, entry.lut, frame, k, n, false) end
if k % 4 == 0 then Warm.yield() end
end
for k = 1, on do
if bank.over then rse_piece(entry, bank, bank.over, entry.lutOver, frame, k, on, true) end
if k % 4 == 0 then Warm.yield() end
end
Warm.yield()
end
end
end
else
for _, bank in pairs(entry.banks) do
local nMids = #bank.mids
for frame = 0, bank.frames - 1 do
for mi = 1, nMids do
get_frame_piece(bank, frame, mi, (frame * nMids + (mi - 1)) * MID_RGBA)
if mi % 8 == 0 then Warm.yield() end
end
Warm.yield()
end
end
end
end, 3)
end
-- pokefirered/src/tileset_anims.c:223
function TilesetAnim.bindPair(pair, atlas, prepared)
if not Versions.NATIVE_RENDER or Versions.TILESET_ANIM == false then
@@ -314,6 +358,7 @@ function TilesetAnim.bindPair(pair, atlas, prepared)
entry = rse_entry(cache, pair, atlas, man)
TilesetAnim._pairs[pair] = entry
if not entry then return false end
prewarm(pair, entry)
if not TilesetAnim._rse then TilesetAnim.enterMap(pair, false) end
TilesetAnim._visible[pair] = true
return true
@@ -324,6 +369,7 @@ function TilesetAnim.bindPair(pair, atlas, prepared)
flower = load_bank(cache, pair, "flower", man.flower),
} }
TilesetAnim._pairs[pair] = entry
prewarm(pair, entry)
end
TilesetAnim._visible[pair] = true
if entry.rse then return true end
@@ -356,7 +402,7 @@ function TilesetAnim.setVisiblePairs(visible)
end
end
local function get_frame_piece(bank, frame, mi, srcOff)
function get_frame_piece(bank, frame, mi, srcOff)
if not (love and love.image and love.image.newImageData) then return nil end
local pieces = bank.pieces
if not pieces then
@@ -395,6 +441,7 @@ function TilesetAnim._applyKind(entry, kind, frame, deferUpload)
local ax = (slot % cols) * 16
local ay = math.floor(slot / cols) * 16
local piece = get_frame_piece(bank, frame, mi, srcOff)
NativeTileset().markDirty(ts, false, slot)
local pasted = false
if piece and ts.imageData.paste then
ts.imageData:paste(piece, ax, ay)
+80 -11
View File
@@ -166,11 +166,16 @@ local function scan_slot(blob, cols, slot, skipZero)
local base = 13 + midCount * 2
local lo, hi = slot * 16, slot * 16 + 15
local n = 0
local cells = {}
out.cells = cells
local warm = package.loaded["src.core.game3.warm"]
for i = 0, midCount * 256 - 1 do
local b = blob:byte(base + i)
if warm and i % 16384 == 16383 then warm.yield() end
if b and b >= lo and b <= hi and not (skipZero and b == 0) then
local mid = math.floor(i / 256)
local within = i % 256
cells[mid] = true
n = n + 1
out[n] = {
(mid % cols) * 16 + within % 16,
@@ -200,43 +205,98 @@ local function retarget(old, new)
end
end
local function flip(ts, imgKey, dataKey, altKey)
local scratch
local function cellData(data, x, y)
if not scratch then scratch = love.image.newImageData(16, 16) end
scratch:paste(data, 0, 0, x, y, 16, 16)
return scratch
end
local function merge(pend, slots, full)
if full or not slots then
pend.full, pend.slots = true, nil
elseif not pend.full then
pend.slots = pend.slots or {}
for slot in pairs(slots) do pend.slots[slot] = true end
end
end
local function apply(ts, image, data, pend)
if pend.full then
image:replacePixels(data)
elseif pend.slots then
local cols = ts.cols or 16
for slot in pairs(pend.slots) do
local x, y = (slot % cols) * 16, math.floor(slot / cols) * 16
image:replacePixels(cellData(data, x, y), 1, 1, x, y)
end
end
pend.full, pend.slots = false, nil
end
local function flip(ts, imgKey, dataKey, altKey, slots)
local img, data = ts[imgKey], ts[dataKey]
if not (img and data and img.replacePixels) then return end
ts._pend = ts._pend or setmetatable({}, { __mode = "k" })
local pend = ts._pend
local alt = ts[altKey]
local full = not slots or not next(slots)
if alt and alt.replacePixels then
alt:replacePixels(data)
pend[alt] = pend[alt] or {}
merge(pend[alt], slots, full)
apply(ts, alt, data, pend[alt])
elseif love and love.graphics and love.graphics.newImage then
alt = love.graphics.newImage(data)
if alt.setFilter then alt:setFilter("nearest", "nearest") end
pend[alt] = {}
else
img:replacePixels(data)
return
end
pend[img] = pend[img] or {}
merge(pend[img], slots, full)
ts[imgKey], ts[altKey] = alt, img
retarget(img, alt)
end
function NativeTileset.flush(ts, under, over)
if under then flip(ts, "image", "imageData", "imageAlt") end
if over then flip(ts, "overImage", "overImageData", "overImageAlt") end
function NativeTileset.markDirty(ts, over, slot)
local key = over and "_dirtyOver" or "_dirtyUnder"
local set = ts[key]
if not set then set = {}; ts[key] = set end
set[slot] = true
end
local function paint(imageData, list, colors)
function NativeTileset.flush(ts, under, over)
local du, dov = ts._dirtyUnder, ts._dirtyOver
ts._dirtyUnder, ts._dirtyOver = nil, nil
if under then flip(ts, "image", "imageData", "imageAlt", du) end
if over then flip(ts, "overImage", "overImageData", "overImageAlt", dov) end
end
function NativeTileset.ensureAlt(ts)
for _, k in ipairs({ { "image", "imageData", "imageAlt" }, { "overImage", "overImageData", "overImageAlt" } }) do
local img, data = ts[k[1]], ts[k[2]]
if img and data and not ts[k[3]] and love and love.graphics and love.graphics.newImage then
local alt = love.graphics.newImage(data)
if alt.setFilter then alt:setFilter("nearest", "nearest") end
ts[k[3]] = alt
end
end
end
local function paint(ts, over, imageData, list, colors)
if not (imageData and #list > 0) then return false end
for i = 1, #list do
local p = list[i]
local c = colors[p[3]]
imageData:setPixel(p[1], p[2], c[1] / 255, c[2] / 255, c[3] / 255, 1)
end
for slot in pairs(list.cells or {}) do NativeTileset.markDirty(ts, over, slot) end
return true
end
-- pokefirered/src/palette.c:88
function NativeTileset.setSlotPalette(pairOrTs, slot, bgr16)
local ts = type(pairOrTs) == "table" and pairOrTs or NativeTileset.get(pairOrTs)
if not (ts and ts.imageData and type(bgr16) == "table") then return false end
slot = tonumber(slot) or 0
function NativeTileset.prepareSlot(ts, slot)
local pix = ts.slotPix[slot]
if not pix then
pix = {
@@ -245,10 +305,19 @@ function NativeTileset.setSlotPalette(pairOrTs, slot, bgr16)
}
ts.slotPix[slot] = pix
end
return pix
end
function NativeTileset.setSlotPalette(pairOrTs, slot, bgr16)
local ts = type(pairOrTs) == "table" and pairOrTs or NativeTileset.get(pairOrTs)
if not (ts and ts.imageData and type(bgr16) == "table") then return false end
slot = tonumber(slot) or 0
local pix = NativeTileset.prepareSlot(ts, slot)
local src = {}
for c = 0, 15 do src[c] = bgr16[c + 1] or 0 end
local colors = NativePack.palsToRgb8({ [0] = src })[0]
NativeTileset.flush(ts, paint(ts.imageData, pix.under, colors), paint(ts.overImageData, pix.over, colors))
NativeTileset.flush(ts, paint(ts, false, ts.imageData, pix.under, colors),
paint(ts, true, ts.overImageData, pix.over, colors))
ts.patchedSlots = ts.patchedSlots or {}
ts.patchedSlots[slot] = true
return true
+83
View File
@@ -0,0 +1,83 @@
local Warm = {}
Warm.BUDGET = 0.002
local jobs, order = {}, {}
local clock = function() return love and love.timer and love.timer.getTime() or os.clock() end
function Warm.add(key, fn, priority)
if jobs[key] then
if priority and priority < jobs[key].priority then jobs[key].priority = priority end
return false
end
local job = { key = key, fn = fn, priority = priority or 5 }
jobs[key] = job
order[#order + 1] = job
return true
end
function Warm.pending(key) return jobs[key] ~= nil end
local function finish(job)
jobs[job.key] = nil
for i = #order, 1, -1 do
if order[i] == job then table.remove(order, i) break end
end
end
local function resume(job)
if not job.co then job.co = coroutine.create(job.fn) end
local ok, err = coroutine.resume(job.co)
if not ok then
print("[game3/warm] " .. tostring(job.key) .. ": " .. tostring(err))
finish(job)
elseif coroutine.status(job.co) == "dead" then
finish(job)
end
end
function Warm.flush(key)
local job = jobs[key]
if not job or (job.co and job.co == coroutine.running()) then return false end
job.flushing = true
while jobs[key] == job do resume(job) end
return true
end
function Warm.step(budget)
if #order == 0 then return end
local deadline = clock() + (budget or Warm.BUDGET)
repeat
local best
for _, job in ipairs(order) do
if not best or job.priority < best.priority then best = job end
end
if not best then return end
resume(best)
until clock() >= deadline
end
function Warm.require(names, priority)
for _, name in ipairs(names) do
if not package.loaded[name] then
Warm.add("mod:" .. name, function() pcall(require, name) end, priority or 8)
end
end
end
function Warm.yield()
local co, main = coroutine.running()
if co and not main then
for _, job in pairs(jobs) do
if job.co == co then
if not job.flushing then coroutine.yield() end
return
end
end
end
end
function Warm.reset()
jobs, order = {}, {}
end
return Warm
+8 -7
View File
@@ -959,12 +959,13 @@ function PaletteFX.shadeMap()
end
local function invalidateColorCaches()
pcall(function() require("src.battle.BattleState").invalidate() end)
pcall(function() require("src.render.SpriteRenderer").invalidate() end)
local loaded = package.loaded
pcall(function() if loaded["src.battle.BattleState"] then loaded["src.battle.BattleState"].invalidate() end end)
pcall(function() if loaded["src.render.SpriteRenderer"] then loaded["src.render.SpriteRenderer"].invalidate() end end)
pcall(function()
require("src.world.MapLoader").invalidateAll()
local Game = require("src.core.Game")
if Game.overworld and Game.overworld.map and Game.overworld.reloadMap then
if loaded["src.world.MapLoader"] then loaded["src.world.MapLoader"].invalidateAll() end
local Game = loaded["src.core.Game"]
if type(Game) == "table" and Game.overworld and Game.overworld.map and Game.overworld.reloadMap then
Game.overworld:reloadMap(Game.overworld.map.id, "colors")
end
end)
@@ -995,8 +996,8 @@ function PaletteFX.setCustomRamp(ramp)
end
function PaletteFX.pickerActive()
local ok, Game = pcall(require, "src.core.Game")
local states = ok and Game.stack and Game.stack.states
local Game = package.loaded["src.core.Game"]
local states = type(Game) == "table" and Game.stack and Game.stack.states or nil
local top = states and states[#states]
return top ~= nil and top.screenId == "PaletteScreen"
end
+4 -4
View File
@@ -970,8 +970,8 @@ function Renderer:endFrame(zones, worldZones)
local extendedBlackBand = false
local bandR, bandG, bandB = 1, 1, 1
if not self.worldActive then
local ok, Game = pcall(require, "src.core.Game")
local stack = ok and Game and Game.stack
local Game = package.loaded["src.core.Game"]
local stack = type(Game) == "table" and Game.stack
local base = stack and stack.visibleBase and stack:visibleBase()
local state = base and stack.states and stack.states[base]
local ownState = self.surroundState
@@ -1214,8 +1214,8 @@ function Renderer:endFrame(zones, worldZones)
-- field, so never cover it with the native back-sprite fallback.
if self.extendedWorldBand and not self.worldOverride
and not FaithfulRes.scaleCap() then
local ok, Game = pcall(require, "src.core.Game")
love.graphics.setColor(PaletteFX.paperShade(ok and Game and Game.data))
local Game = package.loaded["src.core.Game"]
love.graphics.setColor(PaletteFX.paperShade(type(Game) == "table" and Game.data or nil))
love.graphics.rectangle("fill", uox, vuy, uvpw, vuh)
love.graphics.setColor(1, 1, 1, 1)
end
+10
View File
@@ -161,8 +161,18 @@ local TERRAIN_MT = {
__index = function(_, key) return BattleChrome.terrain(key) end,
}
local function install_key(cache)
return tostring(cache) .. "|" .. battle_root()
end
function BattleChrome.ensureInstalled()
if BattleChrome._installKey == install_key(resolve_cache(nil)) and BattleChrome._manifest then return end
BattleChrome.install(nil)
end
function BattleChrome.install(cache)
BattleChrome._cache = resolve_cache(cache)
BattleChrome._installKey = install_key(BattleChrome._cache)
BattleChrome._manifest = nil
BattleChrome._textbox = nil
BattleChrome._playerBox = nil
+67 -7
View File
@@ -19,6 +19,21 @@ local WIN = {
yesno = Window.template(9, 7, 5, 4),
}
ItemStorage.WIN = WIN
-- pokeruby/src/player_pc.c:973
local WIN_RS = {
list = WIN.list,
message = WIN.message,
title = Window.template(1, 1, 10, 2),
-- pokeruby/src/player_pc.c:606
quantity = Window.template(7, 9, 6, 2),
-- pokeruby/src/player_pc.c:703
yesno = Window.template(8, 7, 5, 4),
}
ItemStorage.WIN_RS = WIN_RS
local function is_rs()
return require("src.ui.game3.rs.player_pc_policy").matches(ItemStorage._session)
end
-- pokeemerald/src/player_pc.c:283
local ITEM_X, UP_TEXT_Y = 8, 9
-- pokeemerald/src/player_pc.c:699
@@ -53,10 +68,12 @@ local function pos()
end
local function narrow()
if is_rs() then return { font = "normal" } end
return { font = "narrow" }
end
local function row_height()
if is_rs() then return 16 end
local face = FrlgFont.face and FrlgFont.face(narrow())
return face and face.height or 16
end
@@ -384,7 +401,44 @@ local function frame(tpl)
Window.fill(tpl, 1, 1, 1, 1)
end
-- pokeruby/src/player_pc.c:862
local function draw_list_rs()
local tpl = WIN_RS.list
frame(tpl)
local ox = tpl.left * 8
local list = items()
local shown = page_items()
local font = narrow()
local ItemsData = require("src.core.game3.items_data")
for i = 0, shown - 1 do
local idx = ItemStorage.scroll + i
local y = (i * 2 + 2) * 8
local e = list[idx + 1]
if not e then
FrlgFont.draw(RomText.plain("gText_Cancel2"), ox, y,
{ font = "normal", colors = { fg = FrlgFont.STDPAL[1], bg = { 0, 0, 0, 0 }, shadow = FrlgFont.STDPAL[8] } })
break
end
local colors = { fg = FrlgFont.STDPAL[ItemStorage.swapFrom == idx and 2 or 1], bg = { 0, 0, 0, 0 },
shadow = FrlgFont.STDPAL[8] }
FrlgFont.draw(item_name(e.id), ox, y, { font = "normal", colors = colors })
local pocket = ItemsData.pocketOf(e.id)
-- pokeruby/src/player_pc.c:815
if pocket ~= "KEY_ITEMS" and not ItemsData.isHm(e.id) then
local qx = 26 * 8
FrlgFont.draw("×", qx, y, { font = "normal", colors = colors })
local digits = tostring(tonumber(e.qty) or 0)
local dx = qx + FrlgFont.measure("×", font) + (3 - #digits) * 6
for c = 1, #digits do
FrlgFont.draw(digits:sub(c, c), dx + (c - 1) * 6, y, { font = "normal", colors = colors })
end
end
end
require("src.ui.game3.rs.menu_cursor").draw(ox, 16 + ItemStorage.row * 16, 13 * 8)
end
local function draw_list()
if is_rs() then return draw_list_rs() end
local tpl = WIN.list
frame(tpl)
local ox, oy = tpl.left * 8, tpl.top * 8
@@ -427,14 +481,20 @@ end
function ItemStorage.draw()
if not ItemStorage.open then return end
local title = WIN.title
local rs = is_rs()
local W = rs and WIN_RS or WIN
local title = W.title
frame(title)
local t = RomText.plain(ItemStorage._toss and "gText_TossItem" or "gText_WithdrawItem")
-- pokeemerald/src/player_pc.c:960
Window.printPx(t, title.left * 8 + math.floor((104 - FrlgFont.measure(t)) / 2), title.top * 8 + 1)
if rs then
Window.printPx(t, title.left * 8, title.top * 8)
else
-- pokeemerald/src/player_pc.c:960
Window.printPx(t, title.left * 8 + math.floor((104 - FrlgFont.measure(t)) / 2), title.top * 8 + 1)
end
draw_list()
draw_icon()
local m = WIN.message
if not rs then draw_icon() end
local m = W.message
frame(m)
local pitch = FrlgFont.linePitch()
local y = m.top * 8 + 1
@@ -443,13 +503,13 @@ function ItemStorage.draw()
y = y + pitch
end
if ItemStorage.state == "quantity" then
local q = WIN.quantity
local q = W.quantity
frame(q)
local s = RomText.plain("gText_xVar1", { stringVars = { string.format("%03d", ItemStorage.quantity) } })
-- pokeemerald/src/player_pc.c:1361
Window.printPx(s, q.left * 8 + math.floor((48 - FrlgFont.measure(s)) / 2), q.top * 8 + 1)
elseif ItemStorage.state == "yesno" then
local yn = WIN.yesno
local yn = W.yesno
frame(yn)
local ph = Window.optionHeight()
Window.printPx(RomText.plain("gText_Yes"), yn.left * 8 + 8, yn.top * 8 + 1)
+57
View File
@@ -0,0 +1,57 @@
local U = require("tests.drivers.util")
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()
print(string.format("[driver] em_sendout_profile: %d failure(s)", fails))
love.event.quit(fails == 0 and 0 or 1)
U.wait(10)
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 Ui = require("src.core.game3.battle.ui")
local Map = require("src.core.game3.map")
local C = require("src.core.game3.constants").of("emerald")
local session = Runtime.getSession()
if not session then result(false, "field session"); return finish() end
local ok = pcall(function() Map.load(nil, game, "EM_ROUTE101", { x = 7, y = 13, facing = "down" }) end)
result(ok, "Route 101 loads")
U.wait(120)
session.party = {}
-- pokeemerald/src/battle_setup.c:923
Party.giveMon(session, C.species.byName.SPECIES_TREECKO, 5, "TREECKO")
print("SENDOUT_MARK battle_start")
local started = BattleBridge.startFirstBattle(Runtime._mod, game, { done = function() end })
result(started == true, "first battle started")
if not started then return finish() end
local lastTap, f = 0, 0
for _ = 1, 4000 do
if Battle.isActive() and Battle._phase == "command" and Ui._mode == "menu" 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(Battle._phase == "command" and Ui._mode == "menu", "reached the action menu after send-out")
U.wait(30)
finish()
end
+74
View File
@@ -0,0 +1,74 @@
local U = require("tests.drivers.util")
local BIG = {}
local function census()
collectgarbage("collect")
print(string.format("HEAP_AFTER_COLLECT_KB %.0f", collectgarbage("count")))
local seen, rows = {}, {}
local function size(t, depth)
if seen[t] then return 0, 0 end
seen[t] = true
local slots, bytes = 0, 0
for k, v in next, t do
slots = slots + 1
if type(v) == "string" then
bytes = bytes + #v
if #v > 262144 then BIG[#BIG + 1] = { #v, tostring(k) } end
end
if type(k) == "string" then bytes = bytes + #k end
end
return slots, bytes
end
local function walk(t, path, depth)
if type(t) ~= "table" or seen[t] or depth > 12 then return 0 end
local slots, bytes = size(t, depth)
local total = slots * 40 + bytes
for k, v in next, t do
if type(v) == "table" then
total = total + walk(v, path .. "." .. tostring(k), depth + 1)
elseif type(v) == "function" then
for i = 1, 60 do
local n, uv = debug.getupvalue(v, i)
if not n then break end
if type(uv) == "table" then total = total + walk(uv, path .. "." .. tostring(k) .. "^" .. n, depth + 1) end
end
end
end
rows[#rows + 1] = { path, total, slots }
return total
end
for name, mod in pairs(package.loaded) do
if type(mod) == "table" then walk(mod, name, 0) end
if type(mod) == "function" then
for i = 1, 60 do
local n, uv = debug.getupvalue(mod, i)
if not n then break end
if type(uv) == "table" then walk(uv, name .. "^" .. n, 1) end
end
end
end
walk(debug.getregistry(), "REG", 0)
table.sort(rows, function(a, b) return a[2] > b[2] end)
local bigTotal = 0
table.sort(BIG, function(a, b) return a[1] > b[1] end)
for i, b in ipairs(BIG) do bigTotal = bigTotal + b[1]; if i <= 25 then print(string.format("BIGSTR %8.0fKB key=%s", b[1] / 1024, b[2])) end end
print(string.format("BIGSTR_TOTAL %.0fKB n=%d", bigTotal / 1024, #BIG))
print(string.format("HEAP_TOTAL_KB %.0f", collectgarbage("count")))
local shown = 0
for _, r in ipairs(rows) do
if shown >= 60 then break end
print(string.format("HEAP %8.0fKB slots=%-8d %s", r[2] / 1024, r[3], r[1]))
shown = shown + 1
end
end
return function(game)
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 })
U.wait(300)
census()
love.event.quit(0)
end
+100
View File
@@ -0,0 +1,100 @@
local INNER = assert(os.getenv("HITCH_INNER"), "HITCH_INNER=tests/drivers/<driver>.lua")
local LIMIT_MS = tonumber(os.getenv("HITCH_MS") or "8") or 8
local DEPTH = tonumber(os.getenv("HITCH_DEPTH") or "10") or 10
if os.getenv("HITCH_JIT") ~= "1" and jit then jit.off(); jit.flush() end
local okProf, profile = pcall(require, "jit.profile")
if os.getenv("HITCH_STACKS") == "0" then okProf = false end
local frame, stacks, vm, samples = 0, {}, {}, 0
local hitches, all, known = {}, {}, {}
local frameStart, stuckShown
local clock = love.timer.getTime
local function top(map, n)
local rows = {}
for k, v in pairs(map) do rows[#rows + 1] = { k, v } end
table.sort(rows, function(a, b) return a[2] > b[2] end)
local out = {}
for i = 1, math.min(n, #rows) do out[i] = rows[i] end
return out
end
if okProf then
profile.start("i1", function(th, n, state)
samples = samples + n
vm[state] = (vm[state] or 0) + n
local s = profile.dumpstack(th, "pl < ", DEPTH)
stacks[s] = (stacks[s] or 0) + n
if frameStart and clock() - frameStart > 5 and not stuckShown then
stuckShown = true
print("HITCH_STUCK " .. profile.dumpstack(th, "pl\n", 40))
end
end)
end
local function reset() stacks, vm, samples = {}, {}, 0 end
local baseUpdate, baseDraw = love.update, love.draw
local tu, td, kb0
love.update = function(dt)
frame = frame + 1
reset()
kb0 = collectgarbage("count")
local t = clock()
frameStart = t
baseUpdate(dt)
tu = (clock() - t) * 1000
end
love.draw = function()
local t = clock()
baseDraw()
td = (clock() - t) * 1000
local total = (tu or 0) + td
all[#all + 1] = total
if os.getenv("HITCH_STATUS") and frame % 60 == 0 then
local B = package.loaded["src.core.game3.battle"]
local T = package.loaded["src.core.game3.battle_transition"]
print(string.format("HITCH_STATUS frame=%d battle=%s phase=%s transition=%s",
frame, tostring(B and B.isActive and B.isActive()), tostring(B and B._phase),
tostring(T and T._transitionId)))
end
for name in pairs(package.loaded) do
if not known[name] then
known[name] = true
if frame > 1 then print(string.format("LAZY_REQUIRE frame=%d %s", frame, name)) end
end
end
if total >= LIMIT_MS then
local Map = package.loaded["src.core.game3.map"]
hitches[#hitches + 1] = total
print(string.format("HITCH frame=%d total=%.1fms update=%.1f draw=%.1f gcKB=%+.0f heapKB=%.0f map=%s samples=%d vm[N=%d I=%d C=%d G=%d J=%d]",
frame, total, tu or 0, td, collectgarbage("count") - (kb0 or 0), collectgarbage("count"),
tostring(Map and Map.current), samples, vm.N or 0, vm.I or 0, vm.C or 0, vm.G or 0, vm.J or 0))
for _, row in ipairs(top(stacks, 6)) do
print(string.format(" %3d %s", row[2], row[1]))
end
end
end
local quit = love.event.quit
love.event.quit = function(...)
table.sort(all)
local n = #all
local function p(q) return all[math.max(1, math.ceil(n * q))] or 0 end
print(string.format("HITCH_SUMMARY frames=%d hitches=%d p50=%.2f p95=%.2f p99=%.2f max=%.2f jit=%s",
n, #hitches, p(.5), p(.95), p(.99), all[n] or 0, tostring(jit and jit.status())))
local foreign = {}
for name in pairs(package.loaded) do
if name:find("^src%.[%w_]+%.gen2%.") or name:find("^src%.[%w_]+%.gen2$")
or name == "src.core.Game" or name == "src.core.Game2" or name == "src.world.OverworldController"
or name == "src.battle.BattleState" then
foreign[#foreign + 1] = name
end
end
table.sort(foreign)
print(string.format("FOREIGN_MODULES n=%d %s", #foreign, table.concat(foreign, " ")))
if okProf then profile.stop() end
return quit(...)
end
return assert(loadfile(INNER))()
+64
View File
@@ -0,0 +1,64 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/rs_bike_pc_items"
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") .. " rs_bike_pc_items 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
game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 })
U.wait(240)
local Runtime = require("src.core.game3.runtime")
local session = Runtime.getSession()
if not check(session ~= nil, "session") then return finish() end
local C = require("src.core.game3.constants").active(session)
local ItemUse = require("src.core.game3.item_use")
local Bag = require("src.core.game3.bag")
local bike = C:id("items", "ITEM_MACH_BIKE")
Bag.add(session.bag, bike, 1)
local ok, res, a, b, c = pcall(ItemUse.useField, session, session.bag, bike)
check(ok, "bike use indoors does not error (" .. tostring(ok and "" or res) .. ")")
check(ok and res == false and type(b) == "string" and b:find("DAD") ~= nil,
"bike indoors gives DAD's advice: " .. tostring(b))
local Storage = require("src.core.game3.storage")
local list = Storage.ensure(session).items
for i = #list, 1, -1 do list[i] = nil end
list[1] = { id = C:id("items", "ITEM_POTION"), qty = 5 }
list[2] = { id = C:id("items", "ITEM_SUPER_POTION"), qty = 12 }
list[3] = { id = C:id("items", "ITEM_HM01_CUT"), qty = 1 }
list[4] = { id = C:id("items", "ITEM_ACRO_BIKE"), qty = 1 }
local ItemStorage = require("src.ui.game3.rse.item_storage")
ItemStorage.show({ session = session })
U.wait(10)
check(U.still(game, DIR .. "/01_withdraw.png"), "withdraw shot")
U.tap(game, "down")
U.wait(6)
U.tap(game, "select")
U.wait(6)
check(U.still(game, DIR .. "/02_swap.png"), "swap shot")
U.tap(game, "b")
U.wait(6)
U.tap(game, "a")
U.wait(6)
check(U.still(game, DIR .. "/03_quantity.png"), "quantity shot")
U.tap(game, "b")
U.wait(4)
ItemStorage.close()
U.wait(4)
finish()
end