mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 09:00:51 +02:00
262 lines
8.7 KiB
Lua
262 lines
8.7 KiB
Lua
-- STADIUM battles: finding the ROM, and building the models out of it once.
|
|
--
|
|
-- The mod does not ship the Pokemon Stadium models and cannot: they are that
|
|
-- game's data. What it ships is the READER -- StadiumRom, StadiumFragment,
|
|
-- StadiumFx and StadiumBuild -- and the player supplies the cartridge, which
|
|
-- is exactly the arrangement this engine already has for the Game Boy ROM it
|
|
-- is a recompilation of (src/import/RomImporter.lua).
|
|
--
|
|
-- So: drop a Pokemon Stadium (US) ROM in `baseroms/`, and the first time the
|
|
-- game runs with the mod on, the models are built. Once, on a loading screen,
|
|
-- in about ten seconds. After that the packs sit in the save directory and
|
|
-- the mod reads them like any other asset.
|
|
--
|
|
-- ------- where "baseroms/" is
|
|
--
|
|
-- One relative path, and it deliberately covers two different places at once,
|
|
-- because PhysFS searches the save directory AND the game folder under the
|
|
-- same names:
|
|
--
|
|
-- * a folder install, or a checkout -- `baseroms/` next to main.lua, which
|
|
-- is also where the decompilation's own `make init` puts it
|
|
-- * a packaged or fused build, where the game folder is inside an archive
|
|
-- and cannot be written to -- `baseroms/` in the save directory, whose
|
|
-- absolute path this reports on screen so it can be found
|
|
--
|
|
-- Any of `.z64`, `.n64` and `.v64` is accepted; StadiumRom normalises the
|
|
-- byte order on load.
|
|
--
|
|
-- ------- what "installed" means
|
|
--
|
|
-- A marker file next to the packs, holding the format magic, how many species
|
|
-- were written and the md5 of the ROM they came from. All three matter. The
|
|
-- magic catches a format change (the packs are rebuilt rather than read as
|
|
-- garbage), the count catches a build that was interrupted half way, and the
|
|
-- md5 catches the player swapping the ROM for a different revision.
|
|
|
|
-- the mod namespace (see main.lua): V.require loads a sibling module
|
|
local V = ...
|
|
|
|
local StadiumPack = V.require("StadiumPack")
|
|
|
|
local StadiumInstall = {}
|
|
|
|
-- Where a ROM is looked for, and where the built packs are kept.
|
|
StadiumInstall.ROM_DIR = "baseroms"
|
|
StadiumInstall.DIR = StadiumPack.CACHE_DIR
|
|
StadiumInstall.MARKER = StadiumInstall.DIR .. "/pack.info"
|
|
|
|
-- Bumped whenever the .dsm format changes, so an old cache is rebuilt rather
|
|
-- than misread. Must track StadiumPack's magic.
|
|
StadiumInstall.FORMAT = "DSM3"
|
|
|
|
StadiumInstall.COUNT = 151
|
|
|
|
-- Named ROM files, in the order the decompilation's own pipeline looks for
|
|
-- them, then any ROM at all sitting in the folder.
|
|
local NAMED = {
|
|
StadiumInstall.ROM_DIR .. "/baserom.z64",
|
|
StadiumInstall.ROM_DIR .. "/us/baserom.z64",
|
|
StadiumInstall.ROM_DIR .. "/baserom.n64",
|
|
StadiumInstall.ROM_DIR .. "/baserom.v64",
|
|
}
|
|
|
|
local function fs()
|
|
return love and love.filesystem
|
|
end
|
|
|
|
local function isFile(path)
|
|
local f = fs()
|
|
if not (f and f.getInfo) then return false end
|
|
local ok, info = pcall(f.getInfo, path, "file")
|
|
return (ok and info) and true or false
|
|
end
|
|
|
|
-- The ROM's path on the PhysFS read path, or nil.
|
|
function StadiumInstall.romPath()
|
|
local f = fs()
|
|
if not f then return nil end
|
|
for _, path in ipairs(NAMED) do
|
|
if isFile(path) then return path end
|
|
end
|
|
local ok, items = pcall(f.getDirectoryItems, StadiumInstall.ROM_DIR)
|
|
if ok and items then
|
|
table.sort(items)
|
|
for _, name in ipairs(items) do
|
|
if name:lower():match("%.[nvz]64$") then
|
|
local path = StadiumInstall.ROM_DIR .. "/" .. name
|
|
if isFile(path) then return path end
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
function StadiumInstall.romPresent()
|
|
return StadiumInstall.romPath() ~= nil
|
|
end
|
|
|
|
-- Where to tell the player to put it. The save directory is the answer that
|
|
-- is always writable, and it is the one a packaged build needs.
|
|
function StadiumInstall.romHint()
|
|
local f = fs()
|
|
local base = (f and f.getSaveDirectory and select(2, pcall(f.getSaveDirectory)))
|
|
if type(base) ~= "string" then base = "the game folder" end
|
|
return base .. "/" .. StadiumInstall.ROM_DIR
|
|
end
|
|
|
|
-- ------- the marker
|
|
|
|
local function readMarker()
|
|
local f = fs()
|
|
if not (f and isFile(StadiumInstall.MARKER)) then return nil end
|
|
local ok, text = pcall(f.read, StadiumInstall.MARKER)
|
|
if not (ok and type(text) == "string") then return nil end
|
|
local format, count, md5 = text:match("^(%S+)%s+(%d+)%s*(%S*)")
|
|
if not format then return nil end
|
|
return { format = format, count = tonumber(count), md5 = md5 }
|
|
end
|
|
|
|
-- Whether a complete, current set of packs is on disk.
|
|
local readyCache = nil
|
|
|
|
function StadiumInstall.ready()
|
|
if readyCache ~= nil then return readyCache end
|
|
local m = readMarker()
|
|
readyCache = (m ~= nil and m.format == StadiumInstall.FORMAT
|
|
and m.count == StadiumInstall.COUNT) and true or false
|
|
return readyCache
|
|
end
|
|
|
|
-- Whether a complete set came WITH the mod folder -- a developer checkout
|
|
-- that has run tools/stadium_pack.py. Never true of a released build, which
|
|
-- carries no models at all.
|
|
--
|
|
-- Sampled at both ends of the dex rather than counted. The question being
|
|
-- asked is "did somebody run the packer here", not "is every one of the 151
|
|
-- present"; a genuinely half-written folder is a case for the marker file,
|
|
-- which is what catches an interrupted RUNTIME build.
|
|
local function shipped()
|
|
local mod = V.mod
|
|
if not (mod and mod.read) then return false end
|
|
for _, dex in ipairs({ 1, 151 }) do
|
|
local ok, bytes = pcall(mod.read, mod,
|
|
("%s/%03d.dsm"):format(StadiumPack.DIR, dex))
|
|
if not (ok and type(bytes) == "string" and #bytes > 4) then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- Whether the STADIUM rungs can be offered at all: either the packs have been
|
|
-- built from the player's ROM, or the mod folder already carries a set.
|
|
function StadiumInstall.available()
|
|
if StadiumInstall.ready() then return true end
|
|
return shipped()
|
|
end
|
|
|
|
-- Whether there is work to do: something to build from, and nothing usable
|
|
-- yet.
|
|
--
|
|
-- A checkout that already carries a set is NOT pending. Building anyway would
|
|
-- be correct and would also mean a ten-second loading screen on the first run
|
|
-- of every checkout, to arrive at the files that were already sitting there.
|
|
function StadiumInstall.pending()
|
|
if StadiumInstall.available() then return false end
|
|
return StadiumInstall.romPresent()
|
|
end
|
|
|
|
function StadiumInstall.forget()
|
|
readyCache = nil
|
|
end
|
|
|
|
-- ------- building
|
|
|
|
local job = nil
|
|
local status = { state = "idle", done = 0, total = StadiumInstall.COUNT }
|
|
|
|
StadiumInstall.status = status
|
|
|
|
local function writePack(species, bytes)
|
|
local f = fs()
|
|
if not f then return false, "no filesystem" end
|
|
local ok, err = f.write(("%s/%03d.dsm"):format(StadiumInstall.DIR, species),
|
|
bytes)
|
|
if not ok then return false, tostring(err) end
|
|
return true
|
|
end
|
|
|
|
-- Open the ROM and start a stepped build. Returns false plus a reason when
|
|
-- there is nothing to build from.
|
|
function StadiumInstall.begin()
|
|
local f = fs()
|
|
if not f then return false, "no filesystem" end
|
|
local path = StadiumInstall.romPath()
|
|
if not path then return false, "no ROM in " .. StadiumInstall.ROM_DIR end
|
|
|
|
local okRead, bytes = pcall(f.read, path)
|
|
if not (okRead and type(bytes) == "string") then
|
|
return false, "could not read " .. path
|
|
end
|
|
|
|
local StadiumRom = V.require("StadiumRom")
|
|
local StadiumBuild = V.require("StadiumBuild")
|
|
local rom, err = StadiumRom.open(bytes)
|
|
if not rom then return false, tostring(err) end
|
|
if not rom:isExpectedUS() then
|
|
V.mod.log:warn("stadium: %s is md5 %s, not the US 1.0 ROM the model "
|
|
.. "offsets are keyed to -- building anyway", path,
|
|
tostring(rom:md5()))
|
|
end
|
|
|
|
pcall(f.createDirectory, StadiumInstall.DIR)
|
|
job = StadiumBuild.job(rom, writePack, StadiumInstall.COUNT)
|
|
job.md5 = rom:md5()
|
|
status.state = "building"
|
|
status.done = 0
|
|
status.total = job.total
|
|
status.error = nil
|
|
return true
|
|
end
|
|
|
|
-- One species. Returns true while there is more to do.
|
|
function StadiumInstall.step()
|
|
if not job then return false end
|
|
local more = job:step()
|
|
status.done = job.done
|
|
status.species = job.species
|
|
if job.error then
|
|
status.state = "failed"
|
|
status.error = job.error
|
|
job = nil
|
|
return false
|
|
end
|
|
if not more then
|
|
local f = fs()
|
|
local wrote = #job.failed == 0
|
|
if wrote and f then
|
|
pcall(f.write, StadiumInstall.MARKER,
|
|
("%s %d %s\n"):format(StadiumInstall.FORMAT, job.total,
|
|
tostring(job.md5 or "")))
|
|
readyCache = nil
|
|
StadiumPack.forget()
|
|
end
|
|
if not wrote then
|
|
status.state = "failed"
|
|
status.error = ("%d of %d models could not be built")
|
|
:format(#job.failed, job.total)
|
|
else
|
|
status.state = "done"
|
|
end
|
|
job = nil
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
function StadiumInstall.cancel()
|
|
job = nil
|
|
status.state = "idle"
|
|
end
|
|
|
|
return StadiumInstall
|