Files
DramaticShapeVoxelMod/lib/StadiumBuild.lua
T
2026-08-04 14:18:11 -04:00

711 lines
23 KiB
Lua

-- 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", "attack_default", "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", "attack_default", "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