Files
2026-08-04 10:43:35 -04:00

116 lines
4.4 KiB
Lua

-- Probe: read the .dsm packs back with the mod's OWN reader and print what
-- it made of them, so the Lua side can be diffed against the packer's
-- --report output. Headless -- no love.graphics, no meshes.
--
-- luajit mods/DramaticShapeVoxelMod/tests/stadium_pack_probe.lua [dex ...]
--
-- Run from the repository root.
package.path = "./?.lua;./?/init.lua;" .. package.path
local ROOT = "mods/DramaticShapeVoxelMod"
-- the mod namespace lib/ modules expect (see main.lua), with just enough of
-- it for the two files under test
local V = {}
local modules = {}
V.mod = {
log = {
warn = function(_, fmt, ...) print("[warn] " .. fmt:format(...)) end,
info = function(_, fmt, ...) print("[info] " .. fmt:format(...)) end,
},
read = function(_, rel)
local fp = io.open(ROOT .. "/" .. rel, "rb")
if not fp then return nil end
local bytes = fp:read("*a")
fp:close()
return bytes
end,
}
function V.require(name)
if modules[name] then return modules[name] end
local chunk = assert(loadfile(ROOT .. "/lib/" .. name .. ".lua"))
modules[name] = chunk(V)
return modules[name]
end
-- StadiumRig pulls in Voxel3D, which needs love; the pose walk itself does
-- not, so the require is stubbed out to the one field it reads.
modules.Voxel3D = { FORMAT = {} }
local Pack = V.require("StadiumPack")
local Rig = V.require("StadiumRig")
local want = {}
for i = 1, #arg do want[#want + 1] = tonumber(arg[i]) end
if #want == 0 then want = { 25, 6, 130, 95, 50, 10, 143, 92, 41, 73 } end
print(("%-4s %-6s %6s %6s %6s %8s %8s %8s %8s")
:format("dex", "bones", "prims", "anims", "tex", "root", "height",
"floor", "radius"))
for _, dex in ipairs(want) do
local m = Pack.load(dex)
if not m then
print(("%-4d -- did not load"):format(dex))
else
print(("%-4d %-6d %6d %6d %6d %8.4f %8.2f %8.2f %8.2f")
:format(dex, m.boneCount, m.primCount, m.animCount, m.texCount,
m.rootScale, m.height, m.floor, m.radius))
-- the bind pose, walked by the REAL rig code on a bare instance (no
-- meshes, so no graphics context needed), measured the same way
-- tools/stadium_pack.py measures it. Agreement here means the byte
-- format, the bone tree, the rotation basis and the two-chain scale
-- split all survived the trip into Lua.
local rig = setmetatable({
model = m, pivotM = {}, drawM = {}, accX = {}, accY = {}, accZ = {},
parts = {},
}, Rig)
local function boxAt(anim, frame, wrap)
rig:pose(anim, frame, wrap)
local drw = rig.drawM
local lo, hi = math.huge, -math.huge
for _, prim in ipairs(m.prims) do
for k = 1, prim.vertCount do
local o = (prim.bone[k] - 1) * 12
local x, y, z = prim.px[k], prim.py[k], prim.pz[k]
local wy = drw[o + 5] * x + drw[o + 6] * y + drw[o + 7] * z
+ drw[o + 8]
wy = wy * m.rootScale
if wy < lo then lo = wy end
if wy > hi then hi = wy end
end
end
return hi - lo, lo
end
local h, f = boxAt(nil, 0, false)
print((" bind pose walked in Lua: height %8.2f floor %8.2f "
.. "(header says %8.2f / %8.2f)%s")
:format(h, f, m.height, m.floor,
(math.abs(h - m.height) < 0.5
and math.abs(f - m.floor) < 0.5) and " OK" or " MISMATCH"))
-- the animation tables, decoded lazily like the game does it
local idle = m.ctx[Pack.SLOT.idle]
local names = {}
for i, a in ipairs(m.anims) do names[i] = a.name end
print((" anims: %s"):format(table.concat(names, " ")))
print((" idle slot -> %s, entrance -> %s, faint -> %s")
:format(tostring(idle), tostring(m.ctx[Pack.SLOT.entrance]),
tostring(m.ctx[Pack.SLOT.faint])))
local tracks = Pack.tracks(m, (idle or 0) + 1)
local animated = 0
for b = 1, m.boneCount do if tracks and tracks[b] then animated = animated + 1 end end
print((" idle: %d frames, %d/%d bones animated")
:format(m.anims[(idle or 0) + 1].frames, animated, m.boneCount))
-- the POSED extent, which is what actually gets drawn. Compare against
-- tools/stadium_pack.py's anim_sample, which was checked frame for
-- frame against the reference glTF export.
for _, fr in ipairs({ 0, 1, 5 }) do
local ah, af = boxAt((idle or 0) + 1, fr, true)
print((" idle frame %d: y %8.2f .. %8.2f"):format(fr, af, af + ah))
end
end
end