mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-04 00:20:01 -04:00
Merge pull request #2622 from bryanthaboi/dev
major perf enhancement for gen3
This commit is contained in:
@@ -697,7 +697,7 @@ end
|
||||
|
||||
-- newly obtained mons carry the player's OT name/ID (status screen)
|
||||
local function stampOT(save, mon)
|
||||
save.player.id = save.player.id or math.random(0, 65535)
|
||||
save.player.id = save.player.id or require("src.core.SaveData").rollTrainerId()
|
||||
mon.ot = mon.ot or save.player.name
|
||||
-- engine/battle/experience.asm:69
|
||||
if not mon.traded then mon.otId = mon.otId or save.player.id end
|
||||
|
||||
+13
-4
@@ -2469,11 +2469,19 @@ function SaveData.runMigrations(save, modChains, activeMods)
|
||||
return save
|
||||
end
|
||||
|
||||
local function rollTrainerId()
|
||||
if love and love.math and love.math.random then
|
||||
return love.math.random(0, 65535)
|
||||
end
|
||||
return math.random(0, 65535)
|
||||
end
|
||||
SaveData.rollTrainerId = rollTrainerId
|
||||
|
||||
-- saves from before the trainer ID existed: backfill once on load
|
||||
-- (like the OT backfill for old saves)
|
||||
SaveData.addCoreMigration(1, function(save)
|
||||
if save.player and not save.player.id then
|
||||
save.player.id = math.random(0, 65535)
|
||||
save.player.id = rollTrainerId()
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -3022,9 +3030,10 @@ function SaveData.newGame(boot)
|
||||
facing = facing,
|
||||
name = boot.playerName or "RED",
|
||||
rival = boot.rivalName or "BLUE",
|
||||
-- 16-bit trainer ID rolled at new game (wPlayerID, filled from
|
||||
-- hRandomAdd in OakSpeech)
|
||||
id = math.random(0, 65535),
|
||||
-- 16-bit trainer ID rolled at new game (wPlayerID). The cart copies
|
||||
-- hRandomAdd/hRandomSub, which have been advancing since power-on;
|
||||
-- love.math is the stream that has actually been advancing here.
|
||||
id = rollTrainerId(),
|
||||
},
|
||||
flags = {},
|
||||
inventory = {},
|
||||
|
||||
@@ -21,6 +21,8 @@ end
|
||||
-- version session (save editor or game). GPU release runs before soft
|
||||
-- invalidate via installLoader(nil).
|
||||
function SessionLifecycle.endMountedSession(version)
|
||||
local stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if stream then stream.shutdown() end
|
||||
local Assets = require("src.render.Assets")
|
||||
if Assets.releaseSession then Assets.releaseSession() end
|
||||
if version then
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
-- CPU-only preparation shared by the render thread and the asset worker.
|
||||
local Pack = require("src.import.gba.native_pack")
|
||||
local Palette = require("src.core.game3.palette")
|
||||
local OwExtract = require("src.import.gba.ow_extract")
|
||||
local D = {}
|
||||
|
||||
function D.imageData(rgba, w, h)
|
||||
if not (love and love.image) then return nil, "love.image unavailable" end
|
||||
local ok, data = pcall(love.image.newImageData, w, h, "rgba8", rgba)
|
||||
if ok then return data end
|
||||
data = love.image.newImageData(w, h)
|
||||
local i = 1
|
||||
for y = 0, h - 1 do
|
||||
for x = 0, w - 1 do
|
||||
data:setPixel(x, y, (rgba:byte(i) or 0) / 255, (rgba:byte(i + 1) or 0) / 255,
|
||||
(rgba:byte(i + 2) or 0) / 255, (rgba:byte(i + 3) or 0) / 255)
|
||||
i = i + 4
|
||||
end
|
||||
end
|
||||
return data
|
||||
end
|
||||
|
||||
function D.pair(cache, root, pair)
|
||||
local path = root .. "/" .. pair
|
||||
local blob, pals = cache:read(path .. "/mids.idx"), cache:read(path .. "/palettes.bin")
|
||||
if not blob or not pals then return nil, "missing native blobs for " .. pair end
|
||||
local idx, err = Pack.decodeIdx(blob)
|
||||
if not idx then return nil, err end
|
||||
local rgb, bgr = Palette.load(pals)
|
||||
if not rgb then return nil, bgr end
|
||||
local overBlob = cache:read(path .. "/mids_over.idx")
|
||||
local out = { pair = pair, cols = idx.atlasCols, rows = idx.atlasRows, midCount = idx.midCount,
|
||||
idxBlob = blob, bgr = bgr, midToSlot = {}, layers = {} }
|
||||
for i, mid in ipairs(idx.midIds) do out.midToSlot[mid] = i - 1 end
|
||||
local function layer(key, dataKey, indexed, source, tag, transparent)
|
||||
local hash = Palette.hash(bgr, source)
|
||||
local rel = path .. "/atlas_" .. tag .. "_" .. hash .. ".rgba"
|
||||
local w, h = indexed.atlasCols * 16, indexed.atlasRows * 16
|
||||
local rgba = cache:read(rel)
|
||||
if not rgba or #rgba ~= w * h * 4 then
|
||||
rgba = Pack.bakeRgba(indexed, rgb, { transparentZero = transparent, cancelled = cache.cancelled })
|
||||
if not rgba then return nil, "asset preparation cancelled" end
|
||||
if cache.write then pcall(cache.write, cache, rel, rgba) end
|
||||
end
|
||||
local data, why = D.imageData(rgba, w, h)
|
||||
if not data then return nil, why end
|
||||
out.layers[#out.layers + 1] = { imageKey = key, dataKey = dataKey, data = data, bytes = w * h * 4 }
|
||||
return true
|
||||
end
|
||||
local ok, why = layer("image", "imageData", idx, blob, overBlob and "u" or "flat", false)
|
||||
if not ok then return nil, why end
|
||||
local over = overBlob and Pack.decodeIdx(overBlob)
|
||||
if over then
|
||||
ok, why = layer("overImage", "overImageData", over, overBlob, "o", true)
|
||||
if not ok then return nil, why end
|
||||
out.overBlob, out.layered = overBlob, true
|
||||
end
|
||||
if overBlob then
|
||||
local midBlob = cache:read(path .. "/mids_mid.idx")
|
||||
local mid = midBlob and Pack.decodeIdx(midBlob)
|
||||
if mid then
|
||||
ok, why = layer("midImage", "midImageData", mid, midBlob, "m", true)
|
||||
if not ok then return nil, why end
|
||||
end
|
||||
end
|
||||
out.animFiles = {}
|
||||
local manifestRel = path .. "/anim_manifest.lua"
|
||||
local manifestBlob = cache:read(manifestRel)
|
||||
out.animFiles[manifestRel] = manifestBlob
|
||||
local chunk = manifestBlob and load(manifestBlob, "@anim_manifest.lua", "t", {})
|
||||
local man = chunk and chunk()
|
||||
if man then
|
||||
local function read(file)
|
||||
if file then out.animFiles[path .. "/" .. file] = cache:read(path .. "/" .. file) end
|
||||
end
|
||||
out.animFiles[path .. "/palettes.bin"] = pals
|
||||
for _, row in ipairs(man.banks or {}) do read(row.file); read(row.overFile) end
|
||||
for _, kind in ipairs({ "water", "sand", "flower" }) do
|
||||
if man[kind] then read("anim_" .. kind .. ".rgba") end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function D.sprite(cache, root, gid)
|
||||
local metaBlob, rgba = cache:read(root .. "/" .. gid .. ".meta"), cache:read(root .. "/" .. gid .. ".rgba")
|
||||
local meta = metaBlob and OwExtract.decodeMeta(metaBlob)
|
||||
if not meta or not rgba then return nil, "missing OW sheet " .. tostring(gid) end
|
||||
local w, h, n = meta.width, meta.height, meta.frameCount
|
||||
if w < 1 or h < 1 or n < 1 or w > 256 or h > 256 or n > 512 then return nil, "bad OW dimensions" end
|
||||
local bytes = w * h * n * 4
|
||||
if #rgba ~= bytes then
|
||||
if #rgba < w * h * 4 then return nil, "truncated OW sheet" end
|
||||
rgba = rgba:sub(1, bytes) .. string.rep("\0", math.max(0, bytes - #rgba))
|
||||
end
|
||||
local data, err = D.imageData(rgba, w, h * n)
|
||||
if not data then return nil, err end
|
||||
meta.imageData, meta.bytes = data, bytes
|
||||
return meta
|
||||
end
|
||||
|
||||
-- Snapshots never consult a later GameVersion, cache prefix, or mod mount.
|
||||
function D.cache(spec, cancelSignal)
|
||||
local cache = { cancelled = cancelSignal and function() return cancelSignal:getCount() > 0 end or nil }
|
||||
local function checkCancel()
|
||||
if cache.cancelled and cache.cancelled() then error("asset preparation cancelled") end
|
||||
end
|
||||
local function path(rel) return (spec.prefix or "") .. rel end
|
||||
function cache:read(rel)
|
||||
checkCancel()
|
||||
if spec.directory then
|
||||
local f = io.open(spec.directory .. "/" .. path(rel), "rb")
|
||||
if not f then return nil end
|
||||
local bytes = f:read("*a"); f:close(); return bytes
|
||||
end
|
||||
return love.filesystem.read(path(rel))
|
||||
end
|
||||
function cache:write(rel, bytes)
|
||||
checkCancel()
|
||||
-- No directory creation or mounting in a worker; derived caches are optional.
|
||||
if spec.directory then
|
||||
local f = io.open(spec.directory .. "/" .. path(rel), "wb")
|
||||
if not f then return false end
|
||||
f:write(bytes); f:close(); return true
|
||||
end
|
||||
return love.filesystem.write(path(rel), bytes)
|
||||
end
|
||||
return cache
|
||||
end
|
||||
|
||||
return D
|
||||
@@ -0,0 +1,211 @@
|
||||
-- One CPU worker, bounded decoded residency, and a shared upload budget.
|
||||
local Decode = require("src.core.game3.asset_decode")
|
||||
local Stream = {}
|
||||
Stream.UPLOAD_BUDGET = .002
|
||||
Stream.MAX_DECODED = 4
|
||||
Stream.MAX_PENDING = 64
|
||||
local queue, jobs, serial = {}, {}, 0
|
||||
local thread, input, output, active
|
||||
local function cancel(job)
|
||||
job.cancelled = true
|
||||
if job.cancelSignal then job.cancelSignal:push(true) end
|
||||
end
|
||||
local clock = function() return love and love.timer and love.timer.getTime() or os.clock() end
|
||||
local function record(job, phase, seconds, route)
|
||||
if Stream.onTiming then Stream.onTiming(job.kind, job.key, phase, seconds, route) end
|
||||
if os.getenv("POKEPORT_ASSET_PROFILE") == "1" then
|
||||
print(string.format("[game3/asset] %s=%s %s=%.3fms route=%s", job.kind, job.key, phase, seconds * 1000, route))
|
||||
end
|
||||
end
|
||||
local function startWorker()
|
||||
if thread then return true end
|
||||
if not (love and love.thread and love.thread.newThread and love.thread.newChannel) then return false end
|
||||
local ok, value = pcall(love.thread.newThread, "src/core/game3/asset_worker.lua")
|
||||
if not ok then return false end
|
||||
input, output = love.thread.newChannel(), love.thread.newChannel()
|
||||
local started = pcall(value.start, value, input, output)
|
||||
if not started then return false end
|
||||
thread = value
|
||||
return true
|
||||
end
|
||||
function Stream.shutdown()
|
||||
Stream.cancelPending()
|
||||
if thread then
|
||||
input:clear(); input:push({ stop = true }); thread:wait()
|
||||
end
|
||||
thread, input, output, active = nil, nil, nil, nil
|
||||
queue, jobs = {}, {}
|
||||
Stream._uploadedThisFrame = false
|
||||
Stream.workerFailed = false
|
||||
end
|
||||
function Stream.cancelPending()
|
||||
for _, job in pairs(jobs) do
|
||||
cancel(job)
|
||||
job.owner.pending[job.key] = nil
|
||||
end
|
||||
jobs = {}
|
||||
end
|
||||
function Stream.frameComplete() Stream._uploadedThisFrame = false end
|
||||
local Lifecycle = require("src.core.SessionLifecycle")
|
||||
if not Lifecycle._game3AssetShutdownRegistered then
|
||||
Lifecycle._game3AssetShutdownRegistered = true
|
||||
Lifecycle.registerProcessShutdown(function()
|
||||
local current = package.loaded["src.core.game3.asset_stream"]
|
||||
if current then current.shutdown() end
|
||||
end)
|
||||
end
|
||||
|
||||
function Stream.new(kind, cache, root, upload, publish)
|
||||
local s = { kind = kind, cache = cache, root = root, upload = upload, publish = publish, pending = {} }
|
||||
function s:cancel()
|
||||
self.cancelled = true
|
||||
for _, job in pairs(self.pending) do cancel(job); jobs[job.id] = nil end
|
||||
self.pending = {}
|
||||
end
|
||||
function s:decoded(key)
|
||||
local job = self.pending[key]
|
||||
return job and job.data ~= nil
|
||||
end
|
||||
function s:retain(wanted)
|
||||
for key, job in pairs(self.pending) do
|
||||
if not wanted[key] then
|
||||
cancel(job); jobs[job.id], self.pending[key] = nil, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
function s:prefetch(key, priority)
|
||||
if self.cancelled then return end
|
||||
if self.pending[key] then self.pending[key].priority = priority or self.pending[key].priority; return end
|
||||
local count = 0
|
||||
for _ in pairs(self.pending) do count = count + 1 end
|
||||
if count >= Stream.MAX_PENDING then return end
|
||||
serial = serial + 1
|
||||
local spec = self.cache and self.cache.assetWorkerSpec and self.cache:assetWorkerSpec(root, kind, key)
|
||||
local job = { id = serial, kind = kind, key = key, root = root, owner = self, spec = spec, priority = priority or 1 }
|
||||
self.pending[key], jobs[serial] = job, job
|
||||
queue[#queue + 1] = job
|
||||
end
|
||||
function s:get(key)
|
||||
Stream.poll()
|
||||
local job = self.pending[key]
|
||||
local data = job and job.data
|
||||
if not data then
|
||||
if job and job.cancelSignal then job.cancelSignal:push(true) end
|
||||
local start = clock()
|
||||
local err
|
||||
data, err = Decode[kind](cache, root, key)
|
||||
record({ kind = kind, key = key }, "decode", clock() - start, "sync")
|
||||
if not data then return nil, err end
|
||||
end
|
||||
local start = clock()
|
||||
local co = job and job.co or coroutine.create(function() return upload(data) end)
|
||||
local value
|
||||
repeat
|
||||
local ok, res = coroutine.resume(co)
|
||||
if not ok then
|
||||
if job then job.cancelled = true; jobs[job.id] = nil; self.pending[key] = nil end
|
||||
return nil, tostring(res)
|
||||
end
|
||||
value = res
|
||||
until coroutine.status(co) == "dead"
|
||||
if job then job.cancelled = true; jobs[job.id] = nil; self.pending[key] = nil end
|
||||
publish(key, value)
|
||||
record({ kind = kind, key = key }, "upload", clock() - start, data == (job and job.data) and "decoded" or "sync")
|
||||
return value
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
-- CPU results are published on receipt, without consuming a graphics slice.
|
||||
function Stream.newTask(kind, publish)
|
||||
local payloads = {}
|
||||
local cache = { assetWorkerSpec = function(_, _, _, key)
|
||||
return { task = true, payload = payloads[key] }
|
||||
end }
|
||||
local s = Stream.new(kind, cache, "", nil, function(key, data, err)
|
||||
payloads[key] = nil
|
||||
publish(key, data, err)
|
||||
end)
|
||||
function s:submit(key, payload, priority)
|
||||
payloads[key] = payload
|
||||
self:prefetch(key, priority)
|
||||
payloads[key] = nil
|
||||
end
|
||||
return s
|
||||
end
|
||||
|
||||
function Stream.poll()
|
||||
if output then
|
||||
local result = output:pop()
|
||||
if result then
|
||||
if result.fatal then
|
||||
if active then active.spec, active.sent = nil, false end
|
||||
Stream.workerFailed, active = true, nil
|
||||
print("[game3/asset] worker unavailable: " .. tostring(result.fatal))
|
||||
end
|
||||
local job = jobs[result.id]
|
||||
if active and active.id == result.id then active = nil end
|
||||
if job and not job.cancelled then
|
||||
job.data, job.error, job.done = result.data, result.error, true
|
||||
record(job, "decode", result.seconds, "worker")
|
||||
if job.spec.task then
|
||||
job.owner.publish(job.key, job.data, job.error)
|
||||
job.owner.pending[job.key], jobs[job.id], job.cancelled = nil, nil, true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if thread and thread:getError() then
|
||||
if active then active.spec, active.sent = nil, false end
|
||||
thread, input, output, active = nil, nil, nil, nil
|
||||
Stream.workerFailed = true
|
||||
end
|
||||
if active then return end
|
||||
local decoded = 0
|
||||
for _, job in ipairs(queue) do if not job.cancelled and job.data then decoded = decoded + 1 end end
|
||||
local nextJob
|
||||
for _, job in ipairs(queue) do
|
||||
if not job.cancelled and not job.done and job ~= active and not job.sent then
|
||||
if job.spec and (job.spec.task or decoded < Stream.MAX_DECODED)
|
||||
and (not nextJob or job.priority < nextJob.priority) then nextJob = job end
|
||||
end
|
||||
end
|
||||
if nextJob and not Stream.workerFailed and startWorker() then
|
||||
nextJob.sent, active = true, nextJob
|
||||
nextJob.cancelSignal = love.thread.newChannel()
|
||||
input:push({ id = nextJob.id, kind = nextJob.kind, key = nextJob.key, root = nextJob.root,
|
||||
spec = nextJob.spec, cancelSignal = nextJob.cancelSignal })
|
||||
end
|
||||
-- Unknown/custom caches prepare synchronously only when explicitly consumed.
|
||||
end
|
||||
|
||||
function Stream.update()
|
||||
Stream.poll()
|
||||
if Stream._uploadedThisFrame then return end
|
||||
local start = clock()
|
||||
for i = #queue, 1, -1 do if queue[i].cancelled then table.remove(queue, i) end end
|
||||
local nextJob
|
||||
for _, job in ipairs(queue) do
|
||||
if job.data and (not nextJob or job.priority < nextJob.priority) then nextJob = job end
|
||||
end
|
||||
local job = nextJob
|
||||
if job then
|
||||
Stream._uploadedThisFrame = true
|
||||
if not job.co then job.co = coroutine.create(function() return job.owner.upload(job.data) end) end
|
||||
local t = clock()
|
||||
local ok, value = coroutine.resume(job.co)
|
||||
if not ok then
|
||||
job.error, job.done, job.data = tostring(value), true, nil
|
||||
job.co = nil
|
||||
elseif coroutine.status(job.co) == "dead" then
|
||||
job.owner.publish(job.key, value)
|
||||
job.owner.pending[job.key], jobs[job.id], job.cancelled = nil, nil, true
|
||||
end
|
||||
record(job, "upload_step", clock() - t, "warm")
|
||||
-- A texture upload (including animation catch-up on publication) is
|
||||
-- atomic. One coroutine slice per render frame prevents catch-up ticks
|
||||
-- and a second ready asset from silently exceeding the upload budget.
|
||||
if clock() - start > Stream.UPLOAD_BUDGET then Stream.lastOverBudget = clock() - start end
|
||||
end
|
||||
end
|
||||
return Stream
|
||||
@@ -0,0 +1,30 @@
|
||||
local input, output = ...
|
||||
local running, fatal = pcall(function()
|
||||
require("love.filesystem")
|
||||
require("love.image")
|
||||
require("love.data")
|
||||
require("love.timer")
|
||||
table.insert(package.searchers or package.loaders, 1, function(name)
|
||||
local path = name:gsub("%.", "/") .. ".lua"
|
||||
if love.filesystem.getInfo(path) then return assert(love.filesystem.load(path)) end
|
||||
end)
|
||||
local D = require("src.core.game3.asset_decode")
|
||||
while true do
|
||||
local job = input:demand()
|
||||
if job.stop then break end
|
||||
local start = love.timer.getTime()
|
||||
local ok, data, err
|
||||
if job.spec.task then
|
||||
local prepare = require("src.core.game3." .. (job.kind == "objects" and "object_prepare" or "field_cell_prepare"))
|
||||
ok, data, err = pcall(prepare[job.kind], job.spec.payload,
|
||||
function() return job.cancelSignal:getCount() > 0 end)
|
||||
else
|
||||
ok, data, err = pcall(D[job.kind], D.cache(job.spec, job.cancelSignal), job.root, job.key)
|
||||
end
|
||||
if not ok then err, data = tostring(data), nil end
|
||||
output:push({ id = job.id, data = data, error = err,
|
||||
seconds = love.timer.getTime() - start })
|
||||
end
|
||||
|
||||
end)
|
||||
if not running then output:push({ fatal = tostring(fatal) }) end
|
||||
@@ -2611,12 +2611,18 @@ function Engine.collectResidualEvents(_st, adapter)
|
||||
return Residuals.collectEvents(adapter)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/battle_script_commands.c:3556
|
||||
-- pokefirered/src/battle_script_commands.c:3395
|
||||
local function mon_can_battle(mon)
|
||||
if not mon or (tonumber(mon.hp) or 0) <= 0 then return false end
|
||||
if require("src.core.game3.pokemon").isEgg(mon) then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
function Engine.hasLivingMons(party)
|
||||
if not party then return false end
|
||||
for _, mon in ipairs(party) do
|
||||
if mon and (tonumber(mon.hp) or 0) > 0 then
|
||||
return true
|
||||
end
|
||||
if mon_can_battle(mon) then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
@@ -2624,9 +2630,7 @@ end
|
||||
function Engine.nextLivingMonIndex(party, currentIdx)
|
||||
if not party then return nil end
|
||||
for i, mon in ipairs(party) do
|
||||
if i ~= currentIdx and mon and (tonumber(mon.hp) or 0) > 0 then
|
||||
return i
|
||||
end
|
||||
if i ~= currentIdx and mon_can_battle(mon) then return i end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -65,6 +65,16 @@ end
|
||||
|
||||
local function loveCache()
|
||||
return {
|
||||
assetWorkerSpec = function(_, root, kind, key)
|
||||
local rel = root .. "/" .. tostring(key) .. (kind == "pair" and "/mids.idx" or ".meta")
|
||||
local CacheFs = require("src.import.CacheFs")
|
||||
local prefix = require("src.core.GameVersion").cachePrefix()
|
||||
-- Only a version-qualified cache can be read independently of mounted
|
||||
-- overlays. Overrides/custom readers retain the synchronous fallback.
|
||||
if Dataset.cacheRootOverride or os.getenv("POKEPORT_GBA_CACHE") then return nil end
|
||||
if not CacheFs.existsAt(prefix .. rel) then return nil end
|
||||
return { prefix = prefix, directory = CacheFs.root() }
|
||||
end,
|
||||
read = function(_, rel)
|
||||
local ok, CacheFs = pcall(require, "src.import.CacheFs")
|
||||
if ok and CacheFs and CacheFs.readActive then
|
||||
@@ -391,7 +401,7 @@ function Dataset.attachMidLayouts(maps, cache)
|
||||
local decoded = NativePack.decodeMidLayout(blob)
|
||||
if decoded then
|
||||
local pair = (info and info.pair) or def.pair
|
||||
def.midLayout = LayoutNative.fromDecoded(decoded, mapId, 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
|
||||
|
||||
@@ -231,13 +231,16 @@ local function drawFieldPlane(game, vw, vh, Renderer)
|
||||
local transitioning = Transition and Transition.isActive and Transition.isActive()
|
||||
Oam.resetFrame()
|
||||
local prev = Oam.setLayer("world")
|
||||
local function exchange(current, replacement)
|
||||
return Renderer and Renderer:exchangeWorldCanvas(current, replacement)
|
||||
end
|
||||
if Tilt.active() and not transitioning and Renderer and Renderer.beginUprightPass then
|
||||
FieldView.draw(game, vw, vh, { skipActors = true })
|
||||
FieldView.draw(game, vw, vh, { skipActors = true, exchangeCanvas = exchange })
|
||||
Renderer:beginUprightPass()
|
||||
FieldView.draw(game, vw, vh, { actorsOnly = true, billboard = true })
|
||||
Renderer:endUprightPass()
|
||||
else
|
||||
FieldView.draw(game, vw, vh)
|
||||
FieldView.draw(game, vw, vh, { exchangeCanvas = exchange })
|
||||
end
|
||||
Oam.setLayer(prev)
|
||||
Oam.animateSprites("world")
|
||||
@@ -439,6 +442,8 @@ end
|
||||
Display.presentFlat = presentFlat
|
||||
|
||||
function Display.present(game, winW, winH)
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if Stream then Stream.frameComplete() end
|
||||
if not Display.planesBroken then
|
||||
local ok, err = pcall(presentPlanes, game)
|
||||
if ok then return true end
|
||||
|
||||
@@ -147,6 +147,8 @@ function Field.start(mod, game, session)
|
||||
end
|
||||
|
||||
function Field.stop()
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if Stream then Stream.cancelPending() end
|
||||
lazyReq("src.world.game3.Follower").reset()
|
||||
Field.running = false
|
||||
Field._session = nil
|
||||
@@ -213,9 +215,9 @@ function Field.update(_dt)
|
||||
local Ghosts = lazyReq("src.core.game3.ghosts")
|
||||
Ghosts.sync()
|
||||
Ghosts.update(game)
|
||||
-- pre-load tileset pairs queued by a seamless world refresh, one per frame
|
||||
-- Prepare nearby assets on a worker; share a main-thread texture budget.
|
||||
local MapMod = package.loaded["src.core.game3.map"]
|
||||
if MapMod and MapMod._warmQueue and MapMod.stepWarm then MapMod.stepWarm() end
|
||||
if MapMod and MapMod.stepWarm then MapMod.stepWarm(game) end
|
||||
|
||||
Field.pollMapChange(game)
|
||||
-- pokefirered/src/safari_zone.c:60 CB2_EndSafariBattle
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
-- Immutable metatile/void resolution; no graphics or mutable field state.
|
||||
local Prepare = {}
|
||||
local function sample(layout, x, y)
|
||||
local dx, dy = x - layout.x0, y - layout.y0
|
||||
if dx < 0 or dy < 0 or dx >= layout.w or dy >= layout.h then return nil end
|
||||
local p = layout.packed
|
||||
if p then
|
||||
local ov = p.overrides[y * 1024 + x]
|
||||
if ov then return ov.mid end
|
||||
if x >= 0 and y >= 0 and x < p.trueWidth and y < p.trueHeight then
|
||||
local i = p.off + (y * p.width + x) * 4
|
||||
local a, b = p.blob:byte(i, i + 1)
|
||||
return a + b * 256
|
||||
end
|
||||
return p.borderMids[(y % p.borderHeight) * p.borderWidth + x % p.borderWidth + 1] or 0
|
||||
end
|
||||
local i = (dy * layout.w + dx) * 2 + 1
|
||||
local a, b = layout.blob:byte(i, i + 1)
|
||||
return a + b * 256
|
||||
end
|
||||
local function inside(l, x, y)
|
||||
return x >= 0 and y >= 0 and x < l.width and y < l.height
|
||||
end
|
||||
local function resolve(s, x, y)
|
||||
local root = s.layouts[1]
|
||||
if inside(root, x, y) then return sample(root, x, y), root.pair, false end
|
||||
local function dir(direction)
|
||||
for i = #s.neighbors, 1, -1 do
|
||||
local n = s.neighbors[i]
|
||||
if n.dir == direction then
|
||||
local l = s.layouts[n.layout]
|
||||
local nx, ny
|
||||
if direction == "north" then nx, ny = x - n.offset, l.height + y
|
||||
elseif direction == "south" then nx, ny = x - n.offset, y - root.height
|
||||
elseif direction == "west" then nx, ny = l.width + x, y - n.offset
|
||||
else nx, ny = x - root.width, y - n.offset end
|
||||
if inside(l, nx, ny) then return sample(l, nx, ny), l.pair or root.pair end
|
||||
end
|
||||
end
|
||||
end
|
||||
local mid, pair
|
||||
if y < 0 then mid, pair = dir("north") elseif y >= root.height then mid, pair = dir("south") end
|
||||
if mid ~= nil then return mid, pair, false end
|
||||
if x < 0 then mid, pair = dir("west") elseif x >= root.width then mid, pair = dir("east") end
|
||||
if mid ~= nil then return mid, pair, false end
|
||||
for _, entry in ipairs(s.world) do
|
||||
if entry.layout ~= 1 then
|
||||
local l = s.layouts[entry.layout]
|
||||
local nx, ny = x - entry.ox, y - entry.oy
|
||||
if inside(l, nx, ny) then return sample(l, nx, ny), l.pair or root.pair, false end
|
||||
end
|
||||
end
|
||||
return sample(root, x, y), root.pair, true
|
||||
end
|
||||
function Prepare.cells(s, cancelled)
|
||||
local pairs, ids, rows = {}, {}, {}
|
||||
for y = s.y0, s.y0 + s.rows - 1 do
|
||||
if cancelled and cancelled() then error("field cell preparation cancelled") end
|
||||
local row = {}
|
||||
for x = s.x0, s.x0 + s.cols - 1 do
|
||||
local mid, pair, void = resolve(s, x, y)
|
||||
assert(mid ~= nil, "incomplete field snapshot")
|
||||
local skip = void and s.mode == "black"
|
||||
if void and s.fill then
|
||||
local f = s.fill
|
||||
mid = f.mids[(y % f.h) * f.w + x % f.w + 1]
|
||||
pair = s.layouts[1].pair
|
||||
end
|
||||
if not ids[pair] then pairs[#pairs + 1] = pair; ids[pair] = #pairs end
|
||||
local id = ids[pair]
|
||||
row[#row + 1] = string.char(mid % 256, math.floor(mid / 256), id % 256, math.floor(id / 256),
|
||||
(void and 1 or 0) + (skip and 2 or 0))
|
||||
end
|
||||
rows[#rows + 1] = table.concat(row)
|
||||
end
|
||||
return { blob = table.concat(rows), pairs = pairs, x0 = s.x0, y0 = s.y0, cols = s.cols, rows = s.rows }
|
||||
end
|
||||
function Prepare.cell(plan, x, y)
|
||||
local dx, dy = x - plan.x0, y - plan.y0
|
||||
if dx < 0 or dy < 0 or dx >= plan.cols or dy >= plan.rows then return nil end
|
||||
local i = (dy * plan.cols + dx) * 5 + 1
|
||||
local lo, hi, p0, p1, flags = plan.blob:byte(i, i + 4)
|
||||
return lo + hi * 256, plan.pairs[p0 + p1 * 256], flags % 2 == 1, flags >= 2
|
||||
end
|
||||
return Prepare
|
||||
@@ -0,0 +1,200 @@
|
||||
-- Prepare current/near seam cell windows ahead of drawing. Snapshot sampling
|
||||
-- preserves custom layouts; worker results never contain graphics objects.
|
||||
local Cells = require("src.core.game3.field_cell_prepare")
|
||||
local Connections = require("src.core.game3.connections")
|
||||
local Plan = {}
|
||||
local records, task = {}, nil
|
||||
local MARGIN = 4
|
||||
local function stamp(layout, def)
|
||||
local borders = {}
|
||||
for i, mid in pairs(layout.borderMids or {}) do borders[i] = mid end
|
||||
return { layout = layout, def = def, defPair = def.pair, revision = layout._revision or 0, cells = layout.cells, overrides = layout.overrides,
|
||||
width = layout.width, height = layout.height, trueWidth = layout.trueWidth, trueHeight = layout.trueHeight,
|
||||
borderWidth = layout.borderWidth, borderHeight = layout.borderHeight, borders = borders,
|
||||
pair = layout.pair, midAt = layout.midAt }
|
||||
end
|
||||
local function valid(record, mode)
|
||||
if record.mode ~= mode then return false end
|
||||
if record.voidRevision ~= (require("src.core.game3.void_fill")._revision or 0) then return false end
|
||||
for _, s in ipairs(record.stamps) do
|
||||
local l = s.layout
|
||||
if s.def.midLayout ~= l or s.def.pair ~= s.defPair or (l._revision or 0) ~= s.revision or l.cells ~= s.cells or l.overrides ~= s.overrides
|
||||
or l.width ~= s.width or l.height ~= s.height or l.pair ~= s.pair or l.midAt ~= s.midAt then return false end
|
||||
if l.trueWidth ~= s.trueWidth or l.trueHeight ~= s.trueHeight or l.borderWidth ~= s.borderWidth
|
||||
or l.borderHeight ~= s.borderHeight then return false end
|
||||
local current = l.borderMids or {}
|
||||
for i, mid in pairs(s.borders) do if current[i] ~= mid then return false end end
|
||||
for i, mid in pairs(current) do if s.borders[i] ~= mid then return false end end
|
||||
end
|
||||
return true
|
||||
end
|
||||
local function graphValid(record, Map)
|
||||
if #record.world ~= #(Map.world or {}) or #record.neighbors ~= #(Map.neighborList or {}) then
|
||||
Plan._graphMiss = string.format("counts:%d/%d,%d/%d", #record.world, #(Map.world or {}), #record.neighbors, #(Map.neighborList or {})); return false
|
||||
end
|
||||
for i, entry in ipairs(Map.world or {}) do
|
||||
local old = record.world[i]
|
||||
if old.id ~= entry.id or old.def ~= entry.def or old.ox ~= entry.ox or old.oy ~= entry.oy then
|
||||
Plan._graphMiss = string.format("world:%s/%s,%s/%s,%s/%s", old.id, entry.id, old.ox, entry.ox, old.oy, entry.oy); return false
|
||||
end
|
||||
end
|
||||
for i, n in ipairs(Map.neighborList or {}) do
|
||||
local old = record.neighbors[i]
|
||||
if old.dir ~= n.dir or old.offset ~= n.offset or old.def ~= n.def then
|
||||
Plan._graphMiss = string.format("neighbor:%s/%s,%s/%s", old.dir, n.dir, old.offset, n.offset); return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
local function contains(s, x, y, w, h)
|
||||
return x >= s.x0 and y >= s.y0 and x + w <= s.x0 + s.cols and y + h <= s.y0 + s.rows
|
||||
end
|
||||
function Plan.invalidate()
|
||||
if task then task:cancel() end
|
||||
task, records = nil, {}
|
||||
end
|
||||
local function snapshot(game, id, x, y, cols, rows, mode, Native, reachW, reachH)
|
||||
local Map = require("src.core.game3.map")
|
||||
local maps = game.data.maps
|
||||
local def = maps[id]
|
||||
local root = def and def.midLayout
|
||||
if not root then return nil end
|
||||
-- Map.load changes current before the next draw refreshes Map.world. Do not
|
||||
-- replace a forecast with the previous root's graph during that interval.
|
||||
local world = id == Map._worldRoot and Map.world or Map.computeWorld(maps, id, Map.WORLD_HOPS, reachW, reachH)
|
||||
local s = { x0 = x - MARGIN, y0 = y - MARGIN, cols = cols + MARGIN * 2, rows = rows + MARGIN * 2,
|
||||
mode = mode, layouts = {}, neighbors = {}, world = {} }
|
||||
local stamps, added, neighborDefs = {}, {}, {}
|
||||
local function add(d, ox, oy, primary)
|
||||
local l = d and d.midLayout
|
||||
if not l then return nil end
|
||||
local positions = added[l] or {}; added[l] = positions
|
||||
local pair = l.pair or d.pair or root.pair or def.pair
|
||||
local key = tostring(ox) .. ":" .. tostring(oy) .. ":" .. tostring(pair)
|
||||
if positions[key] then return positions[key] end
|
||||
local x0, y0 = s.x0 - ox, s.y0 - oy
|
||||
local x1, y1 = x0 + s.cols - 1, y0 + s.rows - 1
|
||||
if not primary then
|
||||
x0, y0 = math.max(0, x0), math.max(0, y0)
|
||||
x1, y1 = math.min(l.width - 1, x1), math.min(l.height - 1, y1)
|
||||
end
|
||||
local w, h = math.max(0, x1 - x0 + 1), math.max(0, y1 - y0 + 1)
|
||||
local packed = l.workerPacked and l:workerPacked()
|
||||
local lines = {}
|
||||
for cy = y0, (packed and y0 - 1 or y0 + h - 1) do
|
||||
local bytes = {}
|
||||
for cx = x0, x0 + w - 1 do
|
||||
local mid = l:midAt(cx, cy)
|
||||
if type(mid) ~= "number" or mid < 0 or mid > 65535 then return nil end
|
||||
bytes[#bytes + 1] = string.char(mid % 256, math.floor(mid / 256))
|
||||
end
|
||||
lines[#lines + 1] = table.concat(bytes)
|
||||
end
|
||||
local n = #s.layouts + 1
|
||||
s.layouts[n] = { x0 = x0, y0 = y0, w = w, h = h, width = l.width, height = l.height,
|
||||
pair = pair, blob = table.concat(lines), packed = packed }
|
||||
stamps[n] = stamp(l, d); positions[key] = n
|
||||
return n
|
||||
end
|
||||
if not add(def, 0, 0, true) then return nil end
|
||||
local neighbors = id == Map.current and Map.neighborList or Connections.each(def)
|
||||
for _, n in ipairs(neighbors or {}) do
|
||||
local d = n.def or maps[n.map]
|
||||
local l = d and d.midLayout
|
||||
if d then neighborDefs[#neighborDefs + 1] = { dir = n.dir, offset = n.offset, def = d } end
|
||||
if l then
|
||||
local ox, oy = 0, 0
|
||||
if n.dir == "north" then ox, oy = n.offset, -l.height
|
||||
elseif n.dir == "south" then ox, oy = n.offset, root.height
|
||||
elseif n.dir == "west" then ox, oy = -l.width, n.offset
|
||||
else ox, oy = root.width, n.offset end
|
||||
local index = add(d, ox, oy)
|
||||
if index then s.neighbors[#s.neighbors + 1] = { dir = n.dir, offset = n.offset, layout = index } end
|
||||
end
|
||||
end
|
||||
for _, entry in ipairs(world or {}) do
|
||||
local index = add(entry.def, entry.ox, entry.oy, entry.id == id)
|
||||
if index then s.world[#s.world + 1] = { layout = index, ox = entry.ox, oy = entry.oy } end
|
||||
end
|
||||
local Void = require("src.core.game3.void_fill")
|
||||
if mode ~= "map" and mode ~= "black" and Void.primaryFor(s.layouts[1].pair) == Void.PRIMARY then
|
||||
-- hasMid(pair) demand-loads the atlas. Forecasting must not bypass the
|
||||
-- shared main-thread upload budget while the pair is still warming.
|
||||
local atlas = Native._pairs and Native._pairs[s.layouts[1].pair]
|
||||
if not atlas then return nil end
|
||||
local b = Void.borderFor(mode)
|
||||
local available = b ~= nil
|
||||
for _, mid in ipairs(b and b.mids or {}) do if not Native.hasMid(atlas, mid) then available = false end end
|
||||
if available then s.fill = { w = b.w, h = b.h, mids = {} }; for i, mid in ipairs(b.mids) do s.fill.mids[i] = mid end end
|
||||
end
|
||||
local worldDefs = {}
|
||||
for i, e in ipairs(world or {}) do worldDefs[i] = { id = e.id, def = e.def, ox = e.ox, oy = e.oy } end
|
||||
return s, stamps, worldDefs, neighborDefs
|
||||
end
|
||||
function Plan.prefetch(game)
|
||||
if not (love and love.thread and love.thread.newThread and game and game.data and game.data.maps) then return end
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
if Stream.workerFailed then return end
|
||||
if not task then task = Stream.newTask("cells", function(id, data, err)
|
||||
local r = records[id]; if r then r.data, r.error = data, err end
|
||||
if err then print("[game3/field-plan] " .. tostring(id) .. ": " .. tostring(err)) end
|
||||
end) end
|
||||
local Map = require("src.core.game3.map")
|
||||
local View = package.loaded["src.core.game3.field_view"]
|
||||
local Player = package.loaded["src.core.game3.player"]
|
||||
local Native = package.loaded["src.core.game3.tileset_native"]
|
||||
if not (View and Player and Native and Map.current) then return end
|
||||
local vw, vh = View._viewW or 240, View._viewH or 160
|
||||
local cols, rows = math.ceil(vw / 16) + 3, math.ceil(vh / 16) + 3
|
||||
local cx = math.floor(((Player.px or 0) + 8 - vw / 2 + (View.cameraPanX or 0)) / 16) - 1
|
||||
local cy = math.floor(((Player.py or 0) + 8 - vh / 2 + (View.cameraPanY or 0)) / 16) - 1
|
||||
local mode = require("src.core.game3.void_fill").normalize(require("src.core.game3.void_fill").mode)
|
||||
local candidates = { { id = Map.current, ox = 0, oy = 0, distance = -1 } }
|
||||
local x0, y0, x1, y1 = Map.warmRect()
|
||||
for _, entry in ipairs(Map.world or {}) do
|
||||
if entry.id ~= Map.current and Map.warmNear(entry, x0, y0, x1, y1) then
|
||||
local l = entry.def and entry.def.midLayout
|
||||
if l then
|
||||
local dx = math.max(entry.ox - Player.cellX, Player.cellX - (entry.ox + l.width), 0)
|
||||
local dy = math.max(entry.oy - Player.cellY, Player.cellY - (entry.oy + l.height), 0)
|
||||
candidates[#candidates + 1] = { id = entry.id, ox = entry.ox, oy = entry.oy, distance = dx + dy }
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(candidates, function(a, b) return a.distance < b.distance end)
|
||||
local wanted = {}
|
||||
for i = 1, math.min(3, #candidates) do
|
||||
local e = candidates[i]
|
||||
wanted[e.id] = true
|
||||
local x, y = cx - e.ox, cy - e.oy
|
||||
local record = records[e.id]
|
||||
if not (record and valid(record, mode) and (e.id ~= Map._worldRoot or graphValid(record, Map))
|
||||
and contains(record.snapshot, x - 1, y - 1, cols + 2, rows + 2)
|
||||
and (record.data or record.error or task.pending[e.id])) then
|
||||
local keep = {}; for id in pairs(records) do if id ~= e.id then keep[id] = true end end
|
||||
task:retain(keep)
|
||||
local s, stamps, worldDefs, neighborDefs = snapshot(game, e.id, x, y, cols, rows, mode, Native, math.ceil(vw / 16), math.ceil(vh / 16))
|
||||
if s then
|
||||
records[e.id] = { def = game.data.maps[e.id], mode = mode, snapshot = s, stamps = stamps,
|
||||
world = worldDefs, neighbors = neighborDefs, voidRevision = require("src.core.game3.void_fill")._revision or 0 }
|
||||
task:submit(e.id, s, i == 1 and 0 or 1)
|
||||
else records[e.id] = nil end
|
||||
end
|
||||
end
|
||||
task:retain(wanted)
|
||||
for id in pairs(records) do if not wanted[id] then records[id] = nil end end
|
||||
Stream.poll()
|
||||
end
|
||||
function Plan.get(def, x, y, cols, rows, mode)
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if Stream then Stream.poll() end
|
||||
local Map = require("src.core.game3.map")
|
||||
local id = Map.current
|
||||
local r = records[id]
|
||||
Plan._lastMiss = not r and "not_queued" or r.def ~= def and "map_identity"
|
||||
or r.error and "worker_error" or not r.data and "pending" or not valid(r, mode) and "layout_revision"
|
||||
or not graphValid(r, Map) and "connections" or not contains(r.data, x, y, cols, rows) and "window" or nil
|
||||
if not Plan._lastMiss then return r.data end
|
||||
end
|
||||
Plan.cell = Cells.cell
|
||||
return Plan
|
||||
@@ -1127,16 +1127,24 @@ local function drawNativeTiles(mapDef, camX, camY, canvasW, canvasH)
|
||||
local visibleCells = FieldView._nativeVisibleCells
|
||||
local visiblePairs = FieldView._nativeVisiblePairs
|
||||
local baseBx, baseBy = FieldView._nativeBaseBx, FieldView._nativeBaseBy
|
||||
local CellPlan = require("src.core.game3.field_plan")
|
||||
-- A same-layout bulk invalidation can come from a mod editing its cells
|
||||
-- directly. Resnapshot it rather than reuse an older planned window.
|
||||
if FieldView._nativeDirty and FieldView._nativeLayout == layout then CellPlan.invalidate() end
|
||||
local preparedCells = CellPlan.get(mapDef, cx0, cy0, cols, rows, voidMode)
|
||||
FieldView._cellPreparationRoute = preparedCells and "worker" or "sync"
|
||||
local function addCell(wx, wy)
|
||||
local mid, srcPair, isVoid
|
||||
if Map.worldMidAt then
|
||||
local skip = false
|
||||
if preparedCells then
|
||||
mid, srcPair, isVoid, skip = CellPlan.cell(preparedCells, wx, wy)
|
||||
elseif Map.worldMidAt then
|
||||
mid, srcPair, isVoid = Map.worldMidAt(wx, wy, mapDef)
|
||||
srcPair = srcPair or pair
|
||||
else
|
||||
mid, srcPair, isVoid = layout:midAt(wx, wy), pair, false
|
||||
end
|
||||
local skip = false
|
||||
if isVoid and voidMode ~= "map" then
|
||||
if not preparedCells and isVoid and voidMode ~= "map" then
|
||||
local fill = VoidFill.fillAt(voidMode, wx, wy,
|
||||
function(m) return NativeTileset.hasMid(pair, m) end, VoidFill.primaryFor(pair))
|
||||
if fill == false then
|
||||
@@ -1780,7 +1788,7 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
end
|
||||
local FieldWeather = modFieldWeather()
|
||||
if FieldWeather and FieldWeather.draw then
|
||||
FieldWeather.draw(camX, camY, canvasW, canvasH)
|
||||
FieldWeather.draw(camX, camY, canvasW, canvasH, opts.exchangeCanvas)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1791,6 +1799,8 @@ end
|
||||
|
||||
--- Drop cached atlases/sprites (tileset hot-reload).
|
||||
function FieldView.invalidate()
|
||||
local Plan = package.loaded["src.core.game3.field_plan"]
|
||||
if Plan then Plan.invalidate() end
|
||||
FieldView._atlas = nil
|
||||
FieldView._atlasPath = nil
|
||||
FieldView._quads = {}
|
||||
@@ -1833,10 +1843,21 @@ function FieldView.invalidate()
|
||||
if OwSprites and OwSprites.invalidate then
|
||||
OwSprites.invalidate()
|
||||
end
|
||||
local WeatherRse = package.loaded["src.core.game3.field_weather_rse"]
|
||||
if WeatherRse and WeatherRse.invalidate then WeatherRse.invalidate() end
|
||||
local FieldEffects = modFieldEffects()
|
||||
if FieldEffects and FieldEffects.invalidate then
|
||||
FieldEffects.invalidate()
|
||||
end
|
||||
end
|
||||
|
||||
local Assets = require("src.render.Assets")
|
||||
if Assets.register and not Assets._game3FieldInvalidatorRegistered then
|
||||
Assets._game3FieldInvalidatorRegistered = true
|
||||
Assets.register(function()
|
||||
local current = package.loaded["src.core.game3.field_view"]
|
||||
if current then current.invalidate() end
|
||||
end)
|
||||
end
|
||||
|
||||
return FieldView
|
||||
|
||||
@@ -130,11 +130,11 @@ function FieldWeather.drawBelow(camX, camY, canvasW, canvasH)
|
||||
if E then E.drawBelow(camX or 0, camY or 0, canvasW or W, canvasH or H) end
|
||||
end
|
||||
|
||||
function FieldWeather.draw(camX, camY, canvasW, canvasH)
|
||||
function FieldWeather.draw(camX, camY, canvasW, canvasH, exchangeCanvas)
|
||||
if Weather.isSuspended() then return end
|
||||
local E = Weather.rseEngine()
|
||||
if E then
|
||||
E.draw(camX or 0, camY or 0, canvasW or W, canvasH or H)
|
||||
E.draw(camX or 0, camY or 0, canvasW or W, canvasH or H, exchangeCanvas)
|
||||
return
|
||||
end
|
||||
local w = FieldWeather._current
|
||||
|
||||
@@ -1931,6 +1931,39 @@ local function drawTiled(sheet, frame, baseX, baseY, ox, oy, cw, ch)
|
||||
local sy = oy + (baseY % 64) - 64
|
||||
while sx > -64 do sx = sx - 64 end
|
||||
while sy > -64 do sy = sy - 64 end
|
||||
if love.graphics.newSpriteBatch then
|
||||
local r, g, b, a = love.graphics.getColor()
|
||||
local bucket = r == 1 and g == 1 and b == 1 and a == 1 and "tiledBase" or "tiledTint"
|
||||
local store = sheet[bucket] or {}; sheet[bucket] = store
|
||||
local key = table.concat({ frame, sx, sy, cw, ch, r, g, b, a }, ":")
|
||||
if store.key ~= key then
|
||||
local q = sheetQuad(sheet, frame, 8, 8)
|
||||
local count = (math.floor((cw - sx) / 64) + 1) * (math.floor((ch - sy) / 64) + 1)
|
||||
count = count * (type(q) == "table" and #q or 1)
|
||||
if not store.batch or store.capacity < count then
|
||||
store.batch = love.graphics.newSpriteBatch(sheet.image, count, "dynamic")
|
||||
store.capacity = count
|
||||
end
|
||||
local batch = store.batch
|
||||
batch:clear()
|
||||
-- Image draws store their colour as bytes in the vertex buffer. Use
|
||||
-- the same byte conversion here, including fractional EVA/16, instead
|
||||
-- of applying a float constant colour to the finished SpriteBatch.
|
||||
batch:setColor(r, g, b, a)
|
||||
for y = sy, ch, 64 do
|
||||
for x = sx, cw, 64 do
|
||||
if type(q) == "table" then
|
||||
for _, part in ipairs(q) do batch:add(part[1], x + part[2], y + part[3]) end
|
||||
else batch:add(q, x, y) end
|
||||
end
|
||||
end
|
||||
store.key = key
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(store.batch)
|
||||
love.graphics.setColor(r, g, b, a)
|
||||
return
|
||||
end
|
||||
for y = sy, ch, 64 do
|
||||
for x = sx, cw, 64 do
|
||||
drawSheetFrame(sheet, frame, 8, 8, x, y)
|
||||
@@ -1988,17 +2021,40 @@ local function drawPriority1(A, ox, oy, cw, ch)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/field_weather.c:459 ApplyColorMap
|
||||
local function colorMapPass(A, cw, ch)
|
||||
local function colorMapPass(A, cw, ch, exchangeCanvas)
|
||||
if S.colorMapIndex == 0 or not A.shader then return end
|
||||
local cur = love.graphics.getCanvas()
|
||||
if not cur then return end
|
||||
cw, ch = cur:getWidth(), cur:getHeight()
|
||||
local tmp = R._tmpCanvas
|
||||
if not tmp or tmp:getWidth() ~= cw or tmp:getHeight() ~= ch then
|
||||
tmp = love.graphics.newCanvas(cw, ch)
|
||||
if tmp and tmp.release then tmp:release() end
|
||||
tmp = love.graphics.newCanvas(cw, ch, { dpiscale = cur.getDPIScale and cur:getDPIScale() or 1,
|
||||
format = cur.getFormat and cur:getFormat() or "normal" })
|
||||
tmp:setFilter("nearest", "nearest")
|
||||
R._tmpCanvas = tmp
|
||||
end
|
||||
if exchangeCanvas and not love.graphics.getScissor() and exchangeCanvas(cur, tmp) then
|
||||
local shader = love.graphics.getShader()
|
||||
local blend, alpha = love.graphics.getBlendMode()
|
||||
local r, g, b, a = love.graphics.getColor()
|
||||
-- A transform-only push leaves ownership of the new target with the
|
||||
-- caller. push("all") would restore the old canvas and switch twice more.
|
||||
love.graphics.push()
|
||||
love.graphics.origin()
|
||||
love.graphics.setCanvas(tmp)
|
||||
love.graphics.setBlendMode("replace", "premultiplied")
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
colorMapUniforms(A.shader, COLOR_MAP_DARK_CONTRAST)
|
||||
love.graphics.setShader(A.shader)
|
||||
love.graphics.draw(cur, 0, 0)
|
||||
love.graphics.pop()
|
||||
love.graphics.setShader(shader)
|
||||
love.graphics.setBlendMode(blend, alpha)
|
||||
love.graphics.setColor(r, g, b, a)
|
||||
R._tmpCanvas = cur
|
||||
return tmp
|
||||
end
|
||||
love.graphics.push("all")
|
||||
love.graphics.origin()
|
||||
love.graphics.setShader()
|
||||
@@ -2028,7 +2084,7 @@ function R.drawBelow(camX, camY, canvasW, canvasH)
|
||||
love.graphics.pop()
|
||||
end
|
||||
|
||||
function R.draw(camX, camY, canvasW, canvasH)
|
||||
function R.draw(camX, camY, canvasW, canvasH, exchangeCanvas)
|
||||
R.setCamera(camX, camY)
|
||||
if not S.started or not S.initialized then return end
|
||||
local A = loadAssets()
|
||||
@@ -2041,7 +2097,9 @@ function R.draw(camX, camY, canvasW, canvasH)
|
||||
if R._belowFrame ~= S.frames then
|
||||
drawPriority2(A, camX, camY, ox, oy, canvasW, canvasH)
|
||||
end
|
||||
colorMapPass(A, canvasW, canvasH)
|
||||
love.graphics.pop()
|
||||
colorMapPass(A, canvasW, canvasH, exchangeCanvas)
|
||||
love.graphics.push("all")
|
||||
drawPriority1(A, ox, oy, canvasW, canvasH)
|
||||
love.graphics.pop()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
|
||||
@@ -121,6 +121,21 @@ local function markFade(pool, entry)
|
||||
end
|
||||
end
|
||||
|
||||
local function prepareOffscreen(M, entry)
|
||||
local Obj = Objects()
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if not (love and love.thread and love.thread.newThread) or (Stream and Stream.workerFailed) then return false end
|
||||
if not (M.warmRect and M.warmNear and Obj.prefetchMap and Obj.preparationReady) then return false end
|
||||
local x0, y0, x1, y1 = M.warmRect(0)
|
||||
-- Visible actors and collision queries retain immediate authoritative
|
||||
-- adoption. Offscreen pools can wait for immutable worker preparation.
|
||||
if not x0 or M.warmNear(entry, x0, y0, x1, y1) then return false end
|
||||
x0, y0, x1, y1 = M.warmRect()
|
||||
if not M.warmNear(entry, x0, y0, x1, y1) then return true end
|
||||
if not Obj.prefetchMap(entry.id, entry.def, 1) then return false end
|
||||
return not Obj.preparationReady(entry.id, entry.def)
|
||||
end
|
||||
|
||||
function Ghosts.sync()
|
||||
local M = Map()
|
||||
local world = M.world or EMPTY
|
||||
@@ -129,7 +144,7 @@ function Ghosts.sync()
|
||||
for id in pairs(placed) do placed[id] = nil end
|
||||
for _, entry in ipairs(world) do
|
||||
placed[entry.id] = true
|
||||
if not Ghosts._pools[entry.id] then
|
||||
if not Ghosts._pools[entry.id] and not prepareOffscreen(M, entry) then
|
||||
local defs = defsFor(entry.id, entry.def)
|
||||
if defs then
|
||||
local pool = Objects().spawnFromDefs(defs, entry.def, entry.id)
|
||||
|
||||
@@ -12,7 +12,7 @@ local function wrap_border(cx, cy, w, h, bw, bh)
|
||||
return bx, by
|
||||
end
|
||||
|
||||
function LayoutNative.fromDecoded(decoded, mapId, pair)
|
||||
function LayoutNative.fromDecoded(decoded, mapId, pair, sourceBlob)
|
||||
local self = setmetatable({
|
||||
mapId = mapId,
|
||||
pair = pair or "sevii_outdoor",
|
||||
@@ -25,10 +25,31 @@ function LayoutNative.fromDecoded(decoded, mapId, pair)
|
||||
borderMids = decoded.borderMids or { 0 },
|
||||
cells = decoded.cells or {},
|
||||
overrides = {}, -- [cy*1024+cx] = { mid, coll, elev }
|
||||
_revision = 0,
|
||||
_workerSource = sourceBlob and { blob = sourceBlob, cells = decoded.cells,
|
||||
width = decoded.width, height = decoded.height, trueWidth = decoded.trueWidth or decoded.width,
|
||||
trueHeight = decoded.trueHeight or decoded.height },
|
||||
}, LayoutNative)
|
||||
return self
|
||||
end
|
||||
|
||||
-- Imported base grids are immutable in the built-in runtime; edits live in
|
||||
-- overrides. Custom/mod layouts use sampled snapshots instead of this path.
|
||||
function LayoutNative:workerPacked()
|
||||
local s = self._workerSource
|
||||
if not s or self.cells ~= s.cells or self.width ~= s.width or self.height ~= s.height
|
||||
or self.trueWidth ~= s.trueWidth or self.trueHeight ~= s.trueHeight
|
||||
or self.cellAt ~= LayoutNative.cellAt or self.midAt ~= LayoutNative.midAt then return nil end
|
||||
local Assets = package.loaded["src.render.Assets"]
|
||||
if Assets and Assets.loader and #Assets.loader:overrideOrder() > 0 then return nil end
|
||||
local overrides, borders = {}, {}
|
||||
for k, row in pairs(self.overrides) do overrides[k] = { mid = row.mid } end
|
||||
for i = 1, self.borderWidth * self.borderHeight do borders[i] = self.borderMids[i] end
|
||||
return { blob = s.blob, off = 17 + (s.blob:byte(15) * s.blob:byte(16)) * 2,
|
||||
width = self.width, trueWidth = self.trueWidth, trueHeight = self.trueHeight,
|
||||
borderWidth = self.borderWidth, borderHeight = self.borderHeight, borderMids = borders, overrides = overrides }
|
||||
end
|
||||
|
||||
function LayoutNative:cellAt(cx, cy)
|
||||
local key = cy * 1024 + cx
|
||||
local ov = self.overrides[key]
|
||||
@@ -101,6 +122,7 @@ function LayoutNative:collArray()
|
||||
end
|
||||
|
||||
function LayoutNative:applyOverride(x, y, mid, coll, elev)
|
||||
self._revision = (self._revision or 0) + 1
|
||||
x, y = tonumber(x) or 0, tonumber(y) or 0
|
||||
self.overrides[y * 1024 + x] = {
|
||||
mid = tonumber(mid) or 0,
|
||||
@@ -159,6 +181,7 @@ function LayoutNative:stamp(src, ox, oy)
|
||||
end
|
||||
end
|
||||
markDirty()
|
||||
if n > 0 then self._revision = (self._revision or 0) + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
@@ -177,12 +200,15 @@ function LayoutNative:setMetatiles(rows)
|
||||
n = n + 1
|
||||
cells[n * 2 - 1], cells[n * 2] = x, y
|
||||
end
|
||||
if n > 0 then markCellsDirty(self, cells, n) end
|
||||
if n > 0 then self._revision = (self._revision or 0) + 1; markCellsDirty(self, cells, n) end
|
||||
return n
|
||||
end
|
||||
|
||||
function LayoutNative:clearOverrides()
|
||||
self.overrides = {}
|
||||
if next(self.overrides) then
|
||||
self._revision = (self._revision or 0) + 1
|
||||
self.overrides = {}
|
||||
end
|
||||
local FieldView = package.loaded["src.core.game3.field_view"]
|
||||
if FieldView then
|
||||
FieldView._nativeDirty = true
|
||||
|
||||
+105
-6
@@ -156,7 +156,7 @@ function Map.refreshWorld(game, reachW, reachH, rootId)
|
||||
local queue, seen, entries = {}, {}, {}
|
||||
for _, entry in ipairs(Map.world) do
|
||||
local pair = entry.def and (entry.def.pair or (entry.def.midLayout and entry.def.midLayout.pair))
|
||||
if sync and pair and Map.warmNear(entry, x0, y0, x1, y1) then
|
||||
if sync and pair and not NativeTileset.prefetch and Map.warmNear(entry, x0, y0, x1, y1) then
|
||||
if NativeTileset.ready(pair) then pcall(NativeTileset.get, pair) end
|
||||
elseif type(pair) == "string" and not (NativeTileset._pairs and NativeTileset._pairs[pair]) then
|
||||
if not seen[pair] then
|
||||
@@ -170,6 +170,12 @@ function Map.refreshWorld(game, reachW, reachH, rootId)
|
||||
end
|
||||
Map._warmQueue = queue[1] and queue or nil
|
||||
Map._warmEntries = queue[1] and entries or nil
|
||||
if NativeTileset._stream then
|
||||
local root = maps[rootId]
|
||||
local pair = root and (root.pair or (root.midLayout and root.midLayout.pair))
|
||||
if pair then seen[pair] = true end
|
||||
NativeTileset._stream:retain(seen)
|
||||
end
|
||||
end
|
||||
Map._worldRoot = rootId
|
||||
Map._worldReachW = reachW
|
||||
@@ -190,10 +196,27 @@ function Map.warmNow(game, rootId)
|
||||
local vw = (FieldView and FieldView._viewW) or (Display and Display.W) or 240
|
||||
local vh = (FieldView and FieldView._viewH) or (Display and Display.H) or 160
|
||||
Map._warmPairs = nil
|
||||
return Map.refreshWorld(game, math.ceil(vw / 16), math.ceil(vh / 16), rootId)
|
||||
local world = Map.refreshWorld(game, math.ceil(vw / 16), math.ceil(vh / 16), rootId)
|
||||
local Native = package.loaded["src.core.game3.tileset_native"]
|
||||
local def = host_map_def(game, rootId)
|
||||
local pair = def and (def.pair or (def.midLayout and def.midLayout.pair))
|
||||
if Native and pair and Native.ready(pair) then pcall(Native.get, pair) end
|
||||
local Ow = package.loaded["src.core.game3.ow_sprites"]
|
||||
if Ow and Ow.get and Ow.playerGraphicsId then
|
||||
local gid = Ow.playerGraphicsId(game)
|
||||
if gid then pcall(Ow.get, gid) end
|
||||
local Objects = package.loaded["src.core.game3.objects"]
|
||||
local x0, y0, x1, y1 = Map.warmRect()
|
||||
for _, eo in ipairs(Objects and Objects.forDraw and Objects.forDraw() or {}) do
|
||||
local x, y = eo.cellX or 0, eo.cellY or 0
|
||||
if eo.graphicsId and (not x0 or (x >= x0 and x <= x1 and y >= y0 and y <= y1)) then pcall(Ow.get, eo.graphicsId) end
|
||||
end
|
||||
end
|
||||
Map.stepWarm(game)
|
||||
return world
|
||||
end
|
||||
|
||||
function Map.warmRect()
|
||||
function Map.warmRect(margin)
|
||||
local P = package.loaded["src.core.game3.player"]
|
||||
local px, py = P and tonumber(P.cellX), P and tonumber(P.cellY)
|
||||
if not (px and py) then return nil end
|
||||
@@ -202,8 +225,9 @@ function Map.warmRect()
|
||||
local vw = (FieldView and FieldView._viewW) or (Display and Display.W) or 240
|
||||
local vh = (FieldView and FieldView._viewH) or (Display and Display.H) or 160
|
||||
local cols, rows = math.ceil(vw / 16), math.ceil(vh / 16)
|
||||
local mx = math.ceil(cols / 2) + 1 + cols * Map.WARM_MARGIN_SCREENS
|
||||
local my = math.ceil(rows / 2) + 1 + rows * Map.WARM_MARGIN_SCREENS
|
||||
margin = margin or Map.WARM_MARGIN_SCREENS
|
||||
local mx = math.ceil(cols / 2) + 1 + cols * margin
|
||||
local my = math.ceil(rows / 2) + 1 + rows * margin
|
||||
return px - mx, py - my, px + mx, py + my
|
||||
end
|
||||
|
||||
@@ -216,7 +240,82 @@ function Map.warmNear(entry, x0, y0, x1, y1)
|
||||
return ox + w > x0 and ox <= x1 and oy + h > y0 and oy <= y1
|
||||
end
|
||||
|
||||
function Map.stepWarm()
|
||||
function Map.stepWarm(game)
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
local Native = package.loaded["src.core.game3.tileset_native"]
|
||||
if Native and Native.prefetch then
|
||||
local x0, y0, x1, y1 = Map.warmRect()
|
||||
Map._warmScanTick = ((Map._warmScanTick or 0) + 1) % 4
|
||||
if Map._warmScanTick ~= 0 and Map._warmScanWorld == Map.world
|
||||
and Map._warmScanX == x0 and Map._warmScanY == y0
|
||||
and Map._warmScanX1 == x1 and Map._warmScanY1 == y1 then
|
||||
if Stream then Stream.update() end
|
||||
return true
|
||||
end
|
||||
Map._warmScanWorld = Map.world
|
||||
Map._warmScanX, Map._warmScanY, Map._warmScanX1, Map._warmScanY1 = x0, y0, x1, y1
|
||||
local vx0, vy0, vx1, vy1 = Map.warmRect(0)
|
||||
local wantedPairs = {}
|
||||
local priorities = {}
|
||||
local def = Map.currentDef()
|
||||
local currentPair = def and (def.pair or (def.midLayout and def.midLayout.pair))
|
||||
if currentPair then wantedPairs[currentPair], priorities[currentPair] = true, 0 end
|
||||
local queue = Map._warmQueue or {}
|
||||
for i = #queue, 1, -1 do
|
||||
local pair = queue[i]
|
||||
local near = not (Map._warmEntries and Map._warmEntries[pair])
|
||||
for _, entry in ipairs(Map._warmEntries and Map._warmEntries[pair] or {}) do
|
||||
if Map.warmNear(entry, x0, y0, x1, y1) then near = true end
|
||||
if Map.warmNear(entry, vx0, vy0, vx1, vy1) then priorities[pair] = 0 end
|
||||
end
|
||||
if Native._pairs[pair] then table.remove(queue, i)
|
||||
elseif near then wantedPairs[pair] = true end
|
||||
end
|
||||
if not queue[1] then Map._warmQueue = nil end
|
||||
if Native._stream then Native._stream:retain(wantedPairs) end
|
||||
for pair in pairs(wantedPairs) do Native.prefetch(pair, priorities[pair] or 1) end
|
||||
local Ow = package.loaded["src.core.game3.ow_sprites"]
|
||||
local Obj = package.loaded["src.core.game3.objects"]
|
||||
if Obj and Obj.prefetchMap then
|
||||
local wanted = {}
|
||||
for _, entry in ipairs(Map.world or {}) do
|
||||
if Map.warmNear(entry, x0, y0, x1, y1) then
|
||||
wanted[entry.id] = true
|
||||
Obj.prefetchMap(entry.id, entry.def, entry.id == Map.current and 0 or 1)
|
||||
end
|
||||
end
|
||||
Obj.retainPrepared(wanted)
|
||||
end
|
||||
if game and Ow and Ow.prefetch then
|
||||
local wanted = {}
|
||||
local function actor(eo, ox, oy)
|
||||
if not (eo.visible and not eo.hidden and not eo.invisible) then return end
|
||||
local x, y = (eo.cellX or 0) + (ox or 0), (eo.cellY or 0) + (oy or 0)
|
||||
if not x0 or (x >= x0 and x <= x1 and y >= y0 and y <= y1) then
|
||||
local gid = tonumber(eo.graphicsId)
|
||||
if gid then
|
||||
local visible = not vx0 or (x >= vx0 and x <= vx1 and y >= vy0 and y <= vy1)
|
||||
wanted[gid] = math.min(wanted[gid] or 1, visible and 0 or 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
local Obj = package.loaded["src.core.game3.objects"]
|
||||
for _, eo in ipairs(Obj and Obj.forDraw and Obj.forDraw() or {}) do actor(eo) end
|
||||
local Ghosts = package.loaded["src.core.game3.ghosts"]
|
||||
for _, entry in ipairs(Map.world or {}) do
|
||||
if Map.warmNear(entry, x0, y0, x1, y1) then
|
||||
for _, eo in ipairs(Ghosts and Ghosts.forDraw(entry.id) or {}) do actor(eo, entry.ox, entry.oy) end
|
||||
end
|
||||
end
|
||||
local gid = Ow.playerGraphicsId(game)
|
||||
if gid then wanted[gid] = 0 end
|
||||
if Ow._stream then Ow._stream:retain(wanted) end
|
||||
for id, priority in pairs(wanted) do Ow.prefetch(id, priority) end
|
||||
end
|
||||
if game then require("src.core.game3.field_plan").prefetch(game) end
|
||||
if Stream then Stream.update() end
|
||||
return true
|
||||
end
|
||||
local queue = Map._warmQueue
|
||||
if not queue then return false end
|
||||
local NativeTileset = package.loaded["src.core.game3.tileset_native"]
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
-- CPU-only object construction shared with the field preparation worker.
|
||||
local GfxIds = require("src.core.game3.scripting.gfx_ids")
|
||||
local MovementTypes = require("src.core.game3.movement_types")
|
||||
local Prepare = {}
|
||||
local CELL, WALK_FRAMES, MOVEMENT_TYPE_INVISIBLE = 16, 16, 0x4C
|
||||
local function facingFromDef(def)
|
||||
local r = tostring(def.facing or def.range or "DOWN"):lower()
|
||||
return (r == "up" or r == "down" or r == "left" or r == "right") and r or "down"
|
||||
end
|
||||
local inPlace
|
||||
local function hostSpec(mt, rx, ry, enabled)
|
||||
local spec = GfxIds.hostMovement(mt, rx, ry)
|
||||
if not inPlace then
|
||||
inPlace = {}
|
||||
for _, kind in ipairs({
|
||||
{ "firered", "MOVEMENT_TYPE_WALK_IN_PLACE_", 16, false },
|
||||
{ "firered", "MOVEMENT_TYPE_WALK_IN_PLACE_FAST_", 8, false },
|
||||
{ "firered", "MOVEMENT_TYPE_JOG_IN_PLACE_", 4, false },
|
||||
{ "emerald", "MOVEMENT_TYPE_WALK_SLOWLY_IN_PLACE_", 32, true },
|
||||
}) do
|
||||
for suffix, dir in pairs({ DOWN = "down", UP = "up", LEFT = "left", RIGHT = "right" }) do
|
||||
local id = MovementTypes.canonOf(kind[1], kind[2] .. suffix)
|
||||
if id then inPlace[id] = { dir = dir, frames = kind[3], always = kind[4] } end
|
||||
end
|
||||
end
|
||||
end
|
||||
local ip = inPlace[mt]
|
||||
if ip and (ip.always or enabled) then
|
||||
spec.movement, spec.face, spec.range, spec.frames = "IN_PLACE", ip.dir, ip.dir:upper(), ip.frames
|
||||
end
|
||||
return spec
|
||||
end
|
||||
|
||||
local MT_BERRY_TREE = 0x0C
|
||||
-- pokeemerald/include/constants/event_object_movement.h:61
|
||||
local MT_TREE_DISGUISE = 0x39
|
||||
local MT_MOUNTAIN_DISGUISE = 0x3A
|
||||
local MT_BURIED = 0x3F
|
||||
|
||||
-- pokeemerald/src/event_object_movement.c:1153
|
||||
local COPY_TYPES = {
|
||||
[0x35] = { init = "up" }, [0x36] = { init = "down" }, [0x37] = { init = "left" }, [0x38] = { init = "right" },
|
||||
[0x3B] = { init = "up", grass = true }, [0x3C] = { init = "down", grass = true },
|
||||
[0x3D] = { init = "left", grass = true }, [0x3E] = { init = "right", grass = true },
|
||||
}
|
||||
function Prepare.initRseKind(eo)
|
||||
local mt = tonumber(eo.movementType) or 0
|
||||
eo.rseKind = nil
|
||||
local copy = COPY_TYPES[mt]
|
||||
if copy then
|
||||
eo.rseKind = "copy"
|
||||
eo.copy = { init = copy.init, grass = copy.grass }
|
||||
else
|
||||
eo.copy = nil
|
||||
end
|
||||
if mt == MT_BERRY_TREE then
|
||||
eo.rseKind = "berry_tree"
|
||||
eo.invisible = true
|
||||
eo.berryTree = eo.berryTree or { id = tonumber(eo.def and (eo.def.berryTreeId or eo.def.trainerRange)) or 0 }
|
||||
else
|
||||
eo.berryTree = nil
|
||||
end
|
||||
if mt == MT_TREE_DISGUISE or mt == MT_MOUNTAIN_DISGUISE then
|
||||
eo.rseKind = "disguise"
|
||||
-- pokeemerald/src/event_object_movement.c:4354
|
||||
local sheet = mt == MT_TREE_DISGUISE and "tree_disguise" or "mountain_disguise"
|
||||
if not (eo.disguise and eo.disguise.sheet == sheet) then eo.disguise = { sheet = sheet } end
|
||||
else
|
||||
eo.disguise = nil
|
||||
end
|
||||
if mt == MT_BURIED then
|
||||
-- pokeemerald/src/event_object_movement.c:4390
|
||||
eo.rseKind = "buried"
|
||||
eo.buried = true
|
||||
eo.invisible = true
|
||||
elseif eo.buried then
|
||||
eo.buried = nil
|
||||
eo.invisible = false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function Prepare.instance(def, ctx)
|
||||
local lid = tonumber(def.localId or def.index) or 0
|
||||
local x, y = tonumber(def.x) or 0, tonumber(def.y) or 0
|
||||
local rawMt = ctx.rawMt
|
||||
if rawMt == nil and def.movementType ~= nil then
|
||||
local ok, value = pcall(MovementTypes.canon, ctx.version, def.movementType)
|
||||
rawMt = ok and value or tonumber(def.movementType)
|
||||
end
|
||||
local mt = rawMt or 0
|
||||
local spec = ctx.spec
|
||||
if not spec then
|
||||
if rawMt then spec = hostSpec(rawMt, def.rangeX, def.rangeY, ctx.inPlace)
|
||||
else
|
||||
local r = def.radius or { x = 1, y = 1 }
|
||||
spec = { movement = def.movement or "STAY", range = def.range or "DOWN", rangeX = r.x, rangeY = r.y, radius = r }
|
||||
end
|
||||
end
|
||||
local facing = (def.facing and facingFromDef(def)) or (rawMt and spec.face) or facingFromDef(def)
|
||||
local resolvedGfx = ctx.graphicsId or def.graphicsId or def.graphics
|
||||
local sprite = def.sprite or (resolvedGfx and GfxIds.spriteFor(resolvedGfx))
|
||||
local elev = ctx.elevation or 0
|
||||
local mg, mn = ctx.group, ctx.num
|
||||
local eo = {
|
||||
localId = lid,
|
||||
originLocalId = tonumber(def.originLocalId or def.localId or def.index) or lid,
|
||||
originMapId = def.originMapId or def.mapId or ctx.mapId,
|
||||
originMapGroup = tonumber(def.originMapGroup or def.mapGroup) or mg,
|
||||
originMapNum = tonumber(def.originMapNum or def.mapNum) or mn,
|
||||
def = def,
|
||||
cellX = x,
|
||||
cellY = y,
|
||||
px = x * CELL,
|
||||
py = y * CELL,
|
||||
homeX = x,
|
||||
homeY = y,
|
||||
facing = facing,
|
||||
sprite = sprite or "SPRITE_YOUNGSTER",
|
||||
graphicsId = resolvedGfx,
|
||||
elevation = elev,
|
||||
currentElevation = tonumber(def.elevation) or 0,
|
||||
movementType = mt,
|
||||
movement = spec.movement,
|
||||
range = spec.range,
|
||||
radius = spec.radius or { x = spec.rangeX, y = spec.rangeY },
|
||||
rangeX = spec.rangeX,
|
||||
rangeY = spec.rangeY,
|
||||
spec = spec,
|
||||
seqIndex = 0,
|
||||
sight = tonumber(def.sight or def.trainerRange) or 0,
|
||||
trainerType = tonumber(def.trainerType) or 0,
|
||||
scriptKey = def.scriptKey,
|
||||
flag = def.flag,
|
||||
visible = ctx.visible ~= false,
|
||||
hidden = ctx.visible == false,
|
||||
-- src/event_object_movement.c:1569
|
||||
invisible = mt == MOVEMENT_TYPE_INVISIBLE,
|
||||
frozen = false,
|
||||
passable = def.passable and true or false,
|
||||
moving = false,
|
||||
progress = 0,
|
||||
stepFrames = WALK_FRAMES,
|
||||
targetX = x,
|
||||
targetY = y,
|
||||
stepFlip = false,
|
||||
animClock = 0,
|
||||
scriptBusy = false,
|
||||
}
|
||||
if ctx.rse then Prepare.initRseKind(eo) end
|
||||
return eo
|
||||
end
|
||||
|
||||
function Prepare.objects(snapshot, cancelled)
|
||||
local out = {}
|
||||
for i, def in ipairs(snapshot.defs) do
|
||||
if cancelled and cancelled() then error("object preparation cancelled") end
|
||||
out[i] = Prepare.instance(def, snapshot)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- Snapshot only serializable values; custom functions/metatables keep demand loading.
|
||||
function Prepare.freeze(value)
|
||||
local visiting, count = {}, 0
|
||||
local function copy(v, depth)
|
||||
if type(v) ~= "table" then
|
||||
if type(v) == "function" or type(v) == "userdata" or type(v) == "thread" then error("custom object definition") end
|
||||
return v
|
||||
end
|
||||
if depth > 16 or visiting[v] or getmetatable(v) then error("custom object definition") end
|
||||
visiting[v] = true
|
||||
local out = {}
|
||||
for k, item in pairs(v) do
|
||||
count = count + 1
|
||||
if count > 32768 then error("large object definition") end
|
||||
out[copy(k, depth + 1)] = copy(item, depth + 1)
|
||||
end
|
||||
visiting[v] = nil
|
||||
return out
|
||||
end
|
||||
local ok, result = pcall(copy, value, 0)
|
||||
return ok and result or nil
|
||||
end
|
||||
function Prepare.matches(a, b)
|
||||
if type(a) ~= type(b) then return false end
|
||||
if type(a) ~= "table" then return a == b end
|
||||
for k, v in pairs(a) do if not Prepare.matches(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
|
||||
return Prepare
|
||||
+124
-123
@@ -13,6 +13,7 @@ local Opcodes = require("src.core.game3.scripting.opcodes")
|
||||
local GfxIds = require("src.core.game3.scripting.gfx_ids")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
local VirtualObjects = require("src.core.game3.virtual_objects")
|
||||
local Prepare = require("src.core.game3.object_prepare")
|
||||
|
||||
local Objects = {}
|
||||
|
||||
@@ -209,106 +210,137 @@ end
|
||||
|
||||
Objects.cloneTemplate = cloneTemplate
|
||||
|
||||
local function newEventObject(def, neighbor)
|
||||
-- Shallow-copy template so setobjectxy / removeobject cannot poison the
|
||||
-- shared events.lua / mapDef.objects tables for the rest of the session.
|
||||
local src = def or {}
|
||||
local function templateCopy(src)
|
||||
src = src or {}
|
||||
local tpl = cloneTemplate(src)
|
||||
def = {}
|
||||
for k, v in pairs(tpl or src) do
|
||||
def[k] = v
|
||||
end
|
||||
local def = {}
|
||||
for k, v in pairs(tpl or src) do def[k] = v end
|
||||
if tpl then
|
||||
def.localId, def.index, def.x, def.y = src.localId, src.index, src.x, src.y
|
||||
def.kind, def.cloneTarget = src.kind, src.cloneTarget
|
||||
end
|
||||
local lid = tonumber(def.localId or def.index) or 0
|
||||
local x = tonumber(def.x) or 0
|
||||
local y = tonumber(def.y) or 0
|
||||
local rawMt = canonMt(def.movementType)
|
||||
local mt = rawMt or 0
|
||||
local spec
|
||||
if rawMt then
|
||||
spec = hostSpec(rawMt, def.rangeX, def.rangeY)
|
||||
else
|
||||
local r = def.radius or { x = 1, y = 1 }
|
||||
spec = {
|
||||
movement = def.movement or "STAY", range = def.range or "DOWN",
|
||||
rangeX = r.x, rangeY = r.y, radius = r,
|
||||
}
|
||||
end
|
||||
local facing = (def.facing and facingFromDef(def))
|
||||
or (rawMt and spec.face) or facingFromDef(def)
|
||||
local sprite = def.sprite
|
||||
return def
|
||||
end
|
||||
local function sameTemplate(src, snapshot)
|
||||
if cloneTemplate(src) then return Prepare.matches(templateCopy(src), snapshot) end
|
||||
return Prepare.matches(src, snapshot)
|
||||
end
|
||||
|
||||
local function newEventObject(src, neighbor, prepared)
|
||||
local def = templateCopy(src)
|
||||
local x, y = tonumber(def.x) or 0, tonumber(def.y) or 0
|
||||
local resolvedGfx = def.graphicsId or def.graphics
|
||||
do
|
||||
local okS, Space = pcall(lazyReq, "src.core.game3.scripting.space")
|
||||
if okS and Space and Space.resolveObjectGraphicsId then
|
||||
local gid = Space.resolveObjectGraphicsId(def, neighbor)
|
||||
if gid then resolvedGfx = gid end
|
||||
end
|
||||
end
|
||||
if not sprite and resolvedGfx then
|
||||
sprite = GfxIds.spriteFor(resolvedGfx)
|
||||
local okS, Sp = pcall(lazyReq, "src.core.game3.scripting.space")
|
||||
if okS and Sp and Sp.resolveObjectGraphicsId then
|
||||
local gid = Sp.resolveObjectGraphicsId(def, neighbor)
|
||||
if gid then resolvedGfx = gid end
|
||||
end
|
||||
local Coll = Collision()
|
||||
local elev = (def.elevation and def.elevation ~= 0 and def.elevation)
|
||||
or (Coll and Coll.elevationAt and Coll.elevationAt(x, y)) or 0
|
||||
local MapCatalog = package.loaded["src.import.gba.map_catalog"]
|
||||
or (pcall(lazyReq, "src.import.gba.map_catalog") and package.loaded["src.import.gba.map_catalog"])
|
||||
local mg, mn = nil, nil
|
||||
local mg, mn
|
||||
if MapCatalog and MapCatalog.groupNumFor and (def.mapId or Objects._mapId) then
|
||||
mg, mn = MapCatalog.groupNumFor(def.mapId or Objects._mapId)
|
||||
end
|
||||
local eo = {
|
||||
localId = lid,
|
||||
originLocalId = tonumber(def.originLocalId or def.localId or def.index) or lid,
|
||||
originMapId = def.originMapId or def.mapId or Objects._mapId,
|
||||
originMapGroup = tonumber(def.originMapGroup or def.mapGroup) or mg,
|
||||
originMapNum = tonumber(def.originMapNum or def.mapNum) or mn,
|
||||
def = def,
|
||||
cellX = x,
|
||||
cellY = y,
|
||||
px = x * CELL,
|
||||
py = y * CELL,
|
||||
homeX = x,
|
||||
homeY = y,
|
||||
facing = facing,
|
||||
sprite = sprite or "SPRITE_YOUNGSTER",
|
||||
graphicsId = resolvedGfx,
|
||||
elevation = elev,
|
||||
currentElevation = tonumber(def.elevation) or 0,
|
||||
movementType = mt,
|
||||
movement = spec.movement,
|
||||
range = spec.range,
|
||||
radius = spec.radius or { x = spec.rangeX, y = spec.rangeY },
|
||||
rangeX = spec.rangeX,
|
||||
rangeY = spec.rangeY,
|
||||
spec = spec,
|
||||
seqIndex = 0,
|
||||
sight = tonumber(def.sight or def.trainerRange) or 0,
|
||||
trainerType = tonumber(def.trainerType) or 0,
|
||||
scriptKey = def.scriptKey,
|
||||
flag = def.flag,
|
||||
visible = objectVisible(def),
|
||||
hidden = not objectVisible(def),
|
||||
-- src/event_object_movement.c:1569
|
||||
invisible = mt == MOVEMENT_TYPE_INVISIBLE,
|
||||
frozen = false,
|
||||
passable = def.passable and true or false,
|
||||
moving = false,
|
||||
progress = 0,
|
||||
stepFrames = WALK_FRAMES,
|
||||
targetX = x,
|
||||
targetY = y,
|
||||
stepFlip = false,
|
||||
animClock = 0,
|
||||
scriptBusy = false,
|
||||
}
|
||||
if isRse() then Objects.initRseKind(eo) end
|
||||
local visible = objectVisible(def)
|
||||
local eo = prepared
|
||||
if not eo then
|
||||
local rawMt = canonMt(def.movementType)
|
||||
eo = Prepare.instance(def, { rawMt = rawMt, spec = rawMt and hostSpec(rawMt, def.rangeX, def.rangeY),
|
||||
version = lazyReq("src.core.GameVersion").get(), mapId = Objects._mapId, rse = isRse(),
|
||||
graphicsId = resolvedGfx, elevation = elev, group = mg, num = mn, visible = visible })
|
||||
else
|
||||
-- Only authoritative live state is rebound here. Construction and movement
|
||||
-- specifications came from an immutable worker snapshot.
|
||||
eo.def = def
|
||||
if tonumber(def.movementType) == nil and def.radius then eo.spec.radius = def.radius end
|
||||
if eo.spec.radius then eo.radius = eo.spec.radius end
|
||||
eo.graphicsId = resolvedGfx
|
||||
eo.sprite = def.sprite or (resolvedGfx and GfxIds.spriteFor(resolvedGfx)) or "SPRITE_YOUNGSTER"
|
||||
eo.elevation, eo.visible, eo.hidden = elev, visible, not visible
|
||||
eo.originMapId = def.originMapId or def.mapId or Objects._mapId
|
||||
eo.originMapGroup = tonumber(def.originMapGroup or def.mapGroup) or mg
|
||||
eo.originMapNum = tonumber(def.originMapNum or def.mapNum) or mn
|
||||
end
|
||||
return eo
|
||||
end
|
||||
|
||||
local preparedMaps, preparationStream = {}, nil
|
||||
local function definitions(mapId, mapDef)
|
||||
local Sp = Space()
|
||||
local ev = Sp and Sp.bundle and Sp.bundle.events and Sp.bundle.events[mapId]
|
||||
local defs = ev and (ev.objects or ev.objectEvents)
|
||||
return type(defs) == "table" and defs or (mapDef and mapDef.objects) or {}
|
||||
end
|
||||
local function preparationCurrent(row, defs)
|
||||
if row.defs ~= defs or row.version ~= lazyReq("src.core.GameVersion").get()
|
||||
or row.inPlace ~= fieldBlock().inPlaceMovementTypes or #row.snapshot.defs ~= #defs then return false end
|
||||
for i, def in ipairs(defs) do
|
||||
if not sameTemplate(def, row.snapshot.defs[i]) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
function Objects.prefetchMap(mapId, mapDef, priority)
|
||||
if not (love and love.thread and love.thread.newThread) then return end
|
||||
local Stream = lazyReq("src.core.game3.asset_stream")
|
||||
if Stream.workerFailed then return end
|
||||
local defs = definitions(mapId, mapDef)
|
||||
local old = preparedMaps[mapId]
|
||||
if old and old.error and preparationCurrent(old, defs) then return false end
|
||||
if old and preparationCurrent(old, defs)
|
||||
and (old.data or (preparationStream and preparationStream.pending[mapId])) then return true end
|
||||
if not preparationStream then
|
||||
preparationStream = Stream.newTask("objects", function(key, data, err)
|
||||
local row = preparedMaps[key]
|
||||
if row then row.data, row.error = data, err end
|
||||
end)
|
||||
end
|
||||
if old then
|
||||
local wanted = {}; for id in pairs(preparedMaps) do if id ~= mapId then wanted[id] = true end end
|
||||
preparationStream:retain(wanted)
|
||||
end
|
||||
local templates = {}
|
||||
for i, def in ipairs(defs) do templates[i] = templateCopy(def) end
|
||||
local frozen = Prepare.freeze(templates)
|
||||
if not frozen then preparedMaps[mapId] = nil; return end
|
||||
local version = lazyReq("src.core.GameVersion").get()
|
||||
local inPlaceEnabled = fieldBlock().inPlaceMovementTypes
|
||||
local snapshot = { defs = frozen, version = version, mapId = mapId, rse = isRse(), inPlace = inPlaceEnabled }
|
||||
preparedMaps[mapId] = { defs = defs, version = version, inPlace = inPlaceEnabled, snapshot = snapshot }
|
||||
preparationStream:submit(mapId, snapshot, priority)
|
||||
lazyReq("src.core.game3.asset_stream").poll()
|
||||
return preparationStream.pending[mapId] ~= nil or preparedMaps[mapId].data ~= nil
|
||||
end
|
||||
function Objects.preparationReady(mapId, mapDef)
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if Stream then Stream.poll() end
|
||||
local row = preparedMaps[mapId]
|
||||
return row and row.data ~= nil and preparationCurrent(row, definitions(mapId, mapDef)) or false
|
||||
end
|
||||
function Objects.retainPrepared(wanted)
|
||||
if preparationStream then preparationStream:retain(wanted) end
|
||||
for id in pairs(preparedMaps) do if not wanted[id] then preparedMaps[id] = nil end end
|
||||
end
|
||||
function Objects._preparationPending(mapId)
|
||||
return preparationStream and preparationStream.pending[mapId] ~= nil
|
||||
end
|
||||
local function takePrepared(mapId, defs)
|
||||
local Stream = package.loaded["src.core.game3.asset_stream"]
|
||||
if Stream then Stream.poll() end
|
||||
local row = preparedMaps[mapId]
|
||||
if row and row.data and preparationCurrent(row, defs) then
|
||||
preparedMaps[mapId] = nil
|
||||
Objects._lastPreparationRoute = "worker"
|
||||
return row.data, row.snapshot.defs
|
||||
end
|
||||
Objects._lastPreparationRoute = "sync"
|
||||
end
|
||||
local function preparedRow(rows, snapshots, i, def)
|
||||
return rows and sameTemplate(def, snapshots[i]) and rows[i] or nil
|
||||
end
|
||||
|
||||
function Objects.clear()
|
||||
Objects._byId = {}
|
||||
Objects._order = {}
|
||||
@@ -322,6 +354,7 @@ end
|
||||
|
||||
-- pokefirered/src/overworld.c:405
|
||||
function Objects.reset()
|
||||
Objects.retainPrepared({})
|
||||
Objects.clear()
|
||||
Objects._perm = {}
|
||||
Objects._templateMt = {}
|
||||
@@ -530,8 +563,8 @@ local function resolveContextualMapObjects(mapId)
|
||||
end
|
||||
end
|
||||
|
||||
local function spawnFromTemplate(def, mapId)
|
||||
local eo = newEventObject(def)
|
||||
local function spawnFromTemplate(def, mapId, prepared)
|
||||
local eo = newEventObject(def, nil, prepared)
|
||||
if mapId and (not eo.originMapGroup or not eo.originMapNum) then
|
||||
local okC, MapCatalog = pcall(lazyReq, "src.import.gba.map_catalog")
|
||||
if okC and MapCatalog and MapCatalog.groupNumFor then
|
||||
@@ -617,9 +650,10 @@ function Objects.loadMap(game, mapId, mapDef)
|
||||
end
|
||||
end
|
||||
resolveContextualMapObjects(mapId)
|
||||
local prepared, snapshots = takePrepared(mapId, Objects._defs)
|
||||
local announce = ModRuntime.wants("world.npc_spawned")
|
||||
for _, def in ipairs(Objects._defs) do
|
||||
local eo = spawnFromTemplate(def, mapId)
|
||||
for i, def in ipairs(Objects._defs) do
|
||||
local eo = spawnFromTemplate(def, mapId, preparedRow(prepared, snapshots, i, def))
|
||||
if eo.localId > 0 then
|
||||
Objects._byId[eo.localId] = eo
|
||||
Objects._order[#Objects._order + 1] = eo.localId
|
||||
@@ -1819,41 +1853,7 @@ function Objects.copyDirection(copyInit, playerInit, playerMove)
|
||||
return IDX_DIR[COPY_TO[ci][COPY_FOR[pi][pm]]]
|
||||
end
|
||||
|
||||
function Objects.initRseKind(eo)
|
||||
local mt = tonumber(eo.movementType) or 0
|
||||
eo.rseKind = nil
|
||||
local copy = COPY_TYPES[mt]
|
||||
if copy then
|
||||
eo.rseKind = "copy"
|
||||
eo.copy = { init = copy.init, grass = copy.grass }
|
||||
else
|
||||
eo.copy = nil
|
||||
end
|
||||
if mt == MT_BERRY_TREE then
|
||||
eo.rseKind = "berry_tree"
|
||||
eo.invisible = true
|
||||
eo.berryTree = eo.berryTree or { id = tonumber(eo.def and (eo.def.berryTreeId or eo.def.trainerRange)) or 0 }
|
||||
else
|
||||
eo.berryTree = nil
|
||||
end
|
||||
if mt == MT_TREE_DISGUISE or mt == MT_MOUNTAIN_DISGUISE then
|
||||
eo.rseKind = "disguise"
|
||||
-- pokeemerald/src/event_object_movement.c:4354
|
||||
local sheet = mt == MT_TREE_DISGUISE and "tree_disguise" or "mountain_disguise"
|
||||
if not (eo.disguise and eo.disguise.sheet == sheet) then eo.disguise = { sheet = sheet } end
|
||||
else
|
||||
eo.disguise = nil
|
||||
end
|
||||
if mt == MT_BURIED then
|
||||
-- pokeemerald/src/event_object_movement.c:4390
|
||||
eo.rseKind = "buried"
|
||||
eo.buried = true
|
||||
eo.invisible = true
|
||||
elseif eo.buried then
|
||||
eo.buried = nil
|
||||
eo.invisible = false
|
||||
end
|
||||
end
|
||||
Objects.initRseKind = Prepare.initRseKind
|
||||
|
||||
-- pokeemerald/src/event_object_movement.c:1890
|
||||
local function setBerryTreeGraphics(eo, bt, tree)
|
||||
@@ -2007,8 +2007,9 @@ function Objects.spawnFromDefs(defs, mapDef, mapId)
|
||||
local Sp = mapId and Space()
|
||||
local nb = Sp and Sp.neighborObjectState and Sp.neighborObjectState(mapId)
|
||||
if mapId and not nb and not (Sp and Sp.mapId == mapId) then nb = { store = { flags = {}, vars = {} }, perm = {}, movementType = {} } end
|
||||
for _, def in ipairs(defs or {}) do
|
||||
local eo = newEventObject(def, nb)
|
||||
local prepared, snapshots = takePrepared(mapId, defs or {})
|
||||
for i, def in ipairs(defs or {}) do
|
||||
local eo = newEventObject(def, nb, preparedRow(prepared, snapshots, i, def))
|
||||
if eo.localId > 0 then
|
||||
eo.mapDef = mapDef
|
||||
if nb then
|
||||
|
||||
@@ -4,7 +4,13 @@ local Extract = require("src.import.gba.extract_island1")
|
||||
local OwExtract = require("src.import.gba.ow_extract")
|
||||
local Versions = require("src.import.gba.versions")
|
||||
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local OwSprites = {}
|
||||
local makeStream
|
||||
local function resetStream()
|
||||
if OwSprites._stream then OwSprites._stream:cancel() end
|
||||
OwSprites._stream = OwSprites._cache and makeStream() or nil
|
||||
end
|
||||
|
||||
OwSprites._cache = nil
|
||||
OwSprites._manifest = nil
|
||||
@@ -34,22 +40,35 @@ local function owRoot()
|
||||
return (Extract.CACHE_ROOT or "data/generated/gba") .. "/ow"
|
||||
end
|
||||
|
||||
local function loadManifest()
|
||||
OwSprites._manifestAttempted = true
|
||||
local src = OwSprites._cache and OwSprites._cache:read(owRoot() .. "/manifest.lua")
|
||||
local chunk = src and load(src, "@ow/manifest.lua", "t", {})
|
||||
OwSprites._manifest = chunk and chunk() or nil
|
||||
end
|
||||
|
||||
local function resetPalettes(changed)
|
||||
if changed then OwSprites._overrides = {} end
|
||||
for _, o in pairs(OwSprites._overrides or {}) do o.spr = nil end
|
||||
end
|
||||
|
||||
function OwSprites.install(cache)
|
||||
resetPalettes(OwSprites._cache ~= cache or OwSprites._root ~= owRoot())
|
||||
OwSprites._root = owRoot()
|
||||
OwSprites._cache = cache
|
||||
OwSprites._loaded = {}
|
||||
resetStream()
|
||||
OwSprites._reflectionLoaded = {}
|
||||
OwSprites._manifest = nil
|
||||
OwSprites._logged = false
|
||||
if not cache then return end
|
||||
local src = cache:read(owRoot() .. "/manifest.lua")
|
||||
if src then
|
||||
local chunk = load(src, "@ow/manifest.lua", "t", {})
|
||||
if chunk then OwSprites._manifest = chunk() end
|
||||
end
|
||||
loadManifest()
|
||||
end
|
||||
|
||||
function OwSprites.invalidate()
|
||||
resetPalettes(false)
|
||||
OwSprites._manifestAttempted = false
|
||||
OwSprites._loaded = {}
|
||||
resetStream()
|
||||
OwSprites._reflectionLoaded = {}
|
||||
OwSprites._manifest = nil
|
||||
OwSprites._logged = false
|
||||
@@ -62,69 +81,33 @@ function OwSprites.ready()
|
||||
return OwExtract.ready(cache, Extract.CACHE_ROOT or "data/generated/gba")
|
||||
end
|
||||
|
||||
local function load_one(gid)
|
||||
local cache = OwSprites._cache
|
||||
if not cache then return nil end
|
||||
local root = owRoot()
|
||||
local metaBlob = cache:read(root .. "/" .. gid .. ".meta")
|
||||
local rgba = cache:read(root .. "/" .. gid .. ".rgba")
|
||||
if not metaBlob or not rgba then return nil end
|
||||
local meta = OwExtract.decodeMeta(metaBlob)
|
||||
if not meta then return nil end
|
||||
local function uploadSprite(meta)
|
||||
local image = love.graphics.newImage(meta.imageData)
|
||||
image:setFilter("nearest", "nearest")
|
||||
coroutine.yield("texture")
|
||||
local w, h, n = meta.width, meta.height, meta.frameCount
|
||||
-- decodeMeta reads these as u16 without validating, so a corrupt .meta can
|
||||
-- carry 65535 for any of them and `aw * ah` then reaches ~4.3e9 pixels on a
|
||||
-- cache file in the user-writable save directory. Bound them before sizing.
|
||||
local MAX_FRAME_DIM, MAX_FRAMES = 256, 512
|
||||
if type(w) ~= "number" or type(h) ~= "number" or type(n) ~= "number"
|
||||
or w < 1 or h < 1 or n < 1
|
||||
or w > MAX_FRAME_DIM or h > MAX_FRAME_DIM or n > MAX_FRAMES then
|
||||
return nil
|
||||
end
|
||||
local aw, ah = w, h * n
|
||||
if #rgba ~= aw * ah * 4 then
|
||||
if #rgba < aw * h * 4 then return nil end
|
||||
end
|
||||
if not (love and love.image and love.graphics) then return nil end
|
||||
|
||||
local ok, imageData = pcall(love.image.newImageData, aw, ah, "rgba8", rgba)
|
||||
if not ok or not imageData then
|
||||
imageData = love.image.newImageData(aw, ah)
|
||||
local i = 1
|
||||
for y = 0, ah - 1 do
|
||||
for x = 0, aw - 1 do
|
||||
local r = (rgba:byte(i) or 0) / 255
|
||||
local g = (rgba:byte(i + 1) or 0) / 255
|
||||
local b = (rgba:byte(i + 2) or 0) / 255
|
||||
local a = (rgba:byte(i + 3) or 0) / 255
|
||||
imageData:setPixel(x, y, r, g, b, a)
|
||||
i = i + 4
|
||||
end
|
||||
end
|
||||
end
|
||||
local image = love.graphics.newImage(imageData)
|
||||
if image.setFilter then image:setFilter("nearest", "nearest") end
|
||||
local quads = {}
|
||||
for fi = 0, n - 1 do
|
||||
quads[fi] = love.graphics.newQuad(0, fi * h, w, h, aw, ah)
|
||||
quads[fi] = love.graphics.newQuad(0, fi * h, w, h, w, h * n)
|
||||
if fi % 32 == 31 then coroutine.yield("quads") end
|
||||
end
|
||||
return {
|
||||
image = image,
|
||||
-- Kept so setObjectPalette can recolour the sheet: LOVE 11 has no
|
||||
-- Image:newImageData, so the decoded pixels are the only CPU-side source.
|
||||
imageData = imageData,
|
||||
quads = quads,
|
||||
width = w,
|
||||
height = h,
|
||||
frameCount = n,
|
||||
inanimate = meta.inanimate,
|
||||
paletteTag = meta.paletteTag,
|
||||
reflectionPaletteTag = meta.reflectionPaletteTag,
|
||||
reflectionPaletteMappedTag = meta.reflectionPaletteMappedTag,
|
||||
palette = meta.palette,
|
||||
reflectionPalette = meta.reflectionPalette,
|
||||
mappedReflectionPalette = meta.mappedReflectionPalette,
|
||||
}
|
||||
meta.image, meta.quads = image, quads
|
||||
meta.bytes = nil
|
||||
return meta
|
||||
end
|
||||
makeStream = function()
|
||||
return Stream.new("sprite", OwSprites._cache, owRoot(), uploadSprite, function(gid, spr)
|
||||
OwSprites._loaded[gid] = spr
|
||||
if not OwSprites._logged then
|
||||
print(string.format("[game3/ow] sprites ready (%s sheets)", tostring(OwSprites._manifest and OwSprites._manifest.count or "?")))
|
||||
OwSprites._logged = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function OwSprites.prefetch(gid, priority)
|
||||
gid = tonumber(gid)
|
||||
if gid and not OwSprites._loaded[gid] and OwSprites._stream then OwSprites._stream:prefetch(gid, priority) end
|
||||
end
|
||||
|
||||
function OwSprites.get(graphicsId)
|
||||
@@ -132,20 +115,8 @@ function OwSprites.get(graphicsId)
|
||||
if graphicsId == nil then return nil end
|
||||
local cached = OwSprites._loaded[graphicsId]
|
||||
if cached then return cached end
|
||||
if not OwSprites._manifest then
|
||||
OwSprites.install(OwSprites._cache)
|
||||
end
|
||||
local spr = load_one(graphicsId)
|
||||
if spr then
|
||||
OwSprites._loaded[graphicsId] = spr
|
||||
if not OwSprites._logged then
|
||||
local n = OwSprites._manifest and OwSprites._manifest.count
|
||||
print(string.format("[game3/ow] FRLG sprites ready (%s sheets)",
|
||||
tostring(n or "?")))
|
||||
OwSprites._logged = true
|
||||
end
|
||||
end
|
||||
return spr
|
||||
if not OwSprites._manifestAttempted then loadManifest() end
|
||||
return OwSprites._stream and OwSprites._stream:get(graphicsId) or nil
|
||||
end
|
||||
|
||||
-- src/data/object_events/object_event_anims.h:633
|
||||
@@ -325,7 +296,7 @@ function OwSprites.setObjectPalette(graphicsId, key, colours, sourceColours)
|
||||
graphicsId = tonumber(graphicsId)
|
||||
if graphicsId == nil or type(key) ~= "string" then return false end
|
||||
local current = OwSprites._overrides[graphicsId]
|
||||
if current and current.key == key then return true end
|
||||
if current and current.key == key and current.spr then return true end
|
||||
if type(colours) ~= "table" or #colours == 0 then return false end
|
||||
|
||||
local base = OwSprites.get(graphicsId)
|
||||
@@ -337,7 +308,7 @@ function OwSprites.setObjectPalette(graphicsId, key, colours, sourceColours)
|
||||
end
|
||||
local spr = recolour_sprite(base, from, colours)
|
||||
if not spr then return false end
|
||||
OwSprites._overrides[graphicsId] = { key = key, spr = spr }
|
||||
OwSprites._overrides[graphicsId] = { key = key, spr = spr, colours = colours, sourceColours = from }
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -426,6 +397,10 @@ function OwSprites.getDraw(graphicsId)
|
||||
graphicsId = tonumber(graphicsId)
|
||||
if graphicsId == nil then return nil end
|
||||
local ov = OwSprites._overrides and OwSprites._overrides[graphicsId]
|
||||
if ov and not ov.spr then
|
||||
OwSprites.setObjectPalette(graphicsId, ov.key, ov.colours, ov.sourceColours)
|
||||
ov = OwSprites._overrides[graphicsId]
|
||||
end
|
||||
if ov and ov.spr then return ov.spr end
|
||||
return OwSprites.get(graphicsId)
|
||||
end
|
||||
@@ -493,9 +468,7 @@ function OwSprites.draw(graphicsId, px, py, camX, camY, facing, walkPhase, stepF
|
||||
end
|
||||
|
||||
function OwSprites.avatars()
|
||||
if not OwSprites._manifest and OwSprites._cache then
|
||||
OwSprites.install(OwSprites._cache)
|
||||
end
|
||||
if not OwSprites._manifestAttempted and OwSprites._cache then loadManifest() end
|
||||
local m = OwSprites._manifest
|
||||
return m and m.avatars or nil
|
||||
end
|
||||
|
||||
@@ -27,6 +27,17 @@ local function stdString(id)
|
||||
end
|
||||
Adapters.stdString = stdString
|
||||
|
||||
-- pokeemerald/src/scrcmd.c:1599 StringCopy(..., gDecorations[decorId].name)
|
||||
local function decorationName(src)
|
||||
local ok, DecorInv = pcall(require, "src.core.game3.rse.decoration_inventory")
|
||||
if not ok or type(DecorInv) ~= "table" or not DecorInv.info then return nil end
|
||||
local okInfo, info = pcall(DecorInv.info, src)
|
||||
if okInfo and type(info) == "table" and type(info.name) == "string" and info.name ~= "" then
|
||||
return info.name
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/event_object_movement.c:5208 GetOppositeDirection
|
||||
local OPPOSITE_DIR = { down = "up", up = "down", left = "right", right = "left" }
|
||||
|
||||
@@ -161,6 +172,9 @@ function Adapters.stub(opts)
|
||||
if op == "bufferstdstring" then
|
||||
return stdString(src)
|
||||
end
|
||||
if op == "bufferdecorationname" then
|
||||
return decorationName(src)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
a.leadMonName = opts.leadMonName or function() return "POKéMON" end
|
||||
@@ -1527,6 +1541,9 @@ function Adapters.host(mod, game, world)
|
||||
if op == "bufferstdstring" then
|
||||
return stdString(src) or tostring(src)
|
||||
end
|
||||
if op == "bufferdecorationname" then
|
||||
return decorationName(src)
|
||||
end
|
||||
if op == "bufferpartymonnick" then
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local party = Runtime and Runtime.session and Runtime.session.party
|
||||
|
||||
@@ -295,11 +295,11 @@ local function load_bank(cache, pair, kind, info)
|
||||
end
|
||||
|
||||
-- pokefirered/src/tileset_anims.c:223
|
||||
function TilesetAnim.bindPair(pair, atlas)
|
||||
function TilesetAnim.bindPair(pair, atlas, prepared)
|
||||
if not Versions.NATIVE_RENDER or Versions.TILESET_ANIM == false then
|
||||
return false
|
||||
end
|
||||
local cache = TilesetAnim._cache
|
||||
local cache = prepared and { read = function(_, rel) return prepared[rel] end } or TilesetAnim._cache
|
||||
if not cache or not pair or not atlas then return false end
|
||||
local entry = TilesetAnim._pairs[pair]
|
||||
if entry == false then return false end
|
||||
@@ -326,9 +326,13 @@ function TilesetAnim.bindPair(pair, atlas)
|
||||
end
|
||||
TilesetAnim._visible[pair] = true
|
||||
if entry.rse then return true end
|
||||
TilesetAnim._applyKind(entry, "water", TilesetAnim._waterFrame)
|
||||
TilesetAnim._applyKind(entry, "sand", TilesetAnim._sandFrame)
|
||||
TilesetAnim._applyKind(entry, "flower", TilesetAnim._flowerFrame)
|
||||
local dirty = false
|
||||
if TilesetAnim._applyKind(entry, "water", TilesetAnim._waterFrame, true) then dirty = true end
|
||||
if TilesetAnim._applyKind(entry, "sand", TilesetAnim._sandFrame, true) then dirty = true end
|
||||
if TilesetAnim._applyKind(entry, "flower", TilesetAnim._flowerFrame, true) then dirty = true end
|
||||
-- Initial/catch-up preparation uploads once after all CPU patches. This
|
||||
-- also keeps a warming pair's final step within one texture submission.
|
||||
if dirty then NativeTileset().flush(atlas, true, false) end
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -370,7 +374,7 @@ local function get_frame_piece(bank, frame, mi, srcOff)
|
||||
return nil
|
||||
end
|
||||
|
||||
function TilesetAnim._applyKind(entry, kind, frame)
|
||||
function TilesetAnim._applyKind(entry, kind, frame, deferUpload)
|
||||
local bank = entry.banks[kind]
|
||||
if not bank or bank.frames < 1 then return end
|
||||
frame = frame % bank.frames
|
||||
@@ -411,6 +415,7 @@ function TilesetAnim._applyKind(entry, kind, frame)
|
||||
end
|
||||
end
|
||||
end
|
||||
if deferUpload then return true end
|
||||
if partial then
|
||||
NativeTileset().flush(ts, true, false)
|
||||
elseif ts.image and love and love.graphics then
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
local Extract = require("src.import.gba.extract_island1")
|
||||
local NativePack = require("src.import.gba.native_pack")
|
||||
local Palette = require("src.core.game3.palette")
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local Versions = require("src.import.gba.versions")
|
||||
|
||||
local NativeTileset = {}
|
||||
@@ -13,7 +13,12 @@ NativeTileset._cache = nil
|
||||
NativeTileset._logged = {}
|
||||
NativeTileset._ready = {}
|
||||
|
||||
local NATIVE = Extract.NATIVE_ROOT or (Extract.CACHE_ROOT .. "/native")
|
||||
local makeStream
|
||||
local function nativeRoot() return Extract.NATIVE_ROOT or (Extract.CACHE_ROOT .. "/native") end
|
||||
local function resetStream()
|
||||
if NativeTileset._stream then NativeTileset._stream:cancel() end
|
||||
NativeTileset._stream = NativeTileset._cache and makeStream() or nil
|
||||
end
|
||||
|
||||
local function log(msg)
|
||||
print("[game3/native] " .. tostring(msg))
|
||||
@@ -24,6 +29,7 @@ function NativeTileset.install(cache, _bundle)
|
||||
NativeTileset._pairs = {}
|
||||
NativeTileset._logged = {}
|
||||
NativeTileset._ready = {}
|
||||
resetStream()
|
||||
local okA, TilesetAnim = pcall(require, "src.core.game3.tileset_anim")
|
||||
if okA and TilesetAnim and TilesetAnim.install then
|
||||
TilesetAnim.install(cache)
|
||||
@@ -34,134 +40,21 @@ function NativeTileset.invalidate()
|
||||
NativeTileset._pairs = {}
|
||||
NativeTileset._logged = {}
|
||||
NativeTileset._ready = {}
|
||||
resetStream()
|
||||
local okA, TilesetAnim = pcall(require, "src.core.game3.tileset_anim")
|
||||
if okA and TilesetAnim and TilesetAnim.invalidate then
|
||||
TilesetAnim.invalidate()
|
||||
end
|
||||
end
|
||||
|
||||
local function rgba_to_image(rgba, w, h)
|
||||
if not (love and love.image and love.image.newImageData) then
|
||||
return nil, nil, "love.image unavailable"
|
||||
end
|
||||
local ok, imageData = pcall(love.image.newImageData, w, h, "rgba8", rgba)
|
||||
if not ok or not imageData then
|
||||
ok, imageData = pcall(function()
|
||||
local id = love.image.newImageData(w, h)
|
||||
if id.setString then
|
||||
id:setString(rgba)
|
||||
end
|
||||
return id
|
||||
end)
|
||||
end
|
||||
if not ok or not imageData then
|
||||
imageData = love.image.newImageData(w, h)
|
||||
local i = 1
|
||||
for y = 0, h - 1 do
|
||||
for x = 0, w - 1 do
|
||||
local r = rgba:byte(i) or 0
|
||||
local g = rgba:byte(i + 1) or 0
|
||||
local b = rgba:byte(i + 2) or 0
|
||||
local a = rgba:byte(i + 3) or 255
|
||||
imageData:setPixel(x, y, r / 255, g / 255, b / 255, a / 255)
|
||||
i = i + 4
|
||||
end
|
||||
end
|
||||
end
|
||||
local image = love.graphics.newImage(imageData)
|
||||
if image.setFilter then image:setFilter("nearest", "nearest") end
|
||||
return image, imageData
|
||||
end
|
||||
|
||||
local function bake_or_load(cache, pair, idxTbl, rgb, hash, tag, transparentZero)
|
||||
local w = (idxTbl.atlasCols or 16) * 16
|
||||
local h = (idxTbl.atlasRows or 1) * 16
|
||||
local rgbaRel = NATIVE .. "/" .. pair .. "/atlas_" .. tag .. "_" .. hash .. ".rgba"
|
||||
local rgba = cache:read(rgbaRel)
|
||||
if not rgba or #rgba ~= w * h * 4 then
|
||||
rgba, w, h = NativePack.bakeRgba(idxTbl, rgb, { transparentZero = transparentZero })
|
||||
pcall(function() cache:write(rgbaRel, rgba) end)
|
||||
end
|
||||
return rgba_to_image(rgba, w, h)
|
||||
end
|
||||
|
||||
local function load_pair(cache, pair)
|
||||
local idxBlob = cache:read(NATIVE .. "/" .. pair .. "/mids.idx")
|
||||
local palBlob = cache:read(NATIVE .. "/" .. pair .. "/palettes.bin")
|
||||
if not idxBlob or not palBlob then
|
||||
return nil, "missing native blobs for " .. tostring(pair)
|
||||
end
|
||||
local idxTbl, ierr = NativePack.decodeIdx(idxBlob)
|
||||
if not idxTbl then return nil, ierr end
|
||||
local rgb, bgr = Palette.load(palBlob)
|
||||
if not rgb then return nil, bgr end
|
||||
|
||||
local hash = Palette.hash(bgr, idxBlob)
|
||||
local layered = cache:exists(NATIVE .. "/" .. pair .. "/mids_over.idx")
|
||||
local tag = layered and "u" or "flat"
|
||||
local image, imageData, err = bake_or_load(cache, pair, idxTbl, rgb, hash, tag, false)
|
||||
if not image then return nil, err or "bake under failed" end
|
||||
|
||||
local midToSlot = {}
|
||||
for i, mid in ipairs(idxTbl.midIds or {}) do
|
||||
midToSlot[mid] = i - 1
|
||||
end
|
||||
|
||||
local cols = idxTbl.atlasCols or 16
|
||||
if (idxTbl.atlasRows or 0) * 16 > 2048 then
|
||||
log("WARNING atlas rows large for " .. pair)
|
||||
end
|
||||
|
||||
local ts = {
|
||||
pair = pair,
|
||||
image = image,
|
||||
imageData = imageData,
|
||||
midToSlot = midToSlot,
|
||||
cols = cols,
|
||||
rows = idxTbl.atlasRows or 1,
|
||||
midCount = idxTbl.midCount or 0,
|
||||
quads = {},
|
||||
layered = false,
|
||||
overImage = nil,
|
||||
overImageData = nil,
|
||||
overQuads = {},
|
||||
idxBlob = idxBlob,
|
||||
overBlob = nil,
|
||||
bgr = bgr,
|
||||
slotPix = {},
|
||||
}
|
||||
|
||||
if layered then
|
||||
local overBlob = cache:read(NATIVE .. "/" .. pair .. "/mids_over.idx")
|
||||
local overTbl = overBlob and NativePack.decodeIdx(overBlob)
|
||||
if overTbl then
|
||||
local overHash = Palette.hash(bgr, overBlob)
|
||||
local oImg, oData = bake_or_load(cache, pair, overTbl, rgb, overHash, "o", true)
|
||||
if oImg then
|
||||
ts.layered = true
|
||||
ts.overImage = oImg
|
||||
ts.overImageData = oData
|
||||
ts.overBlob = overBlob
|
||||
end
|
||||
end
|
||||
local midBlob = cache:read(NATIVE .. "/" .. pair .. "/mids_mid.idx")
|
||||
local midTbl = midBlob and NativePack.decodeIdx(midBlob)
|
||||
if midTbl then
|
||||
ts.midImage = bake_or_load(cache, pair, midTbl, rgb, Palette.hash(bgr, midBlob), "m", true)
|
||||
end
|
||||
end
|
||||
|
||||
return ts
|
||||
end
|
||||
|
||||
function NativeTileset.ready(pair)
|
||||
if not Versions.NATIVE_RENDER then return false end
|
||||
if not pair then return false end
|
||||
if NativeTileset._pairs[pair] or NativeTileset._ready[pair] then return true end
|
||||
local cache = NativeTileset._cache
|
||||
if not cache then return false end
|
||||
local ok = cache:exists(NATIVE .. "/" .. pair .. "/mids.idx")
|
||||
and cache:exists(NATIVE .. "/" .. pair .. "/palettes.bin")
|
||||
local ok = cache:exists(nativeRoot() .. "/" .. pair .. "/mids.idx")
|
||||
and cache:exists(nativeRoot() .. "/" .. pair .. "/palettes.bin")
|
||||
if ok then NativeTileset._ready[pair] = true end
|
||||
return ok
|
||||
end
|
||||
@@ -175,40 +68,55 @@ local function tilesetAnim()
|
||||
return animMod or nil
|
||||
end
|
||||
|
||||
local function bind_anim(pair, atlas)
|
||||
local function bind_anim(pair, atlas, prepared)
|
||||
local TilesetAnim = tilesetAnim()
|
||||
if TilesetAnim and TilesetAnim.bindPair then
|
||||
TilesetAnim.bindPair(pair, atlas)
|
||||
TilesetAnim.bindPair(pair, atlas, prepared)
|
||||
end
|
||||
end
|
||||
|
||||
local function uploadPair(data)
|
||||
local ts = { pair = data.pair, midToSlot = data.midToSlot, cols = data.cols, rows = data.rows,
|
||||
midCount = data.midCount, layered = data.layered or false, idxBlob = data.idxBlob,
|
||||
overBlob = data.overBlob, bgr = data.bgr, quads = {}, overQuads = {}, slotPix = {}, preparedAnim = data.animFiles }
|
||||
for _, layer in ipairs(data.layers) do
|
||||
local image = love.graphics.newImage(layer.data)
|
||||
image:setFilter("nearest", "nearest")
|
||||
ts[layer.imageKey], ts[layer.dataKey] = image, layer.data
|
||||
coroutine.yield("texture")
|
||||
end
|
||||
return ts
|
||||
end
|
||||
makeStream = function()
|
||||
return Stream.new("pair", NativeTileset._cache, nativeRoot(), uploadPair, function(pair, ts)
|
||||
NativeTileset._pairs[pair] = ts
|
||||
bind_anim(pair, ts, ts.preparedAnim)
|
||||
ts.preparedAnim = nil
|
||||
if not NativeTileset._logged[pair] then
|
||||
log(string.format("atlas ready pair=%s mids=%d %dx%d layered=%s", pair,
|
||||
ts.midCount, ts.cols * 16, ts.rows * 16, tostring(ts.layered)))
|
||||
NativeTileset._logged[pair] = true
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function NativeTileset.prefetch(pair, priority)
|
||||
if type(pair) == "string" and not NativeTileset._pairs[pair] and NativeTileset._stream then
|
||||
NativeTileset._stream:prefetch(pair, priority)
|
||||
end
|
||||
end
|
||||
|
||||
function NativeTileset.get(pair)
|
||||
if not pair then return nil end
|
||||
local cached = NativeTileset._pairs[pair]
|
||||
if cached then
|
||||
-- Cheap for a bound pair (no manifest read): marks it visible and
|
||||
-- catches its atlas up to the current anim frames before a draw.
|
||||
-- FieldView reads cached atlases directly and does not come here per frame.
|
||||
bind_anim(pair, cached)
|
||||
return cached
|
||||
end
|
||||
local cache = NativeTileset._cache
|
||||
if not cache then return nil end
|
||||
local ts, err = load_pair(cache, pair)
|
||||
if not ts then
|
||||
if not NativeTileset._logged[pair] then
|
||||
log("load failed " .. tostring(pair) .. ": " .. tostring(err))
|
||||
NativeTileset._logged[pair] = true
|
||||
end
|
||||
return nil
|
||||
end
|
||||
NativeTileset._pairs[pair] = ts
|
||||
if not NativeTileset._logged[pair] then
|
||||
log(string.format("atlas ready pair=%s mids=%d %dx%d layered=%s",
|
||||
pair, ts.midCount, ts.cols * 16, ts.rows * 16, tostring(ts.layered)))
|
||||
if cached then bind_anim(pair, cached); return cached end
|
||||
local stream = NativeTileset._stream
|
||||
if not stream then return nil end
|
||||
local ts, err = stream:get(pair)
|
||||
if not ts and not NativeTileset._logged[pair] then
|
||||
log("load failed " .. tostring(pair) .. ": " .. tostring(err))
|
||||
NativeTileset._logged[pair] = true
|
||||
end
|
||||
bind_anim(pair, ts)
|
||||
return ts
|
||||
end
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ function VoidFill.label(mode)
|
||||
end
|
||||
|
||||
function VoidFill.invalidate()
|
||||
VoidFill._revision = (VoidFill._revision or 0) + 1
|
||||
VoidFill._borders = {}
|
||||
local FieldView = package.loaded["src.core.game3.field_view"]
|
||||
if FieldView then FieldView._nativeDirty = true end
|
||||
|
||||
@@ -235,6 +235,7 @@ function NativePack.bakeRgba(idxTbl, rgbPals, opts)
|
||||
local pixels = idxTbl.pixels or {}
|
||||
local rowChunks = {}
|
||||
for ay = 0, h - 1 do
|
||||
if ay % 16 == 0 and opts.cancelled and opts.cancelled() then return nil, w, h end
|
||||
local midRow = math.floor(ay / 16)
|
||||
local py = ay % 16
|
||||
local line = {}
|
||||
|
||||
@@ -496,6 +496,15 @@ function Renderer:endWorldPass()
|
||||
love.graphics.setCanvas(self.canvas)
|
||||
end
|
||||
|
||||
-- A field compositor may consume the current world into another compatible
|
||||
-- target. Its caller continues drawing into the replacement until endWorldPass.
|
||||
function Renderer:exchangeWorldCanvas(current, replacement)
|
||||
if self.worldCanvas ~= current or not self.worldActive then return false end
|
||||
if current:getWidth() ~= replacement:getWidth() or current:getHeight() ~= replacement:getHeight() then return false end
|
||||
self.worldCanvas = replacement
|
||||
return true
|
||||
end
|
||||
|
||||
-- Tilt mode's upright pass: standing things (sprites, tall-grass feet
|
||||
-- overdraw, screen-anchored FX) draw here instead of into the ground
|
||||
-- world canvas, each already projected to its ground anchor and colorized
|
||||
|
||||
@@ -322,10 +322,13 @@ local function step_pic_bounce()
|
||||
end
|
||||
|
||||
-- pokefirered/src/pokemon_summary_screen.c:3956
|
||||
-- Emerald's summary has no sEggPicShake table
|
||||
local function step_egg_shake()
|
||||
local b = SummaryMenu._bounce
|
||||
if b.count >= 2 then return end
|
||||
local deltas = SummaryChrome.manifest().eggPicShake[b.vigor + 1]
|
||||
local m = SummaryChrome.manifest()
|
||||
local deltas = m and m.eggPicShake and m.eggPicShake[b.vigor + 1]
|
||||
if not deltas or #deltas == 0 then return end
|
||||
local ready = b.delay >= EGG_SHAKE_DELAY[b.vigor]
|
||||
b.delay = b.delay + 1
|
||||
if not ready then return end
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local function stats(v)
|
||||
table.sort(v)
|
||||
return string.format("n=%d p99=%.3f max=%.3f", #v, v[math.max(1, math.ceil(#v * .99))] or 0, v[#v] or 0)
|
||||
end
|
||||
return function(game)
|
||||
for _ = 1, 900 do if game.boot then break end U.wait(1) end
|
||||
local Native = require("src.core.game3.tileset_native")
|
||||
local Ow = require("src.core.game3.ow_sprites")
|
||||
local Map = require("src.core.game3.map")
|
||||
local family = require("src.core.GameVersion").get()
|
||||
local records = {}
|
||||
for _, spec in ipairs({ { Native, "get", "pair" }, { Ow, "get", "sprite" }, { Map, "load", "map" } }) do
|
||||
local owner, key, label = unpack(spec)
|
||||
local fn = owner[key]
|
||||
owner[key] = function(...)
|
||||
local args = { ... }
|
||||
local t = love.timer.getTime()
|
||||
local a, b = fn(...)
|
||||
local ms = (love.timer.getTime() - t) * 1000
|
||||
if ms > 1 then print(string.format("ASSET_STALL %s id=%s ms=%.3f", label, tostring(args[label == "map" and 3 or 1]), ms)) end
|
||||
records[label] = records[label] or {}; records[label][#records[label] + 1] = ms
|
||||
return a, b
|
||||
end
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "STREAM", gender = 0 })
|
||||
U.wait(60)
|
||||
local map = family == "emerald" and "EM_ROUTE111" or "FR_CELADON_CITY"
|
||||
Map.load(nil, game, map, { x = 10, y = 10 })
|
||||
U.wait(120)
|
||||
local pairsToTest = {}
|
||||
for pair in pairs(Native._pairs) do pairsToTest[#pairsToTest + 1] = pair end
|
||||
table.sort(pairsToTest)
|
||||
local spritesToTest = {}
|
||||
for gid in pairs(Ow._loaded) do spritesToTest[#spritesToTest + 1] = gid end
|
||||
table.sort(spritesToTest)
|
||||
local base = Native._cache
|
||||
local cold = { read = function(_, rel) if rel:find("/atlas_", 1, true) then return nil end return base:read(rel) end,
|
||||
write = function() return true end, exists = function(_, rel) return base:exists(rel) end }
|
||||
local savedUpdate, savedDraw = game.update, game.draw
|
||||
game.update, game.draw = function() end, function() end
|
||||
if os.getenv("ASSET_COLD_WORKER") == "1" and Native.prefetch then
|
||||
assert(love.filesystem.getIdentity() == os.getenv("POKEPORT_IDENTITY"), "use a disposable driver identity")
|
||||
Native.install(base)
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
for _, pair in ipairs(pairsToTest) do
|
||||
local root = require("src.import.gba.extract_island1").NATIVE_ROOT
|
||||
local spec = assert(base:assetWorkerSpec(root, "pair", pair))
|
||||
assert(not spec.directory, "cold worker benchmark requires a disposable save-dir cache")
|
||||
local dir = spec.prefix .. root .. "/" .. pair
|
||||
for _, file in ipairs(love.filesystem.getDirectoryItems(dir)) do
|
||||
if file:match("^atlas_.*%.rgba$") then assert(love.filesystem.remove(dir .. "/" .. file)) end
|
||||
end
|
||||
Native.prefetch(pair)
|
||||
local deadline = love.timer.getTime() + 20
|
||||
while not Native._stream:decoded(pair) and love.timer.getTime() < deadline do Stream.poll(); U.wait(1) end
|
||||
assert(Native._stream:decoded(pair), pair)
|
||||
local data = Native._stream.pending[pair].data
|
||||
local reference = assert(require("src.core.game3.asset_decode").pair(base, root, pair))
|
||||
for i, layer in ipairs(data.layers) do
|
||||
assert(layer.data:getString() == reference.layers[i].data:getString(), "worker RGBA mismatch " .. pair)
|
||||
end
|
||||
print("PASS worker cold RGBA parity " .. pair)
|
||||
while not Native._pairs[pair] do Stream.frameComplete(); Stream.update(); U.wait(1) end
|
||||
end
|
||||
end
|
||||
for _, mode in ipairs({ "cold_rgba_cache", "disk_rgba_cache", "decoded", "gpu_warm" }) do
|
||||
local v = {}
|
||||
if mode ~= "gpu_warm" then Native.install(mode == "cold_rgba_cache" and cold or base) end
|
||||
for _, pair in ipairs(pairsToTest) do
|
||||
if mode == "decoded" and Native.prefetch then
|
||||
Native.prefetch(pair)
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local deadline = love.timer.getTime() + 20
|
||||
while not Native._stream:decoded(pair) and love.timer.getTime() < deadline do Stream.poll(); U.wait(1) end
|
||||
assert(Native._stream:decoded(pair), "worker failed to prepare " .. pair)
|
||||
end
|
||||
local t = love.timer.getTime(); assert(Native.get(pair), pair)
|
||||
local ms = (love.timer.getTime() - t) * 1000
|
||||
v[#v + 1] = ms
|
||||
print(string.format("ASSET_PAIR mode=%s id=%s ms=%.3f", mode, pair, ms))
|
||||
end
|
||||
print("ASSET_PROFILE " .. mode .. " " .. stats(v))
|
||||
end
|
||||
local spriteCache = Ow._cache
|
||||
for _, mode in ipairs({ "disk_rgba_cache", "decoded", "gpu_warm" }) do
|
||||
if mode ~= "gpu_warm" then Ow.install(spriteCache) end
|
||||
local v = {}
|
||||
for _, gid in ipairs(spritesToTest) do
|
||||
if mode == "decoded" and Ow.prefetch then
|
||||
Ow.prefetch(gid)
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local deadline = love.timer.getTime() + 20
|
||||
while not Ow._stream:decoded(gid) and love.timer.getTime() < deadline do Stream.poll(); U.wait(1) end
|
||||
assert(Ow._stream:decoded(gid), "worker failed to prepare sprite " .. gid)
|
||||
end
|
||||
local t = love.timer.getTime(); assert(Ow.get(gid), gid)
|
||||
local ms = (love.timer.getTime() - t) * 1000
|
||||
v[#v + 1] = ms
|
||||
print(string.format("ASSET_SPRITE mode=%s id=%s ms=%.3f", mode, gid, ms))
|
||||
end
|
||||
print("ASSET_PROFILE sprite_" .. mode .. " " .. stats(v))
|
||||
end
|
||||
game.update, game.draw = savedUpdate, savedDraw
|
||||
for label, v in pairs(records) do print("ASSET_PROFILE " .. label .. " " .. stats(v)) end
|
||||
print("PASS game3_asset_stream_profile")
|
||||
love.event.quit(0)
|
||||
end
|
||||
@@ -0,0 +1,130 @@
|
||||
local U = require("tests.drivers.util")
|
||||
return function(game)
|
||||
for _ = 1, 900 do if game.boot then break end U.wait(1) end
|
||||
game:_handleBootAction({ action = "new_game", name = "PREP", gender = 0 })
|
||||
U.wait(60)
|
||||
local Map = require("src.core.game3.map")
|
||||
local Player = require("src.core.game3.player")
|
||||
local View = require("src.core.game3.field_view")
|
||||
local Plan = require("src.core.game3.field_plan")
|
||||
local Cells = require("src.core.game3.field_cell_prepare")
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local Native = require("src.core.game3.tileset_native")
|
||||
local Void = require("src.core.game3.void_fill")
|
||||
local Prepare = require("src.core.game3.object_prepare")
|
||||
local version = require("src.core.GameVersion").get()
|
||||
local id = version == "emerald" and "EM_PETALBURG_CITY" or "FR_PALLET_TOWN"
|
||||
Map.load(nil, game, id, { x = 1, y = 1, facing = "down" }); U.wait(60)
|
||||
local baselinePath = os.getenv("POKEPORT_OBJECT_BASELINE") or "src/core/game3/objects.lua"
|
||||
local baseline = assert(love.filesystem.load(baselinePath))()
|
||||
local originalUpdate, originalDraw = game.update, game.draw
|
||||
game.update, game.draw = function() end, function() end
|
||||
local failures = 0
|
||||
local function check(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
end
|
||||
local function waitPrepared(test)
|
||||
local deadline = love.timer.getTime() + 5
|
||||
while not test() and love.timer.getTime() < deadline do Stream.poll(); U.wait(1) end
|
||||
return test()
|
||||
end
|
||||
local function stats(v)
|
||||
table.sort(v); return string.format("n=%d p99=%.3f max=%.3f", #v, v[math.ceil(#v * .99)] or 0, v[#v] or 0)
|
||||
end
|
||||
local def = game.data.maps[id]
|
||||
local times = { baseline = {}, worker_adopt = {}, snapshot = {} }
|
||||
for _ = 1, 20 do
|
||||
baseline._mapId, baseline._perm, baseline._templateMt = id, Objects._perm, Objects._templateMt
|
||||
local t = love.timer.getTime(); baseline.loadMap(game, id, def)
|
||||
times.baseline[#times.baseline + 1] = (love.timer.getTime() - t) * 1000
|
||||
t = love.timer.getTime(); Objects.prefetchMap(id, def, 0)
|
||||
times.snapshot[#times.snapshot + 1] = (love.timer.getTime() - t) * 1000
|
||||
local deadline = love.timer.getTime() + 5
|
||||
repeat
|
||||
Stream.poll(); U.wait(1)
|
||||
until not Objects._preparationPending(id) or love.timer.getTime() > deadline
|
||||
t = love.timer.getTime(); Objects.loadMap(game, id, def)
|
||||
times.worker_adopt[#times.worker_adopt + 1] = (love.timer.getTime() - t) * 1000
|
||||
check(Objects._lastPreparationRoute == "worker", "object reload used worker")
|
||||
local equal = Prepare.matches(Prepare.freeze(baseline._byId), Prepare.freeze(Objects._byId))
|
||||
if not equal and #times.baseline == 1 then
|
||||
local function differences(a, b, path)
|
||||
if type(a) ~= type(b) then print("OBJECT_DIFF " .. path .. " " .. tostring(a) .. " / " .. tostring(b)); return end
|
||||
if type(a) == "table" then
|
||||
for k, v in pairs(a) do differences(v, b[k], path .. "." .. tostring(k)) end
|
||||
for k, v in pairs(b) do if a[k] == nil then differences(nil, v, path .. "." .. tostring(k)) end end
|
||||
elseif a ~= b then print("OBJECT_DIFF " .. path .. " " .. tostring(a) .. " / " .. tostring(b)) end
|
||||
end
|
||||
differences(baseline._byId, Objects._byId, "objects")
|
||||
end
|
||||
check(equal, "worker object pool equals synchronous pool")
|
||||
end
|
||||
for phase, values in pairs(times) do print("OBJECT_PROFILE " .. phase .. " " .. stats(values)) end
|
||||
local savedGet = Plan.get
|
||||
local function render(w, h, forceSync)
|
||||
local canvas = love.graphics.newCanvas(w, h, { dpiscale = 1 })
|
||||
love.graphics.setCanvas(canvas); love.graphics.clear(0, 0, 0, 1)
|
||||
View._nativeVisibleCells = nil
|
||||
if forceSync then Plan.get = function() return nil end end
|
||||
local t = love.timer.getTime(); View.draw(game, w, h)
|
||||
local ms = (love.timer.getTime() - t) * 1000
|
||||
Plan.get = savedGet
|
||||
love.graphics.setCanvas()
|
||||
local image = canvas:newImageData()
|
||||
canvas:release()
|
||||
return image, ms
|
||||
end
|
||||
for _, size in ipairs({ { "fit", 240, 160 }, { "survey", 960, 640 } }) do
|
||||
for _, mode in ipairs(Void.MODES) do
|
||||
Void.setMode(mode)
|
||||
Player.reset(-2, -2, "down")
|
||||
render(size[2], size[3], true)
|
||||
Plan.prefetch(game)
|
||||
local cx = math.floor((Player.px + 8 - size[2] / 2) / 16) - 1
|
||||
local cy = math.floor((Player.py + 8 - size[3] / 2) / 16) - 1
|
||||
local cols, rows = math.ceil(size[2] / 16) + 3, math.ceil(size[3] / 16) + 3
|
||||
check(waitPrepared(function() return Plan.get(def, cx, cy, cols, rows, mode) ~= nil end), size[1] .. " " .. mode .. " cell worker ready")
|
||||
local p = Plan.get(def, cx, cy, cols, rows, mode)
|
||||
local matches = p ~= nil
|
||||
if p then
|
||||
for y = cy, cy + rows - 1 do for x = cx, cx + cols - 1 do
|
||||
local mid, pair, void = Map.worldMidAt(x, y, def)
|
||||
local skip = false
|
||||
if void and mode ~= "map" then
|
||||
local fill = Void.fillAt(mode, x, y, function(m) return Native.hasMid(def.midLayout.pair, m) end, Void.primaryFor(def.midLayout.pair))
|
||||
if fill == false then skip = true elseif fill then mid, pair = fill, def.midLayout.pair end
|
||||
end
|
||||
local actual, ap, av, as = Cells.cell(p, x, y)
|
||||
matches = matches and actual == mid and ap == pair and av == not not void and as == skip
|
||||
end end
|
||||
end
|
||||
check(matches, size[1] .. " " .. mode .. " all planned cells equal original resolver")
|
||||
local prepared, preparedMs = render(size[2], size[3], false)
|
||||
check(View._cellPreparationRoute == "worker", size[1] .. " " .. mode .. " draw consumes planned cells")
|
||||
local original, originalMs = render(size[2], size[3], true)
|
||||
check(prepared:getString() == original:getString(), size[1] .. " " .. mode .. " complete field pixels equal")
|
||||
local workerTimes, syncTimes = { preparedMs }, { originalMs }
|
||||
for i = 2, 20 do
|
||||
-- Alternate order and finish each readback before the next sample.
|
||||
local a, ams = render(size[2], size[3], i % 2 == 0)
|
||||
local b, bms = render(size[2], size[3], i % 2 ~= 0)
|
||||
if i % 2 == 0 then syncTimes[i], workerTimes[i] = ams, bms
|
||||
else workerTimes[i], syncTimes[i] = ams, bms end
|
||||
a:release(); b:release()
|
||||
end
|
||||
print(string.format("VOID_PROFILE %s %s worker_draw %s sync_draw %s", size[1], mode, stats(workerTimes), stats(syncTimes)))
|
||||
if os.getenv("POKEPORT_SHOT_DIR") then
|
||||
local bytes = prepared:encode("png")
|
||||
local f = assert(io.open(os.getenv("POKEPORT_SHOT_DIR") .. "/" .. size[1] .. "_" .. mode .. ".png", "wb"))
|
||||
f:write(bytes:getString()); f:close()
|
||||
end
|
||||
prepared:release(); original:release()
|
||||
end
|
||||
end
|
||||
Void.setMode("map"); Plan.invalidate()
|
||||
game.update, game.draw = originalUpdate, originalDraw
|
||||
print((failures == 0 and "PASS" or "FAIL") .. " game3_field_prepare_profile failures=" .. failures)
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,55 @@
|
||||
local run = require("tests.drivers.game3_seam_walk_stress")
|
||||
local function stats(v)
|
||||
table.sort(v)
|
||||
local sum = 0; for _, n in ipairs(v) do sum = sum + n end
|
||||
return string.format("n=%d mean=%.3f p99=%.3f max=%.3f", #v, sum / math.max(1, #v), v[math.ceil(#v * .99)] or 0, v[#v] or 0)
|
||||
end
|
||||
return function(game)
|
||||
local samples = { update = {}, draw = {}, seam = {}, pair = {}, sprite = {} }
|
||||
local function wrap(owner, key, label, onlySeams)
|
||||
local old = owner[key]
|
||||
if type(old) ~= "function" then return end
|
||||
samples[label] = samples[label] or {}
|
||||
owner[key] = function(...)
|
||||
local t = love.timer.getTime()
|
||||
local a, b, c = old(...)
|
||||
local ms = (love.timer.getTime() - t) * 1000
|
||||
local args = { ... }
|
||||
if not onlySeams or (args[4] and args[4].seamless) then
|
||||
local v = samples[label]; v[#v + 1] = ms
|
||||
if label == "objects" or label == "ghost_spawn" or label == "field_cells" then
|
||||
local route = (label == "field_cells" and owner._cellPreparationRoute) or owner._lastPreparationRoute or "sync"
|
||||
local name = label .. "_" .. route
|
||||
samples[name] = samples[name] or {}; samples[name][#samples[name] + 1] = ms
|
||||
if label == "objects" then print(string.format("OBJECT_RELOAD map=%s count=%s ms=%.3f route=%s", tostring(args[2]), tostring(a), ms, route)) end
|
||||
if label == "field_cells" then
|
||||
local id = require("src.core.game3.map").current
|
||||
if owner._profileMap ~= id then
|
||||
owner._profileMap = id
|
||||
local Plan = package.loaded["src.core.game3.field_plan"] or {}
|
||||
print(string.format("VOID_TRANSITION map=%s ms=%.3f route=%s fade=%s miss=%s graph=%s", tostring(id), ms, route,
|
||||
tostring(owner._voidFrom and owner._voidFrom.t), tostring(Plan._lastMiss), tostring(Plan._graphMiss)))
|
||||
end
|
||||
end
|
||||
end
|
||||
if (label == "pair" or label == "sprite") and ms > 1 then
|
||||
print(string.format("SEAM_ASSET_STALL %s id=%s ms=%.3f", label, tostring(args[1]), ms))
|
||||
end
|
||||
end
|
||||
return a, b, c
|
||||
end
|
||||
end
|
||||
wrap(game, "update", "update")
|
||||
wrap(game, "draw", "draw")
|
||||
wrap(require("src.core.game3.map"), "load", "seam", true)
|
||||
wrap(require("src.core.game3.tileset_native"), "get", "pair")
|
||||
wrap(require("src.core.game3.ow_sprites"), "get", "sprite")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
wrap(Objects, "loadMap", "objects")
|
||||
wrap(Objects, "spawnFromDefs", "ghost_spawn")
|
||||
local hasPlan, Plan = pcall(require, "src.core.game3.field_plan")
|
||||
if hasPlan then wrap(Plan, "prefetch", "plan_snapshot") end
|
||||
wrap(require("src.core.game3.field_view"), "draw", "field_cells")
|
||||
run(game)
|
||||
for label, v in pairs(samples) do print("SEAM_PROFILE " .. label .. " " .. stats(v)) end
|
||||
end
|
||||
@@ -335,6 +335,9 @@ return function(game)
|
||||
patch(Field, "tryWalkIntoSign", function() return false end)
|
||||
patch(TrainerSight, "check", function() return false end)
|
||||
patch(ForcedMovement, "onStepFinished", function() return false end)
|
||||
-- Keep Cycling Road's automatic downhill steps from restarting movement
|
||||
-- during stepOnce's settling window, like the other forced paths above.
|
||||
patch(Player, "cyclingRoadPull", function() return false end)
|
||||
patch(StepEvents, "onStepTaken", function() return false end)
|
||||
patch(Encounters, "onStep", function() return nil end)
|
||||
patch(Encounters, "noteGrass", function() return nil end)
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local function stats(v)
|
||||
table.sort(v)
|
||||
local sum = 0; for _, n in ipairs(v) do sum = sum + n end
|
||||
return string.format("n=%d mean=%.3f p99=%.3f max=%.3f", #v, sum / #v, v[math.ceil(#v * .99)], v[#v])
|
||||
end
|
||||
return function(game)
|
||||
for _ = 1, 900 do if game.boot then break end U.wait(1) end
|
||||
game:_handleBootAction({ action = "new_game", name = "WEATHER", gender = 0 })
|
||||
U.wait(60)
|
||||
local Map = require("src.core.game3.map")
|
||||
Map.load(nil, game, "EM_PETALBURG_CITY", { x = 17, y = 13 })
|
||||
U.wait(90)
|
||||
local R = require("src.core.game3.field_weather_rse")
|
||||
local baselinePath = os.getenv("WEATHER_BASELINE")
|
||||
local baseline = baselinePath and assert(love.filesystem.load(baselinePath))() or nil
|
||||
local Field = require("src.core.game3.field_view")
|
||||
local Renderer = require("src.render.Renderer")
|
||||
local Rng = require("src.core.game3.rng")
|
||||
local Oam = require("src.core.game3.oam")
|
||||
local failures = 0
|
||||
local function check(ok, label) print((ok and "PASS " or "FAIL ") .. label); if not ok then failures = failures + 1 end end
|
||||
local saveUpdate, saveDraw = game.update, game.draw
|
||||
game.update, game.draw = function() end, function() end
|
||||
local function weather(engine, id, frames)
|
||||
engine.restart(); Rng._value = 42
|
||||
engine.setCurrentAndNextWeatherNoDelay(id)
|
||||
for _ = 1, frames do engine.update() end
|
||||
end
|
||||
local function render(engine, w, h, swap)
|
||||
package.loaded["src.core.game3.field_weather_rse"] = engine
|
||||
Oam.resetFrame()
|
||||
local canvas = Renderer.worldCanvas
|
||||
if not canvas or canvas:getWidth() ~= w or canvas:getHeight() ~= h then
|
||||
canvas = love.graphics.newCanvas(w, h, { dpiscale = 1 })
|
||||
end
|
||||
Renderer.worldCanvas, Renderer.worldActive = canvas, true
|
||||
love.graphics.setCanvas(canvas); love.graphics.clear(.2, .3, .4, 1)
|
||||
local switches, oldSet = 0, love.graphics.setCanvas
|
||||
love.graphics.setCanvas = function(...) switches = switches + 1; return oldSet(...) end
|
||||
local t = love.timer.getTime()
|
||||
Field.draw(game, w, h, { exchangeCanvas = swap and function(cur, next) return Renderer:exchangeWorldCanvas(cur, next) end or nil })
|
||||
love.graphics.setCanvas = oldSet
|
||||
local cpu = (love.timer.getTime() - t) * 1000
|
||||
oldSet()
|
||||
local image = Renderer.worldCanvas:newImageData()
|
||||
local sync = (love.timer.getTime() - t) * 1000
|
||||
return image, cpu, sync, switches
|
||||
end
|
||||
for _, size in ipairs({ { "fit", 240, 160 }, { "survey", 960, 640 } }) do
|
||||
for _, spec in ipairs({ { "clear", 2, 180 }, { "shade", 11, 180 }, { "rain", 3, 180 },
|
||||
{ "drought", 12, 180 }, { "fog", 6, 180 }, { "sandstorm", 8, 180 }, { "sandstorm_blend", 8, 2 } }) do
|
||||
weather(R, spec[2], spec[3])
|
||||
local copy = render(R, size[2], size[3], false)
|
||||
local swapped = render(R, size[2], size[3], true)
|
||||
check(copy:getString() == swapped:getString(), size[1] .. " " .. spec[1] .. " copy vs exchange pixels")
|
||||
if baseline then
|
||||
weather(baseline, spec[2], spec[3])
|
||||
render(baseline, size[2], size[3], false)
|
||||
local original = render(baseline, size[2], size[3], false)
|
||||
if original:getString() ~= swapped:getString() then
|
||||
local a, b, count, max = original:getString(), swapped:getString(), 0, 0
|
||||
for i = 1, #a do local d = math.abs(a:byte(i) - b:byte(i)); if d > 0 then count = count + 1; max = math.max(max, d) end end
|
||||
print(string.format("WEATHER_DIFF %s %s channels=%d max=%d eva=%d evb=%d", size[1], spec[1], count, max, R.state.currBlendEVA, R.state.currBlendEVB))
|
||||
end
|
||||
check(original:getString() == swapped:getString(), size[1] .. " " .. spec[1] .. " original vs batched exchange pixels")
|
||||
end
|
||||
for _, mode in ipairs({ { "copy", baseline or R, false }, { "exchange", R, true } }) do
|
||||
weather(mode[2], spec[2], spec[3])
|
||||
local cpus, synced, switches = {}, {}, {}
|
||||
for _ = 1, 60 do
|
||||
local _, cpu, sync, count = render(mode[2], size[2], size[3], mode[3])
|
||||
cpus[#cpus + 1], synced[#synced + 1], switches[#switches + 1] = cpu, sync, count
|
||||
end
|
||||
print(string.format("WEATHER_PROFILE %s %s %s cpu %s completion_with_readback %s switches=%d", size[1], spec[1], mode[1],
|
||||
stats(cpus), stats(synced), switches[#switches]))
|
||||
end
|
||||
if os.getenv("POKEPORT_SHOT_DIR") then
|
||||
swapped:encode("png", "weather-preview.png")
|
||||
local bytes = love.filesystem.read("weather-preview.png")
|
||||
local f = assert(io.open(os.getenv("POKEPORT_SHOT_DIR") .. "/" .. size[1] .. "_" .. spec[1] .. ".png", "wb")); f:write(bytes); f:close()
|
||||
love.filesystem.remove("weather-preview.png")
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Exercise Display's actual target exchange, upright actors, and UI planes.
|
||||
local Zoom, Tilt = require("src.render.Zoom"), require("src.render.Tilt")
|
||||
local uprightCalls, beginUpright = 0, Renderer.beginUprightPass
|
||||
Renderer.beginUprightPass = function(self, ...)
|
||||
uprightCalls = uprightCalls + 1
|
||||
return beginUpright(self, ...)
|
||||
end
|
||||
Zoom.allowSurvey = true
|
||||
if baseline then
|
||||
for _, zoom in ipairs({ 0, Zoom.offsetRange(Renderer:fitScale()) }) do
|
||||
game.options.zoom = zoom; Zoom.offset = zoom
|
||||
for _, tilt in ipairs({ 0, 2 }) do
|
||||
game.options.tilt = tilt; Tilt.setLevel(tilt); Tilt.update(1)
|
||||
for _, id in ipairs({ 2, 3, 6, 11, 12, 8 }) do
|
||||
local function planes(engine)
|
||||
package.loaded["src.core.game3.field_weather_rse"] = engine
|
||||
weather(engine, id, 180)
|
||||
local before = uprightCalls
|
||||
saveDraw(game); saveDraw(game)
|
||||
if tilt > 0 then check(uprightCalls == before + 2, "upright pass rendered") end
|
||||
local out = {}
|
||||
for _, key in ipairs({ "worldCanvas", "uprightCanvas", "canvas" }) do
|
||||
local target = Renderer[key]
|
||||
if target then out[key] = target:newImageData():getString() end
|
||||
end
|
||||
return out
|
||||
end
|
||||
local a, b = planes(baseline), planes(R)
|
||||
for key, bytes in pairs(a) do
|
||||
check(bytes == b[key], string.format("display zoom=%d tilt=%d weather=%d %s pixels", zoom, tilt, id, key))
|
||||
end
|
||||
end
|
||||
if os.getenv("POKEPORT_SHOT_DIR") then
|
||||
game.draw = saveDraw
|
||||
U.still(game, os.getenv("POKEPORT_SHOT_DIR") .. string.format("/display_zoom%d_tilt%d.png", zoom, tilt))
|
||||
game.draw = function() end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Renderer.beginUprightPass = beginUpright
|
||||
package.loaded["src.core.game3.field_weather_rse"] = R
|
||||
game.update, game.draw = saveUpdate, saveDraw
|
||||
love.graphics.setCanvas()
|
||||
print((failures == 0 and "PASS" or "FAIL") .. " game3_weather_compositor_profile failures=" .. failures)
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,104 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
love = require("tests.love_stub")
|
||||
local channels, worker = {}, {}
|
||||
love.thread = {
|
||||
newChannel = function()
|
||||
local ch = { q = {} }
|
||||
function ch:push(v) self.q[#self.q + 1] = v end
|
||||
function ch:pop() return table.remove(self.q, 1) end
|
||||
function ch:clear() self.q = {} end
|
||||
channels[#channels + 1] = ch
|
||||
return ch
|
||||
end,
|
||||
newThread = function()
|
||||
function worker:start(input, output) self.input, self.output = input, output end
|
||||
function worker:getError() return self.error end
|
||||
function worker:wait() self.joined = true end
|
||||
return worker
|
||||
end,
|
||||
}
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local published, uploads = {}, 0
|
||||
local cache = { assetWorkerSpec = function() return { prefix = "emerald/" } end }
|
||||
local function owner()
|
||||
return Stream.new("pair", cache, "ROOT", function(data)
|
||||
for _ = 1, data.layers do uploads = uploads + 1; coroutine.yield("texture") end
|
||||
return data
|
||||
end, function(key, data) published[key] = data end)
|
||||
end
|
||||
local s = owner()
|
||||
s:prefetch("A"); s:prefetch("A"); Stream.poll()
|
||||
eq(#worker.input.q, 1, "duplicate requests are coalesced")
|
||||
local request = worker.input:pop()
|
||||
eq(request.spec.prefix, "emerald/", "worker receives an immutable cartridge namespace")
|
||||
eq(uploads, 0, "queuing CPU work does not touch graphics")
|
||||
worker.output:push({ id = request.id, data = { layers = 3 }, seconds = .01 })
|
||||
Stream.update()
|
||||
eq(uploads, 1, "only one texture layer uploads in an update")
|
||||
Stream.update(); eq(uploads, 1, "fixed-step catchup cannot upload another layer in the same render frame")
|
||||
Stream.frameComplete(); Stream.update(); eq(uploads, 2, "next render frame admits the next layer")
|
||||
check(published.A == nil, "partially uploaded atlases are never published")
|
||||
local result = s:get("A")
|
||||
eq(uploads, 3, "unexpected demand finishes the existing partial upload without duplicating textures")
|
||||
check(published.A == result and result.layers == 3, "fallback publishes the complete atlas")
|
||||
|
||||
s:prefetch("STALE"); Stream.poll(); request = worker.input:pop()
|
||||
s:cancel()
|
||||
eq(#request.cancelSignal.q, 1, "cache reset signals active CPU preparation to stop")
|
||||
worker.output:push({ id = request.id, data = { layers = 1 }, seconds = .01 })
|
||||
Stream.frameComplete(); Stream.update()
|
||||
check(published.STALE == nil, "cache reset rejects a late result")
|
||||
eq(uploads, 3, "stale data does not allocate a texture")
|
||||
local nextOwner = owner()
|
||||
nextOwner:prefetch("OUTSIDE"); Stream.poll(); request = worker.input:pop()
|
||||
nextOwner:retain({})
|
||||
eq(#request.cancelSignal.q, 1, "leaving the neighborhood interrupts active preparation")
|
||||
worker.output:push({ id = request.id, data = { layers = 1 }, seconds = .01 })
|
||||
Stream.frameComplete(); Stream.update()
|
||||
check(published.OUTSIDE == nil, "leaving the warm neighborhood cancels queued work")
|
||||
for i = 1, 100 do nextOwner:prefetch("LIMIT" .. i) end
|
||||
local count = 0; for _ in pairs(nextOwner.pending) do count = count + 1 end
|
||||
eq(count, Stream.MAX_PENDING, "pending assets have a residency bound")
|
||||
Stream.cancelPending()
|
||||
check(next(nextOwner.pending) == nil, "field stop cancels all pending requests")
|
||||
Stream.shutdown()
|
||||
check(worker.joined, "process teardown joins the worker before filesystem teardown")
|
||||
local priorityOwner = owner()
|
||||
priorityOwner:prefetch("NEAR", 1)
|
||||
priorityOwner:prefetch("VISIBLE", 0)
|
||||
Stream.poll(); request = worker.input:pop()
|
||||
eq(request.key, "VISIBLE", "visible assets decode before nearby assets")
|
||||
worker.output:push({ id = request.id, data = { layers = 1 }, seconds = .01 })
|
||||
Stream.poll(); request = worker.input:pop()
|
||||
eq(request.key, "NEAR", "nearby preparation follows visible preparation")
|
||||
worker.output:push({ id = request.id, data = { layers = 1 }, seconds = .01 })
|
||||
Stream.poll()
|
||||
Stream.frameComplete(); Stream.update()
|
||||
eq(coroutine.status(priorityOwner.pending.VISIBLE.co), "suspended", "visible decoded assets upload first")
|
||||
check(priorityOwner.pending.NEAR.co == nil, "nearby uploads wait for the visible upload slice")
|
||||
Stream.shutdown()
|
||||
local cpuResult
|
||||
local cpuTask = Stream.newTask("objects", function(key, data) cpuResult = { key, data } end)
|
||||
cpuTask:submit("MAP", { defs = { { localId = 1 } } }, 0)
|
||||
Stream.poll(); request = worker.input:pop()
|
||||
check(request.spec.task and request.spec.payload.defs[1].localId == 1, "CPU tasks receive their immutable input")
|
||||
Stream._uploadedThisFrame = true
|
||||
worker.output:push({ id = request.id, data = { prepared = true }, seconds = .01 })
|
||||
Stream.poll()
|
||||
check(cpuResult[1] == "MAP" and cpuResult[2].prepared, "CPU results publish without a graphics slice")
|
||||
check(cpuTask.pending.MAP == nil, "CPU publication releases its queued snapshot")
|
||||
Stream.shutdown()
|
||||
local fullOwner = owner()
|
||||
for i = 1, Stream.MAX_DECODED do
|
||||
fullOwner:prefetch("FULL" .. i); Stream.poll(); request = worker.input:pop()
|
||||
worker.output:push({ id = request.id, data = { layers = 1 }, seconds = .01 }); Stream.poll()
|
||||
end
|
||||
fullOwner:prefetch("BLOCKED", 0)
|
||||
cpuTask = Stream.newTask("cells", function() end)
|
||||
cpuTask:submit("CELL_WINDOW", {}, 1); Stream.poll(); request = worker.input:pop()
|
||||
eq(request.key, "CELL_WINDOW", "CPU field preparation proceeds while decoded graphics residency is full")
|
||||
check(not fullOwner.pending.BLOCKED.sent, "graphics decoding still obeys the residency bound")
|
||||
Stream.shutdown()
|
||||
T.finish("game3_asset_stream_test")
|
||||
@@ -0,0 +1,53 @@
|
||||
-- pokeemerald/src/scrcmd.c:1599 bufferdecorationname copies gDecorations[id].name.
|
||||
-- Lanette's givedecoration DECOR_LOTAD_DOLL (99) was printing "99" because
|
||||
-- bufferName never asked the decoration cache.
|
||||
-- luajit tests/engine/game3_bufferdecorationname_test.lua
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local eq = T.eq
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local DecorInv = require("src.core.game3.rse.decoration_inventory")
|
||||
local Adapters = require("src.core.game3.scripting.adapters")
|
||||
local Vm = require("src.core.game3.scripting.vm")
|
||||
local Ops = require("src.core.game3.scripting.ops_a")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
|
||||
DecorInv.install({
|
||||
decorations = {
|
||||
[99] = { id = 99, name = "LOTAD DOLL", category = 6 },
|
||||
},
|
||||
})
|
||||
|
||||
local stub = Adapters.stub({})
|
||||
local host = Adapters.host(nil, nil, nil)
|
||||
|
||||
eq(stub.bufferName("bufferdecorationname", 99), "LOTAD DOLL", "stub names decoration 99")
|
||||
eq(host.bufferName("bufferdecorationname", 99), "LOTAD DOLL", "host names decoration 99")
|
||||
eq(stub.bufferName("bufferdecorationname", 1), nil, "unknown decoration stays unresolved")
|
||||
|
||||
local vm = Vm.new({
|
||||
store = Flags.newStore(),
|
||||
scripts = { t = { { op = "end" } } },
|
||||
adapters = stub,
|
||||
})
|
||||
vm:start("t")
|
||||
Ops.dispatch(vm, { op = "setvar", [1] = 0x8000, [2] = 99 })
|
||||
Ops.dispatch(vm, { op = "bufferdecorationname", dest = 1, src = 0x8000 })
|
||||
eq(vm.ctx.stringVars[2], "LOTAD DOLL", "VAR_0x8000 decoration name fills STR_VAR_2")
|
||||
|
||||
local home = os.getenv("HOME")
|
||||
local cache = home and (home .. "/Library/Application Support/LOVE/pokemon-love2d/emerald/data/generated/gba/decorations/decorations.lua")
|
||||
local f = cache and io.open(cache, "rb")
|
||||
if f then
|
||||
local src = f:read("*a")
|
||||
f:close()
|
||||
DecorInv.reset()
|
||||
local pack = assert(load(src, "@decorations.lua", "t", {}))()
|
||||
DecorInv.install(pack)
|
||||
eq(stub.bufferName("bufferdecorationname", 99), "LOTAD DOLL", "emerald cache decoration 99 is LOTAD DOLL")
|
||||
end
|
||||
|
||||
T.finish("game3_bufferdecorationname_test")
|
||||
@@ -0,0 +1,205 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
love = require("tests.love_stub")
|
||||
local worker = {}
|
||||
love.thread = {
|
||||
newChannel = function()
|
||||
local ch = { q = {} }
|
||||
function ch:push(v) self.q[#self.q + 1] = v end
|
||||
function ch:pop() return table.remove(self.q, 1) end
|
||||
function ch:clear() self.q = {} end
|
||||
function ch:getCount() return #self.q end
|
||||
return ch
|
||||
end,
|
||||
newThread = function()
|
||||
function worker:start(input, output) self.input, self.output = input, output end
|
||||
function worker:getError() return nil end
|
||||
function worker:wait() end
|
||||
return worker
|
||||
end,
|
||||
}
|
||||
require("src.core.GameVersion").set("firered")
|
||||
local Stream = require("src.core.game3.asset_stream")
|
||||
local ObjectPrepare = require("src.core.game3.object_prepare")
|
||||
local Cells = require("src.core.game3.field_cell_prepare")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local state = { gid = 17, hidden = false }
|
||||
package.loaded["src.core.game3.scripting.space"] = {
|
||||
resolveObjectGraphicsId = function(def) return def.graphicsId == 240 and state.gid or def.graphicsId end,
|
||||
objectVisible = function(def) return not (def.flag == 1 and state.hidden) end,
|
||||
}
|
||||
package.loaded["src.core.game3.collision"] = { elevationAt = function() return 3 end }
|
||||
local function finishJobs()
|
||||
for _ = 1, 20 do
|
||||
Stream.poll()
|
||||
local req = worker.input and worker.input:pop()
|
||||
if not req then break end
|
||||
local data = req.kind == "objects" and ObjectPrepare.objects(req.spec.payload) or Cells.cells(req.spec.payload)
|
||||
worker.output:push({ id = req.id, data = data, seconds = .001 })
|
||||
Stream.poll()
|
||||
end
|
||||
end
|
||||
local def = { objects = {
|
||||
{ localId = 1, x = 3, y = 4, graphicsId = 240, movementType = 2, flag = 1 },
|
||||
{ localId = 2, x = 5, y = 6, graphicsId = 16, movementType = 7 },
|
||||
} }
|
||||
Objects.prefetchMap("WORKER_MAP", def, 0); finishJobs()
|
||||
state.gid, state.hidden = 25, true
|
||||
Objects.loadMap({}, "WORKER_MAP", def)
|
||||
eq(Objects._lastPreparationRoute, "worker", "map adopts worker-built instances")
|
||||
local eo = Objects._byId[1]
|
||||
eq(eo.graphicsId, 25, "variable graphics resolve against live state after preparation")
|
||||
check(eo.hidden and not eo.visible, "hide flags resolve against live state after preparation")
|
||||
eq(eo.elevation, 3, "elevation binds against the authoritative collision grid")
|
||||
check(eo.def ~= def.objects[1], "prepared instances own their template copy")
|
||||
local workerEo = ObjectPrepare.freeze(eo)
|
||||
check(not pcall(ObjectPrepare.objects, { defs = def.objects }, function() return true end), "stale object preparation stops cooperatively")
|
||||
Objects.loadMap({}, "WORKER_MAP", def)
|
||||
check(ObjectPrepare.matches(workerEo, Objects._byId[1]), "worker and synchronous instance fields agree")
|
||||
Objects.prefetchMap("WORKER_MAP", def); finishJobs()
|
||||
def.objects[1].x = 9
|
||||
Objects.loadMap({}, "WORKER_MAP", def)
|
||||
eq(Objects._lastPreparationRoute, "sync", "changed definitions reject stale instances")
|
||||
eq(Objects._byId[1].cellX, 9, "changed template position is applied immediately")
|
||||
check(ObjectPrepare.freeze({ callback = function() end }) == nil, "custom functions keep the synchronous path")
|
||||
Objects.prefetchMap("STALE_MAP", def); Stream.poll()
|
||||
Objects.reset(); finishJobs()
|
||||
Objects.loadMap({}, "STALE_MAP", def)
|
||||
eq(Objects._lastPreparationRoute, "sync", "reset discards late object preparation")
|
||||
|
||||
local Layout = require("src.core.game3.layout_native")
|
||||
local Pack = require("src.import.gba.native_pack")
|
||||
local decoded = { width = 4, height = 4, trueWidth = 3, trueHeight = 3, borderWidth = 2, borderHeight = 2,
|
||||
borderMids = { 11, 12, 13, 14 }, cells = {} }
|
||||
for i = 1, 16 do decoded.cells[i] = { mid = i, coll = 0, elev = 0 } end
|
||||
local blob = Pack.encodeMidLayout(decoded)
|
||||
local layout = Layout.fromDecoded(decoded, "PLAN", "general__test", blob)
|
||||
layout:applyOverride(1, 1, 90, 0, 3)
|
||||
local p = layout:workerPacked()
|
||||
check(p ~= nil, "native grid snapshots use immutable packed bytes")
|
||||
local s = { x0 = -5, y0 = -5, cols = 15, rows = 15, mode = "map", neighbors = {}, world = {},
|
||||
layouts = { { x0 = -5, y0 = -5, w = 15, h = 15, width = 4, height = 4, pair = layout.pair, packed = p } } }
|
||||
local plan = Cells.cells(s)
|
||||
local equal = true
|
||||
for y = -5, 9 do for x = -5, 9 do
|
||||
local mid, pair, void, skip = Cells.cell(plan, x, y)
|
||||
equal = equal and mid == layout:midAt(x, y) and pair == layout.pair
|
||||
and void == (x < 0 or y < 0 or x >= 4 or y >= 4) and not skip
|
||||
end end
|
||||
check(equal, "packed worker sampling matches borders padding and metatile overrides")
|
||||
s.mode = "black"; plan = Cells.cells(s)
|
||||
local _, _, void, skip = Cells.cell(plan, -2, -3)
|
||||
check(void and skip, "black void skips only off-map cells")
|
||||
s.mode, s.fill = "trees", { w = 2, h = 2, mids = { 40, 41, 42, 43 } }
|
||||
plan = Cells.cells(s)
|
||||
eq(Cells.cell(plan, -1, -1), 43, "patterned void wraps negative coordinates identically")
|
||||
check(not pcall(Cells.cells, s, function() return true end), "stale cell preparation stops cooperatively")
|
||||
local Assets = require("src.render.Assets")
|
||||
Assets.loader = { overrideOrder = function() return { {} } end }
|
||||
check(layout:workerPacked() == nil, "modded layouts use sampled snapshots")
|
||||
Assets.loader = nil
|
||||
|
||||
local Map = require("src.core.game3.map")
|
||||
local Plan = require("src.core.game3.field_plan")
|
||||
local mapDef = { pair = layout.pair, midLayout = layout, connections = {} }
|
||||
local game = { data = { maps = { PLAN = mapDef } } }
|
||||
Map.current, Map._worldRoot, Map._def, Map.neighborList = "PLAN", "PLAN", mapDef, {}
|
||||
Map.world = { { id = "PLAN", def = mapDef, ox = 0, oy = 0 } }
|
||||
package.loaded["src.core.game3.field_view"] = { _viewW = 64, _viewH = 64 }
|
||||
package.loaded["src.core.game3.player"] = { px = 16, py = 16, cellX = 1, cellY = 1 }
|
||||
package.loaded["src.core.game3.tileset_native"] = { hasMid = function() return true end }
|
||||
require("src.core.game3.void_fill").mode = "map"
|
||||
Plan.prefetch(game); finishJobs()
|
||||
check(Plan.get(mapDef, -2, -2, 7, 7, "map") ~= nil, "completed cell windows are consumed without waiting")
|
||||
local oldBorder = layout.borderMids[1]
|
||||
layout.borderMids[1] = 99
|
||||
check(Plan.get(mapDef, -2, -2, 7, 7, "map") == nil, "border edits reject stale cell windows")
|
||||
layout.borderMids[1] = oldBorder
|
||||
layout:applyOverride(2, 2, 91)
|
||||
check(Plan.get(mapDef, -2, -2, 7, 7, "map") == nil, "layout edits reject stale cell windows")
|
||||
Plan.prefetch(game); finishJobs()
|
||||
Map.world[2] = { id = "OTHER", def = mapDef, ox = 4, oy = 0 }
|
||||
check(Plan.get(mapDef, -2, -2, 7, 7, "map") == nil, "world topology changes reject stale cell windows")
|
||||
Map.world[2] = nil
|
||||
Plan.invalidate()
|
||||
check(Plan.get(mapDef, -2, -2, 7, 7, "map") == nil, "context reset clears prepared cell windows")
|
||||
|
||||
local nextDef = { pair = layout.pair, midLayout = Layout.fromDecoded(decoded, "NEXT", layout.pair, blob),
|
||||
connections = { { direction = "west", map = "PLAN", offset = 0 } } }
|
||||
mapDef.connections = { { direction = "east", map = "NEXT", offset = 0 } }
|
||||
game.data.maps.NEXT = nextDef
|
||||
Map.neighborList = { { dir = "east", offset = 0, def = nextDef } }
|
||||
Map.world = Map.computeWorld(game.data.maps, "PLAN", 2, 4, 4)
|
||||
Plan.prefetch(game); finishJobs()
|
||||
Map.current, Map._def, Map._worldRoot = "NEXT", nextDef, nil
|
||||
Map.neighborList = { { dir = "west", offset = 0, def = mapDef } }
|
||||
local player = package.loaded["src.core.game3.player"]
|
||||
player.px, player.cellX = -48, -3
|
||||
-- The old world graph survives Map.load until the first draw refreshes it.
|
||||
Plan.prefetch(game)
|
||||
Map.world, Map._worldRoot = Map.computeWorld(game.data.maps, "NEXT", 2, 4, 4), "NEXT"
|
||||
check(Plan.get(nextDef, -6, -2, 7, 7, "map") ~= nil, "seam forecast survives stale world between load and first draw")
|
||||
local Ghosts = require("src.core.game3.ghosts")
|
||||
Ghosts.clear()
|
||||
player.px, player.cellX = 16, 1
|
||||
local ghostDef = { midLayout = layout, objects = { { localId = 1, x = 2, y = 2, graphicsId = 17 } } }
|
||||
Map.world = { { id = "WORKER_GHOST", def = ghostDef, ox = 5, oy = 0 } }
|
||||
Ghosts.sync()
|
||||
check(Ghosts._pools.WORKER_GHOST == nil and Objects._preparationPending("WORKER_GHOST"), "offscreen nearby ghosts wait for worker preparation")
|
||||
finishJobs(); Ghosts.sync()
|
||||
check(Ghosts._pools.WORKER_GHOST ~= nil and Objects._lastPreparationRoute == "worker", "completed ghost preparation adopts on main thread")
|
||||
Map.world = { { id = "COLLISION_GHOST", def = ghostDef, ox = 5, oy = 0 } }
|
||||
Ghosts.sync()
|
||||
check(Ghosts.blocksOn("COLLISION_GHOST", ghostDef, 2, 2), "collision remains authoritative while ghost work is pending")
|
||||
Map.world = { { id = "FAR_GHOST", def = ghostDef, ox = 100, oy = 0 } }
|
||||
Ghosts.sync()
|
||||
check(Ghosts._pools.FAR_GHOST == nil and not Objects._preparationPending("FAR_GHOST"), "far ghosts do not preload indiscriminately")
|
||||
Stream.workerFailed = true; Ghosts.sync()
|
||||
check(Ghosts._pools.FAR_GHOST ~= nil, "unavailable worker retains synchronous ghost fallback")
|
||||
Stream.workerFailed = false
|
||||
Plan.invalidate()
|
||||
Map.current, Map._worldRoot, Map._def = "PLAN", "PLAN", mapDef
|
||||
Map.world = Map.computeWorld(game.data.maps, "PLAN", 2, 4, 4)
|
||||
Map.neighborList = { { dir = "east", offset = 0, def = nextDef } }
|
||||
local Native = package.loaded["src.core.game3.tileset_native"]
|
||||
local Void = require("src.core.game3.void_fill")
|
||||
local calls, savedBorder = 0, Void.borderFor
|
||||
Void.mode, Void.borderFor = "trees", function() return { w = 1, h = 1, mids = { 40 } } end
|
||||
Native.hasMid = function(atlas) calls = calls + 1; return type(atlas) == "table" end
|
||||
Plan.prefetch(game); finishJobs()
|
||||
check(calls == 0, "forecasting a cold patterned pair never demand-loads a graphics atlas")
|
||||
Native._pairs = { [layout.pair] = {} }
|
||||
Plan.prefetch(game); finishJobs()
|
||||
local trees = Plan.get(mapDef, -2, -2, 7, 7, "trees")
|
||||
check(trees and Plan.cell(trees, -2, -2) == 40 and calls > 0, "pattern validation consumes the warmed atlas without bypassing upload budgets")
|
||||
Void.mode, Void.borderFor = "map", savedBorder
|
||||
Plan.invalidate()
|
||||
local shared = { width = 4, height = 4, midAt = function() return 77 end }
|
||||
local a, b = { midLayout = shared, pair = "pair_a" }, { midLayout = shared, pair = "pair_b" }
|
||||
game.data.maps.A, game.data.maps.B = a, b
|
||||
mapDef.connections = { { dir = "east", map = "A", offset = 0 }, { dir = "east", map = "B", offset = 0 } }
|
||||
Map.world = Map.computeWorld(game.data.maps, "PLAN", 2, 4, 4)
|
||||
Map.neighborList = { { dir = "east", offset = 0, def = a }, { dir = "east", offset = 0, def = b } }
|
||||
Plan.prefetch(game); finishJobs()
|
||||
local aliases = Plan.get(mapDef, -2, -2, 7, 7, "map")
|
||||
local mid, pair
|
||||
if aliases then mid, pair = Plan.cell(aliases, 4, 0) end
|
||||
check(mid == 77 and pair == "pair_b", "shared mod layouts retain each map's pair and reverse connection priority")
|
||||
Stream.shutdown()
|
||||
-- Exercise the production worker's result envelope, including a cancelled
|
||||
-- job, rather than just the Channel transport double above.
|
||||
for _, name in ipairs({ "filesystem", "image", "data", "timer" }) do package.loaded["love." .. name] = {} end
|
||||
local workerInput, workerOutput = love.thread.newChannel(), love.thread.newChannel()
|
||||
function workerInput:demand() return self:pop() end
|
||||
workerInput:push({ id = 1, kind = "objects", spec = { task = true,
|
||||
payload = { defs = { { localId = 1 } }, version = "firered" } }, cancelSignal = { getCount = function() return 0 end } })
|
||||
workerInput:push({ id = 2, kind = "objects", spec = { task = true,
|
||||
payload = { defs = { { localId = 1 } }, version = "firered" } }, cancelSignal = { getCount = function() return 1 end } })
|
||||
workerInput:push({ stop = true })
|
||||
assert(loadfile("src/core/game3/asset_worker.lua"))(workerInput, workerOutput)
|
||||
local success, cancelled = workerOutput:pop(), workerOutput:pop()
|
||||
check(success and success.data and success.data[1].localId == 1, "production worker returns prepared objects")
|
||||
check(success and success.error == nil, "successful production worker replies have no error")
|
||||
check(cancelled and cancelled.data == nil and cancelled.error:find("cancelled"), "cancelled production worker replies return an error without data")
|
||||
T.finish("game3_field_prepare_worker_test")
|
||||
@@ -390,6 +390,16 @@ draw()
|
||||
eq(drawn, 60 - 32 + 2 + 64, "the egg pic draws h-flipped at its shaken x")
|
||||
SummaryMenu.close()
|
||||
Pokemon.frontPic = realFront
|
||||
|
||||
print("[test] 13. emerald chrome has no egg shake table")
|
||||
for _, manifest in ipairs({ nil, {}, { eggPicShake = {} } }) do
|
||||
MANIFEST = manifest
|
||||
SummaryMenu.openMenu({ { species = 1, isEgg = true, friendship = 5 } }, 1, {})
|
||||
frames(200)
|
||||
eq(SummaryMenu._bounce.dx, 0, "missing eggPicShake leaves the egg still")
|
||||
eq(SummaryMenu._bounce.egg, true, "the summary still treats the mon as an egg")
|
||||
SummaryMenu.close()
|
||||
end
|
||||
MANIFEST = nil
|
||||
|
||||
T.finish("game3_summary_layout_s3_test")
|
||||
|
||||
@@ -71,6 +71,24 @@ do
|
||||
{ species = 4, hp = 0, maxHp = 19 },
|
||||
}
|
||||
eq(Engine.hasLivingMons(deadParty), false, "hasLivingMons false when all dead")
|
||||
|
||||
-- pokeemerald/src/battle_script_commands.c:3556 Cmd_checkteamslost
|
||||
local eggParty = {
|
||||
{ species = 1, hp = 0, maxHp = 20 },
|
||||
{ species = 4, hp = 20, maxHp = 20, isEgg = true },
|
||||
}
|
||||
eq(Engine.hasLivingMons(eggParty), false, "an egg with HP is not a living battler")
|
||||
eq(Engine.nextLivingMonIndex(eggParty, 1), nil, "nextLivingMonIndex skips the egg")
|
||||
local foeUp = { { species = 16, hp = 30, maxHp = 30 } }
|
||||
local foeDown = { { species = 16, hp = 0, maxHp = 30 } }
|
||||
eq(Engine.checkEnd({ playerParty = eggParty, foeParty = foeUp }), "lose",
|
||||
"egg-only party loses while the foe is still up")
|
||||
eq(Engine.checkEnd({ playerParty = eggParty, foeParty = foeDown }), "lose",
|
||||
"egg-only party loses even when the foe has also fainted")
|
||||
eq(Engine.checkEnd({
|
||||
playerParty = { { species = 1, hp = 12, maxHp = 20 } },
|
||||
foeParty = { { species = 16, hp = 20, maxHp = 20, isEgg = true } },
|
||||
}), "win", "a foe party that is only an egg is defeated")
|
||||
end
|
||||
|
||||
print("\n--- Testing SwitchSeq Dynamic Withdraw Strings ---")
|
||||
|
||||
Reference in New Issue
Block a user