mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 08:01:09 +02:00
dramatic stadium
This commit is contained in:
@@ -0,0 +1,709 @@
|
||||
-- STADIUM battles: turning the ROM into assets/stadium/NNN.dsm.
|
||||
--
|
||||
-- The Lua half of tools/stadium_pack.py: measure the bind pose, decide
|
||||
-- whether a species' standby loop can be trusted, and write the packed file.
|
||||
-- Together with StadiumRom, StadiumFragment and StadiumFx this is everything
|
||||
-- between `baserom.z64` and a Pokemon standing on a battle tile.
|
||||
--
|
||||
-- The Python remains the ORACLE. tests/stadium_extract_test.lua runs this
|
||||
-- over the same ROM and requires all 151 files to come out byte for byte
|
||||
-- identical to what the packer writes. That is a strong test in a way a unit
|
||||
-- test of any one function here would not be: every rounding mode, every
|
||||
-- iteration order, every off-by-one in an index shows up as a differing byte,
|
||||
-- and there are thirty-four megabytes of them.
|
||||
--
|
||||
-- ------- stepped, not blocking
|
||||
--
|
||||
-- `StadiumBuild.job()` returns a coroutine-backed job that does one species
|
||||
-- per `step()`, so the caller can draw a progress bar between them
|
||||
-- (StadiumInstall). A species is a few tens of milliseconds; the whole set is
|
||||
-- around half a minute, which is far too long to spend inside one frame and
|
||||
-- perfectly fine spread across a loading screen.
|
||||
|
||||
-- the mod namespace (see main.lua): V.require loads a sibling module
|
||||
local V = ...
|
||||
|
||||
local StadiumRom = V.require("StadiumRom")
|
||||
local StadiumFragment = V.require("StadiumFragment")
|
||||
local StadiumFx = V.require("StadiumFx")
|
||||
|
||||
local StadiumBuild = {}
|
||||
|
||||
local floor = math.floor
|
||||
local sin, cos = math.sin, math.cos
|
||||
local pi = math.pi
|
||||
local char = string.char
|
||||
local concat = table.concat
|
||||
local frexp = math.frexp
|
||||
local roundHalfEven = StadiumFragment.roundHalfEven
|
||||
|
||||
-- The battle system's fixed context slots, in slot order from 165. The mod
|
||||
-- indexes this list by POSITION, so the ORDER is the format's contract and
|
||||
-- has to stay identical to StadiumPack.CONTEXT and to the packer's CONTEXTS.
|
||||
StadiumBuild.CONTEXTS = {
|
||||
"idle", "hit", "faint", "entrance", "reaction_169", "reaction_170",
|
||||
"reaction_171", "reaction_172", "reaction_173", "reaction_174",
|
||||
"struggle", "idle_alt", "faint_alt", "flinch", "reaction_179",
|
||||
"reaction_180", "reaction_181", "reaction_182", "entrance_alt",
|
||||
"idle_return",
|
||||
}
|
||||
|
||||
-- Which context name wins when several claim the same animation.
|
||||
local NAME_PREF = { "idle", "hit", "faint", "entrance", "struggle", "flinch" }
|
||||
|
||||
local N_MOVES = StadiumRom.N_MOVES
|
||||
local CTX_BASE = 165
|
||||
local NONE16 = 0xFFFF
|
||||
|
||||
-- ------- the bind pose
|
||||
|
||||
-- The game's rotation as a 3x3, rows first (src/F420.c func_8000F730):
|
||||
-- Rx*Ry*Rz in row-vector form.
|
||||
local function quatBasis(r)
|
||||
local sx, cx = sin(r[1] / 32768 * pi), cos(r[1] / 32768 * pi)
|
||||
local sy, cy = sin(r[2] / 32768 * pi), cos(r[2] / 32768 * pi)
|
||||
local sz, cz = sin(r[3] / 32768 * pi), cos(r[3] / 32768 * pi)
|
||||
return { cy * cz, sx * sy * cz - cx * sz, cx * sy * cz + sx * sz },
|
||||
{ cy * sz, sx * sy * sz + cx * cz, cx * sy * sz - sx * cz },
|
||||
{ -sy, sx * cy, cx * cy }
|
||||
end
|
||||
|
||||
-- 3x4 (three rotation rows plus a translation column) times the same.
|
||||
local function matMul(a, b)
|
||||
local out = {}
|
||||
for r = 1, 3 do
|
||||
local ar = a[r]
|
||||
out[r] = {
|
||||
ar[1] * b[1][1] + ar[2] * b[2][1] + ar[3] * b[3][1],
|
||||
ar[1] * b[1][2] + ar[2] * b[2][2] + ar[3] * b[3][2],
|
||||
ar[1] * b[1][3] + ar[2] * b[2][3] + ar[3] * b[3][3],
|
||||
ar[1] * b[1][4] + ar[2] * b[2][4] + ar[3] * b[3][4] + ar[4],
|
||||
}
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- One component of one bone's t/r/s at a frame. The extractor's own shape: a
|
||||
-- bare number when the component holds still for the whole animation, one
|
||||
-- number a frame when it does not.
|
||||
local function component(comps, i, frame, fallback)
|
||||
if comps == nil then return fallback end
|
||||
local c = comps[i]
|
||||
if type(c) == "table" then
|
||||
local n = #c
|
||||
if n == 0 then return fallback end
|
||||
return c[frame % n + 1]
|
||||
end
|
||||
return c
|
||||
end
|
||||
|
||||
-- The bone TRS an animation holds at `frame`, rest where it is silent.
|
||||
local function animSample(bones, anim, frame)
|
||||
local tracks = anim.tracks
|
||||
return function(i)
|
||||
local b = bones[i]
|
||||
local tr = tracks[i]
|
||||
if not tr then return b.t, b.r, b.s end
|
||||
return { component(tr.t, 1, frame, b.t[1]),
|
||||
component(tr.t, 2, frame, b.t[2]),
|
||||
component(tr.t, 3, frame, b.t[3]) },
|
||||
{ component(tr.r, 1, frame, b.r[1]),
|
||||
component(tr.r, 2, frame, b.r[2]),
|
||||
component(tr.r, 3, frame, b.r[3]) },
|
||||
{ component(tr.s, 1, frame, b.s[1]),
|
||||
component(tr.s, 2, frame, b.s[2]),
|
||||
component(tr.s, 3, frame, b.s[3]) }
|
||||
end
|
||||
end
|
||||
|
||||
local function restSample(bones)
|
||||
return function(i)
|
||||
local b = bones[i]
|
||||
return b.t, b.r, b.s
|
||||
end
|
||||
end
|
||||
|
||||
-- Every bone's draw matrix at one instant, as 3x4 rows.
|
||||
--
|
||||
-- The game keeps bone scale OUT of the matrix chain: it accumulates in its own
|
||||
-- stack, a bone's local translation is pre-multiplied by the PARENT's
|
||||
-- accumulated scale, and the bone's own accumulated scale is applied to the
|
||||
-- finished matrix at draw time.
|
||||
--
|
||||
-- Two chains, and the distinction is the whole point: `pivot` is the
|
||||
-- rotation/translation a CHILD inherits, and the draw matrix is that with the
|
||||
-- bone's own accumulated scale applied on the right. Folding the scale into
|
||||
-- the chain instead applies every ancestor's scale twice -- which is exactly
|
||||
-- the multiplicative propagation glTF has and the game does not.
|
||||
local function bindMatrices(bones, sample)
|
||||
sample = sample or restSample(bones)
|
||||
local pivot, draw, acc = {}, {}, {}
|
||||
local IDENT = { { 1, 0, 0, 0 }, { 0, 1, 0, 0 }, { 0, 0, 1, 0 } }
|
||||
for i = 1, #bones do
|
||||
local bt, br, bs = sample(i)
|
||||
local p = bones[i].parent
|
||||
local pa = (p >= 0) and acc[p + 1] or { 1.0, 1.0, 1.0 }
|
||||
local pm = (p >= 0) and pivot[p + 1] or IDENT
|
||||
local r1, r2, r3 = quatBasis(br)
|
||||
local m = matMul(pm, {
|
||||
{ r1[1], r1[2], r1[3], bt[1] * pa[1] },
|
||||
{ r2[1], r2[2], r2[3], bt[2] * pa[2] },
|
||||
{ r3[1], r3[2], r3[3], bt[3] * pa[3] },
|
||||
})
|
||||
local a = { pa[1] * bs[1], pa[2] * bs[2], pa[3] * bs[3] }
|
||||
acc[i] = a
|
||||
pivot[i] = m
|
||||
-- scale on the right: the bone's own space, so it cannot reach children
|
||||
draw[i] = {
|
||||
{ m[1][1] * a[1], m[1][2] * a[2], m[1][3] * a[3], m[1][4] },
|
||||
{ m[2][1] * a[1], m[2][2] * a[2], m[2][3] * a[3], m[2][4] },
|
||||
{ m[3][1] * a[1], m[3][2] * a[2], m[3][3] * a[3], m[3][4] },
|
||||
}
|
||||
end
|
||||
return draw
|
||||
end
|
||||
|
||||
StadiumBuild.bindMatrices = bindMatrices
|
||||
StadiumBuild.animSample = animSample
|
||||
|
||||
-- The axis-aligned box the whole model occupies under `mats`, in game units
|
||||
-- after the model_root scale.
|
||||
local function poseBox(data, mats)
|
||||
local root = data.rootScale[1]
|
||||
local lo1, lo2, lo3 = 1e30, 1e30, 1e30
|
||||
local hi1, hi2, hi3 = -1e30, -1e30, -1e30
|
||||
for _, prim in ipairs(data.prims) do
|
||||
local pos, skin = prim.pos, prim.skin
|
||||
for i = 1, prim.nverts do
|
||||
local m = mats[skin[i] + 1]
|
||||
if m then
|
||||
local x, y, z = pos[i * 3 - 2], pos[i * 3 - 1], pos[i * 3]
|
||||
local a = (m[1][1] * x + m[1][2] * y + m[1][3] * z + m[1][4]) * root
|
||||
local b = (m[2][1] * x + m[2][2] * y + m[2][3] * z + m[2][4]) * root
|
||||
local c = (m[3][1] * x + m[3][2] * y + m[3][3] * z + m[3][4]) * root
|
||||
if a < lo1 then lo1 = a end
|
||||
if b < lo2 then lo2 = b end
|
||||
if c < lo3 then lo3 = c end
|
||||
if a > hi1 then hi1 = a end
|
||||
if b > hi2 then hi2 = b end
|
||||
if c > hi3 then hi3 = c end
|
||||
end
|
||||
end
|
||||
end
|
||||
return lo1, lo2, lo3, hi1, hi2, hi3
|
||||
end
|
||||
|
||||
-- (height, floor, radius): how tall the mon is, where its lowest point sits
|
||||
-- relative to the model's own origin, and how wide it is -- all in game units
|
||||
-- after the model_root scale.
|
||||
--
|
||||
-- Measured on the BIND POSE, which is the one pose in the set that can be
|
||||
-- trusted for this. It reproduces the verified glTF export exactly on all 151
|
||||
-- species, and it is immune to the animation quirks a handful of them carry
|
||||
-- (see idleIsBroken) -- quirks that would otherwise decide how big every OTHER
|
||||
-- frame of those species is drawn.
|
||||
--
|
||||
-- The floor is the interesting number, and it reads cleanly: 119 of the 151
|
||||
-- sit within 5% of zero, which says the model origin IS where the game stands
|
||||
-- a Pokemon on its field. Every species that does not is one that hovers.
|
||||
local function stance(data)
|
||||
local lo1, lo2, lo3, hi1, hi2, hi3 = poseBox(data, bindMatrices(data.bones))
|
||||
if lo1 > hi1 then return 0.0, 0.0, 0.0 end
|
||||
local w, d = hi1 - lo1, hi3 - lo3
|
||||
return hi2 - lo2, lo2, (w > d and w or d) / 2
|
||||
end
|
||||
|
||||
StadiumBuild.stance = stance
|
||||
|
||||
-- Whether this species' standby loop is corrupt in the source data.
|
||||
--
|
||||
-- A handful come out of the extraction with animations that throw bones
|
||||
-- hundreds of units off the body -- Exeggutor, Tangela and Magmar, whose
|
||||
-- channel streams the game's own index arithmetic evidently reads differently
|
||||
-- from the way this does. Played, they look like a Pokemon coming apart; the
|
||||
-- mod would rather stand them still.
|
||||
--
|
||||
-- The test is deliberately narrow, because "differs from the bind pose" is NOT
|
||||
-- brokenness. It is asked only of the STANDBY loop -- the one animation that
|
||||
-- is supposed to stay where it is, since a faint is meant to end far from the
|
||||
-- standing pose and an attack is meant to lunge -- and it wants both a large
|
||||
-- size blow-up and real drift, or an enormous amount of one. Dewgong is what
|
||||
-- calibrates it: its idle is 2.4x its own bind pose because the BIND is the
|
||||
-- collapsed one, and it drifts barely at all, so it must not be caught.
|
||||
local function idleIsBroken(data, idle)
|
||||
if idle == nil then return false end
|
||||
local bones = data.bones
|
||||
local _, lo2, _, _, hi2 = poseBox(data, bindMatrices(bones))
|
||||
local span = hi2 - lo2
|
||||
if span <= 0 then return false end
|
||||
local worstH, worstDrift = 1.0, 0.0
|
||||
local frame = 0
|
||||
while frame < idle.frames do
|
||||
local _, flo2, _, _, fhi2 = poseBox(data,
|
||||
bindMatrices(bones, animSample(bones, idle, frame)))
|
||||
local h = (fhi2 - flo2) / span
|
||||
if h > worstH then worstH = h end
|
||||
local d1 = (flo2 - lo2) / span
|
||||
local d2 = (fhi2 - hi2) / span
|
||||
if d1 < 0 then d1 = -d1 end
|
||||
if d2 < 0 then d2 = -d2 end
|
||||
if d1 > worstDrift then worstDrift = d1 end
|
||||
if d2 > worstDrift then worstDrift = d2 end
|
||||
frame = frame + 3
|
||||
end
|
||||
return (worstH > 2.5 and worstDrift > 1.5)
|
||||
or worstDrift > 2.0 or worstH > 3.4
|
||||
end
|
||||
|
||||
-- ------- writing
|
||||
|
||||
local function clamp(v, lo, hi)
|
||||
if v < lo then return lo end
|
||||
if v > hi then return hi end
|
||||
return v
|
||||
end
|
||||
|
||||
-- Toward zero, which is what Python's int() does to a float and NOT what
|
||||
-- floor() does to a negative one.
|
||||
--
|
||||
-- It matters in exactly one place, and it is easy to miss: almost everything
|
||||
-- reaching the integer writers below has already been rounded, so truncation
|
||||
-- is a no-op on it. The exception is the generated effects' crossed quads
|
||||
-- (StadiumFx), whose vertices are raw floats and straddle the origin -- so
|
||||
-- the ones at negative x, and only those, come out a unit adrift if this
|
||||
-- floors.
|
||||
local function trunc(v)
|
||||
if v >= 0 then return floor(v) end
|
||||
return -floor(-v)
|
||||
end
|
||||
|
||||
-- 16.16, which holds every bone scale in the set (-31 .. 100) with more
|
||||
-- precision than anything can see.
|
||||
local function fixed(v)
|
||||
return clamp(roundHalfEven(v * 65536), -2147483648, 2147483647)
|
||||
end
|
||||
|
||||
-- IEEE 754 single, little-endian, rounded to nearest with ties to even -- the
|
||||
-- same rounding Python's struct.pack('<f') does, so the four floats in the
|
||||
-- header come out bit for bit the same as the packer's.
|
||||
local function f32(x)
|
||||
local sign = 0
|
||||
if x < 0 or (x == 0 and 1 / x < 0) then
|
||||
sign = 128
|
||||
x = -x
|
||||
end
|
||||
if x ~= x then return char(0, 0, 192, 127 + sign) end -- NaN
|
||||
if x == math.huge then return char(0, 0, 128, 127 + sign) end
|
||||
if x == 0 then return char(0, 0, 0, sign) end
|
||||
local m, e = frexp(x) -- x = m * 2^e, 0.5 <= m < 1
|
||||
local E = e - 1 + 127
|
||||
local mant
|
||||
if E >= 255 then
|
||||
return char(0, 0, 128, 127 + sign) -- overflow
|
||||
elseif E <= 0 then
|
||||
-- subnormal: no exponent left, so the mantissa carries the whole value
|
||||
mant = roundHalfEven(x / 2 ^ -149)
|
||||
if mant >= 8388608 then
|
||||
mant, E = mant - 8388608, 1
|
||||
else
|
||||
E = 0
|
||||
end
|
||||
else
|
||||
mant = roundHalfEven((m * 2 - 1) * 8388608)
|
||||
if mant == 8388608 then -- rounded up into the next
|
||||
mant, E = 0, E + 1
|
||||
if E >= 255 then return char(0, 0, 128, 127 + sign) end
|
||||
end
|
||||
end
|
||||
local b4 = sign + floor(E / 2)
|
||||
local b3 = (E % 2) * 128 + floor(mant / 65536)
|
||||
local b2 = floor(mant / 256) % 256
|
||||
local b1 = mant % 256
|
||||
return char(b1, b2, b3, b4)
|
||||
end
|
||||
|
||||
StadiumBuild.f32 = f32
|
||||
|
||||
local Writer = {}
|
||||
Writer.__index = Writer
|
||||
|
||||
local function newWriter()
|
||||
return setmetatable({ parts = {}, n = 0 }, Writer)
|
||||
end
|
||||
|
||||
function Writer:raw(s)
|
||||
self.n = self.n + 1
|
||||
self.parts[self.n] = s
|
||||
end
|
||||
|
||||
function Writer:u8(v)
|
||||
self:raw(char(v % 256))
|
||||
end
|
||||
|
||||
function Writer:i8(v)
|
||||
v = clamp(trunc(v), -128, 127)
|
||||
self:raw(char(v % 256))
|
||||
end
|
||||
|
||||
function Writer:u16(v)
|
||||
v = v % 65536
|
||||
self:raw(char(v % 256, floor(v / 256)))
|
||||
end
|
||||
|
||||
function Writer:i16(v)
|
||||
v = clamp(trunc(v), -32768, 32767) % 65536
|
||||
self:raw(char(v % 256, floor(v / 256)))
|
||||
end
|
||||
|
||||
function Writer:u32(v)
|
||||
v = v % 4294967296
|
||||
self:raw(char(v % 256, floor(v / 256) % 256, floor(v / 65536) % 256,
|
||||
floor(v / 16777216) % 256))
|
||||
end
|
||||
|
||||
function Writer:i32(v)
|
||||
v = clamp(trunc(v), -2147483648, 2147483647) % 4294967296
|
||||
self:raw(char(v % 256, floor(v / 256) % 256, floor(v / 65536) % 256,
|
||||
floor(v / 16777216) % 256))
|
||||
end
|
||||
|
||||
function Writer:f32(v)
|
||||
self:raw(f32(v))
|
||||
end
|
||||
|
||||
function Writer:bytes()
|
||||
return concat(self.parts)
|
||||
end
|
||||
|
||||
-- One component of one bone's t/r/s in one animation. `values` is the
|
||||
-- extractor's own shape: a bare number when the component holds still for the
|
||||
-- whole animation, or one number a frame when it does not. That fold is where
|
||||
-- most of the size saving is -- a bone that only rotates costs two bytes for
|
||||
-- each of its six other components.
|
||||
local function writeTrackComponent(w, values, kind)
|
||||
local isArray = type(values) == "table"
|
||||
w:u8(isArray and 1 or 0)
|
||||
if kind == "s" then
|
||||
if isArray then
|
||||
for i = 1, #values do w:i32(fixed(values[i])) end
|
||||
else
|
||||
w:i32(fixed(values))
|
||||
end
|
||||
else
|
||||
if isArray then
|
||||
for i = 1, #values do w:i16(roundHalfEven(values[i])) end
|
||||
else
|
||||
w:i16(roundHalfEven(values))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Which animation each fixed battle context slot resolves to: entries 165
|
||||
-- upward of the species' own battle table, in slot order. An entry naming an
|
||||
-- animation the species does not have is written as "none" rather than
|
||||
-- clamped -- the mod would rather fall back than play the wrong clip.
|
||||
function StadiumBuild.contextTable(rows, nAnims)
|
||||
local ctx = {}
|
||||
for i = 1, #StadiumBuild.CONTEXTS do
|
||||
local row = rows[CTX_BASE + i - 1]
|
||||
local ai = row and row[1] or nil
|
||||
ctx[i] = (ai ~= nil and ai < nAnims) and ai or NONE16
|
||||
end
|
||||
return ctx
|
||||
end
|
||||
|
||||
-- ------- naming the animations
|
||||
--
|
||||
-- build.py's label_animations. The names are not read at runtime -- the mod
|
||||
-- addresses animations by index through the move and context tables -- but
|
||||
-- they are in the format, so they have to be produced the same way for the
|
||||
-- oracle diff to mean anything. They also make a packed file readable in a
|
||||
-- hex dump, which is worth the byte apiece.
|
||||
|
||||
local function labelAnimations(data, rows, nAux)
|
||||
local anims = data.anims
|
||||
local n = #anims
|
||||
local uses, moveUses = {}, {}
|
||||
local auxOrder, auxCount = {}, {}
|
||||
for i = 1, n do
|
||||
uses[i], moveUses[i] = {}, 0
|
||||
auxOrder[i], auxCount[i] = {}, {}
|
||||
end
|
||||
for e = 0, rows.n - 1 do
|
||||
local ai = rows[e][1]
|
||||
if ai < n then
|
||||
if e < N_MOVES then
|
||||
moveUses[ai + 1] = moveUses[ai + 1] + 1
|
||||
elseif e >= CTX_BASE and e < CTX_BASE + #StadiumBuild.CONTEXTS then
|
||||
local list = uses[ai + 1]
|
||||
list[#list + 1] = StadiumBuild.CONTEXTS[e - CTX_BASE + 1]
|
||||
end
|
||||
local ax = rows[e][2]
|
||||
if ax >= 0 and ax < nAux then
|
||||
local counts, order = auxCount[ai + 1], auxOrder[ai + 1]
|
||||
if counts[ax] == nil then
|
||||
counts[ax] = 0
|
||||
order[#order + 1] = ax
|
||||
end
|
||||
counts[ax] = counts[ax] + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, n do
|
||||
-- sorted(set(uses)) -- the alphabetically first context is the fallback
|
||||
-- name, so the ordering is part of the answer
|
||||
local seen, ctx = {}, {}
|
||||
for _, name in ipairs(uses[i]) do
|
||||
if not seen[name] then
|
||||
seen[name] = true
|
||||
ctx[#ctx + 1] = name
|
||||
end
|
||||
end
|
||||
table.sort(ctx)
|
||||
local name = nil
|
||||
for _, pref in ipairs(NAME_PREF) do
|
||||
if seen[pref] then
|
||||
name = pref
|
||||
break
|
||||
end
|
||||
end
|
||||
if not name then
|
||||
if moveUses[i] > 0 then
|
||||
name = "attack"
|
||||
elseif ctx[1] then
|
||||
name = ctx[1]
|
||||
else
|
||||
name = "anim" .. (i - 1)
|
||||
end
|
||||
end
|
||||
anims[i].name = name
|
||||
-- Counter.most_common(1): the highest count, and on a tie the one that
|
||||
-- was inserted first
|
||||
local best, bestN = -1, -1
|
||||
local order, counts = auxOrder[i], auxCount[i]
|
||||
for _, ax in ipairs(order) do
|
||||
if counts[ax] > bestN then
|
||||
best, bestN = ax, counts[ax]
|
||||
end
|
||||
end
|
||||
anims[i].aux = best
|
||||
end
|
||||
|
||||
local seenName = {}
|
||||
for i = 1, n do
|
||||
local base = anims[i].name
|
||||
local k = seenName[base] or 0
|
||||
seenName[base] = k + 1
|
||||
if k > 0 then anims[i].name = base .. "_" .. (k + 1) end
|
||||
end
|
||||
end
|
||||
|
||||
-- ------- the pack
|
||||
|
||||
function StadiumBuild.pack(data, species, moveRows, ctx)
|
||||
local w = newWriter()
|
||||
local bones, prims = data.bones, data.prims
|
||||
local textures, anims, aux = data.textures, data.anims, data.auxAnims
|
||||
|
||||
local height, floorY, radius = stance(data)
|
||||
|
||||
local idleIndex = ctx[1] -- CONTEXTS[1] is "idle"
|
||||
local idle = (idleIndex ~= NONE16) and anims[idleIndex + 1] or nil
|
||||
local static = idleIsBroken(data, idle)
|
||||
|
||||
w:raw("DSM3")
|
||||
w:u16(species)
|
||||
w:u16(#bones)
|
||||
w:u16(#prims)
|
||||
w:u16(#textures)
|
||||
w:u16(#anims)
|
||||
w:u16(#aux)
|
||||
w:f32(data.rootScale[1])
|
||||
-- 1 = hold the bind pose, never play an animation
|
||||
w:u8(static and 1 or 0)
|
||||
w:f32(height)
|
||||
w:f32(floorY)
|
||||
w:f32(radius)
|
||||
|
||||
for m = 1, N_MOVES do
|
||||
local row = moveRows[m]
|
||||
w:u16((row and row[1] < #anims) and row[1] or NONE16)
|
||||
end
|
||||
for m = 1, N_MOVES do
|
||||
local row = moveRows[m]
|
||||
w:i16((row and row[2] >= 0 and row[2] < #aux) and row[2] or -1)
|
||||
end
|
||||
for i = 1, #ctx do w:u16(ctx[i]) end
|
||||
|
||||
for i = 1, #bones do
|
||||
local b = bones[i]
|
||||
w:i16(b.parent)
|
||||
for k = 1, 3 do w:i16(roundHalfEven(b.t[k])) end
|
||||
for k = 1, 3 do w:i16(b.r[k]) end
|
||||
for k = 1, 3 do w:i32(fixed(b.s[k])) end
|
||||
end
|
||||
|
||||
for i = 1, #prims do
|
||||
local p = prims[i]
|
||||
w:u16(p.tex)
|
||||
-- the display list's own cull mode: 1024 is G_CULL_BACK
|
||||
w:u8((p.cull and p.cull ~= 0) and 1 or 0)
|
||||
w:u8((p.blend == "add") and 1 or 0)
|
||||
w:i16(p.texAnim or -1)
|
||||
-- sorted by the stream's own byte, which is what the reader keys on
|
||||
local keys = {}
|
||||
if p.texMap then
|
||||
for k in pairs(p.texMap) do keys[#keys + 1] = k end
|
||||
table.sort(keys)
|
||||
end
|
||||
w:u8(#keys)
|
||||
for _, k in ipairs(keys) do
|
||||
w:u8(k)
|
||||
w:u16(p.texMap[k])
|
||||
end
|
||||
local frames = p.fxFrames
|
||||
w:u16(frames and #frames or 0)
|
||||
if frames then
|
||||
for k = 1, #frames do w:u16(frames[k]) end
|
||||
end
|
||||
local pos, uv, nrm, skin = p.pos, p.uv, p.nrm, p.skin
|
||||
w:u16(p.nverts)
|
||||
w:u16(p.nidx)
|
||||
for k = 1, p.nverts do
|
||||
w:i16(pos[k * 3 - 2])
|
||||
w:i16(pos[k * 3 - 1])
|
||||
w:i16(pos[k * 3])
|
||||
-- 1/512, which puts a texel of the largest texture in the set well
|
||||
-- inside a step and still reaches the +-32 the wrapped coordinates of
|
||||
-- some display lists run to
|
||||
w:i16(roundHalfEven(uv[k * 2 - 1] * 512))
|
||||
w:i16(roundHalfEven(uv[k * 2] * 512))
|
||||
w:i8(roundHalfEven(nrm[k * 3 - 2] * 127))
|
||||
w:i8(roundHalfEven(nrm[k * 3 - 1] * 127))
|
||||
w:i8(roundHalfEven(nrm[k * 3] * 127))
|
||||
w:u8(skin[k])
|
||||
end
|
||||
for k = 1, p.nidx do w:u16(p.idx[k]) end
|
||||
end
|
||||
|
||||
for i = 1, #textures do
|
||||
local t = textures[i]
|
||||
w:u16(t.w)
|
||||
w:u16(t.h)
|
||||
w:u32(#t.rgba)
|
||||
w:raw(t.rgba)
|
||||
end
|
||||
|
||||
local REST = { t = { 0, 0, 0 }, r = { 0, 0, 0 }, s = { 1.0, 1.0, 1.0 } }
|
||||
for i = 1, #anims do
|
||||
local a = anims[i]
|
||||
local name = a.name or ""
|
||||
if #name > 255 then name = name:sub(1, 255) end
|
||||
w:u8(#name)
|
||||
w:raw(name)
|
||||
w:u16(a.frames)
|
||||
w:u16(a.loopStart or 0)
|
||||
w:i16(a.aux or -1)
|
||||
for bi = 1, #bones do
|
||||
local tr = a.tracks[bi]
|
||||
if not tr then
|
||||
w:u8(0)
|
||||
else
|
||||
w:u8(1)
|
||||
for _, key in ipairs({ "t", "r", "s" }) do
|
||||
local comps = tr[key]
|
||||
if comps == nil then
|
||||
-- a bone the animation leaves at its rest value for this path:
|
||||
-- written as three constants so the reader never has to branch on
|
||||
-- a missing path
|
||||
comps = bones[bi][key] or REST[key]
|
||||
end
|
||||
for c = 1, 3 do writeTrackComponent(w, comps[c], key) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, #aux do
|
||||
local a = aux[i]
|
||||
w:u16(a.frames)
|
||||
w:u16(a.loopStart or 0)
|
||||
w:u16(#a.channels)
|
||||
for _, ch in ipairs(a.channels) do
|
||||
w:u16(ch.n)
|
||||
for k = 1, ch.n do w:u16(ch[k]) end
|
||||
end
|
||||
end
|
||||
|
||||
return w:bytes(), height, floorY, radius
|
||||
end
|
||||
|
||||
-- ------- one species, end to end
|
||||
|
||||
-- The same three steps build.py takes: parse the fragment, label the
|
||||
-- animations off the species' battle table, then hang the generated fire/gas
|
||||
-- stand-ins on the bones the game's own effect callbacks hang off.
|
||||
function StadiumBuild.species(rom, fileno)
|
||||
local blob = rom:model(fileno)
|
||||
if not blob then return nil, ("file %d is not in the archive"):format(fileno) end
|
||||
local data, err = StadiumFragment.extract(blob, ("%d.bin"):format(fileno))
|
||||
if not data then return nil, err end
|
||||
local species = data.species
|
||||
local rows = rom:battleRows(species)
|
||||
labelAnimations(data, rows, #data.auxAnims)
|
||||
StadiumFx.attach(data, species)
|
||||
|
||||
local moveRows = {}
|
||||
for m = 1, N_MOVES do moveRows[m] = rows[m - 1] end
|
||||
local ctx = StadiumBuild.contextTable(rows, #data.anims)
|
||||
local bytes, height, floorY, radius =
|
||||
StadiumBuild.pack(data, species, moveRows, ctx)
|
||||
return { species = species, bytes = bytes, height = height,
|
||||
floor = floorY, radius = radius, bones = #data.bones,
|
||||
prims = #data.prims, anims = #data.anims,
|
||||
warnings = data.warnings }
|
||||
end
|
||||
|
||||
-- ------- the stepped job
|
||||
--
|
||||
-- `write(species, bytes)` is called for each finished pack and must answer
|
||||
-- truthy; anything else stops the job with an error. Returning a job rather
|
||||
-- than taking a callback for progress keeps the caller in charge of when work
|
||||
-- happens, which is what lets a loading screen stay responsive.
|
||||
function StadiumBuild.job(rom, write, count)
|
||||
local total = count or StadiumRom.N_POKEMON
|
||||
local n = rom:modelCount()
|
||||
if total > n then total = n end
|
||||
local job = { total = total, done = 0, bytes = 0, failed = {}, species = nil }
|
||||
|
||||
function job:step()
|
||||
if self.done >= self.total then return false end
|
||||
local fileno = self.done
|
||||
local ok, res, err = pcall(StadiumBuild.species, rom, fileno)
|
||||
if ok and res then
|
||||
local wrote, wErr = write(res.species, res.bytes)
|
||||
if not wrote then
|
||||
self.error = wErr or ("could not write species " .. res.species)
|
||||
self.done = self.total
|
||||
return false
|
||||
end
|
||||
self.bytes = self.bytes + #res.bytes
|
||||
self.species = res.species
|
||||
else
|
||||
self.failed[#self.failed + 1] = fileno
|
||||
self.lastError = ok and err or res
|
||||
end
|
||||
self.done = self.done + 1
|
||||
return self.done < self.total
|
||||
end
|
||||
|
||||
function job:progress()
|
||||
if self.total <= 0 then return 1 end
|
||||
return self.done / self.total
|
||||
end
|
||||
|
||||
return job
|
||||
end
|
||||
|
||||
return StadiumBuild
|
||||
Reference in New Issue
Block a user