mirror of
https://github.com/DramaticShape/DramaticShapeVoxelMod.git
synced 2026-08-12 09:10:49 +02:00
fix some issues, fail gracefully
This commit is contained in:
@@ -1361,6 +1361,98 @@ end)()
|
||||
"and a whole frame is that frame exactly, with nothing blended into it")
|
||||
end)()
|
||||
|
||||
-- ------- the pack cache must not evict a Pokemon that is standing there
|
||||
--
|
||||
-- The eviction order is keyed on LOADS, and a side only loads when its
|
||||
-- species changes -- so a Pokemon that has been out for a few turns is the
|
||||
-- least recently loaded thing in the cache. A fifth species entering the
|
||||
-- battle evicted it and RELEASED ITS TEXTURES mid-fight, and the next draw
|
||||
-- threw "Cannot use object after it has been released" from inside the scene
|
||||
-- pass, which took both models off the screen for the rest of the battle.
|
||||
;(function()
|
||||
if not HAVE_STADIUM_PACKS then return end
|
||||
local Pack = run.loader.exports.DRAMATIC_SHAPE.lib.require("StadiumPack")
|
||||
local keep = Pack.KEEP
|
||||
Pack.forget()
|
||||
Pack.KEEP = 2
|
||||
|
||||
local held = Pack.load(1)
|
||||
T.check(held ~= nil, "a model loads")
|
||||
local slot = held.textures and held.textures[1]
|
||||
T.check(slot ~= nil, "and carries at least one texture slot")
|
||||
|
||||
-- more species than the cache holds, WITHOUT saying the first is in use
|
||||
Pack.load(4) Pack.load(7) Pack.load(10)
|
||||
T.eq(slot.image, nil,
|
||||
"an evicted model's texture slot is CLEARED, not left holding a released "
|
||||
.. "object -- a released Image is still truthy, so the corpse came back "
|
||||
.. "out of image() and died at mesh:setTexture")
|
||||
|
||||
-- and with `keep` said every frame, as the mode does, it is never evicted
|
||||
Pack.forget()
|
||||
local live = Pack.load(1)
|
||||
for _, dex in ipairs({ 4, 7, 10, 13 }) do
|
||||
Pack.keep(1)
|
||||
Pack.load(dex)
|
||||
end
|
||||
T.eq(Pack.load(1), live,
|
||||
"a species the mode keeps saying is on the field is still the same "
|
||||
.. "cached model after four others have loaded past it")
|
||||
|
||||
Pack.KEEP = keep
|
||||
Pack.forget()
|
||||
end)()
|
||||
|
||||
-- ------- and an animation must not walk the Pokemon out of the shot
|
||||
--
|
||||
-- Stadium's animations were authored for a camera that followed the Pokemon;
|
||||
-- this one holds two fixed cells. 65 of the 148 send-out entrances travel
|
||||
-- more than a body-height off the spot, up to seven and a half -- which is
|
||||
-- not drama here, it is an empty tile.
|
||||
;(function()
|
||||
if not HAVE_STADIUM_PACKS then return end
|
||||
local lib = run.loader.exports.DRAMATIC_SHAPE.lib
|
||||
local Pack, Rig, Mon = lib.require("StadiumPack"), lib.require("StadiumRig"),
|
||||
lib.require("StadiumMon")
|
||||
local model = Pack.load(87) -- Dewgong, the worst of them
|
||||
local rig = setmetatable({ model = model, pivotM = {}, drawM = {},
|
||||
accX = {}, accY = {}, accZ = {}, parts = {} }, Rig)
|
||||
rig:measureBind()
|
||||
|
||||
local function centre()
|
||||
local n, xs, ys, zs = model.boneCount, {}, {}, {}
|
||||
for b = 1, n do
|
||||
local o = (b - 1) * 12
|
||||
xs[b], ys[b], zs[b] = rig.drawM[o + 4], rig.drawM[o + 8], rig.drawM[o + 12]
|
||||
end
|
||||
table.sort(xs) table.sort(ys) table.sort(zs)
|
||||
local h = math.floor(n / 2) + 1
|
||||
return xs[h], ys[h], zs[h]
|
||||
end
|
||||
local raw = model.height / (model.rootScale > 0 and model.rootScale or 1)
|
||||
local slot = model.ctx[Pack.SLOT.entrance]
|
||||
local anim = slot + 1
|
||||
|
||||
local function driftAt(frame, limit)
|
||||
rig:pose(anim, frame, false)
|
||||
rig:anchor(limit)
|
||||
local x, y, z = centre()
|
||||
return (((x - model.bindCX) ^ 2 + (y - model.bindCY) ^ 2
|
||||
+ (z - model.bindCZ) ^ 2) ^ 0.5) / raw
|
||||
end
|
||||
|
||||
T.check(driftAt(40, nil) > 5,
|
||||
"unanchored, Dewgong's entrance carries it more than five body-heights "
|
||||
.. "off its tile -- straight out of a frame that holds about one")
|
||||
T.check(driftAt(40, Mon.TRAVEL) <= Mon.TRAVEL * 1.001,
|
||||
"anchored, it stays inside the travel limit")
|
||||
-- and the animations that never travel are left completely alone
|
||||
local before = driftAt(0, nil)
|
||||
T.eq(driftAt(0, Mon.TRAVEL), before,
|
||||
"a frame already inside the limit is not moved at all -- the anchor takes "
|
||||
.. "out the EXCESS, so a lunge is still a lunge")
|
||||
end)()
|
||||
|
||||
-- ------- the three species the extraction cannot read stand as PICS
|
||||
--
|
||||
-- Exeggutor, Tangela and Magmar come out of the ROM with standby loops that
|
||||
|
||||
+106
-11
@@ -73,6 +73,19 @@ local STEP = tonumber(args.step or "0.5")
|
||||
-- hundreds of units off the body, which comes out in the dozens.
|
||||
local EXPLODE = 6.0
|
||||
|
||||
-- The texture index of a primitive the source says has no texture. The pack
|
||||
-- stores indices one-based, so the packer's 0xFFFF sentinel arrives as this.
|
||||
local UNTEXTURED = 0xFFFF + 1
|
||||
|
||||
-- species -> true, for the ones that carry such a primitive. Counted rather
|
||||
-- than reported (see the texture check).
|
||||
local untextured = {}
|
||||
|
||||
-- How far the mode lets an animation carry the Pokemon off its tile, in body
|
||||
-- heights. Read from StadiumMon rather than repeated, so the sweep cannot go
|
||||
-- on passing against a number the mode has since changed.
|
||||
local TRAVEL = nil
|
||||
|
||||
-- ------- the harness
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
@@ -195,6 +208,7 @@ end }
|
||||
local Pack = V.require("StadiumPack")
|
||||
local Rig = V.require("StadiumRig")
|
||||
local Mon = V.require("StadiumMon")
|
||||
TRAVEL = Mon.TRAVEL
|
||||
|
||||
-- ------- findings
|
||||
|
||||
@@ -212,6 +226,26 @@ end
|
||||
|
||||
-- ------- one animation, frame by frame
|
||||
|
||||
-- Where the BODY of a posed rig is: the median bone origin on each axis.
|
||||
--
|
||||
-- The median rather than the mean or the root, for the reason StadiumRig's
|
||||
-- own anchor uses it -- Farfetch'd's five-bone trail streaks three thousand
|
||||
-- units out while the bird stays where it is, and a mean would follow the
|
||||
-- trail. This is deliberately a second, independent implementation of the
|
||||
-- same idea: a check that shared the code it is checking would agree with it
|
||||
-- by construction.
|
||||
local function bodyCentreOf(rig)
|
||||
local n = rig.model.boneCount
|
||||
local xs, ys, zs, d = {}, {}, {}, rig.drawM
|
||||
for b = 1, n do
|
||||
local o = (b - 1) * 12
|
||||
xs[b], ys[b], zs[b] = d[o + 4], d[o + 8], d[o + 12]
|
||||
end
|
||||
table.sort(xs) table.sort(ys) table.sort(zs)
|
||||
local h = math.floor(n / 2) + 1
|
||||
return xs[h], ys[h], zs[h]
|
||||
end
|
||||
|
||||
local function bboxOf(rig)
|
||||
local lo, hi = math.huge, -math.huge
|
||||
local bad = false
|
||||
@@ -258,10 +292,18 @@ local function sweepSpecies(dex)
|
||||
end
|
||||
|
||||
-- the bind pose, as the yardstick every posed frame is measured against
|
||||
rig:measureBind()
|
||||
rig:pose(nil, 0, false)
|
||||
rig:skin(0)
|
||||
local bind = bboxOf(rig)
|
||||
if not (bind > 0) then bind = 1 end
|
||||
-- and where the bind pose puts the BODY, for the travel check below. The
|
||||
-- tracks are in raw units, before the model_root scale model.height is
|
||||
-- measured after.
|
||||
local bcx, bcy, bcz = bodyCentreOf(rig)
|
||||
local rawHeight = (model.height or 0)
|
||||
/ ((model.rootScale or 0) > 0 and model.rootScale or 1)
|
||||
if not (rawHeight > 0) then rawHeight = 1 end
|
||||
|
||||
local steps = 0
|
||||
for index, anim in ipairs(model.anims) do
|
||||
@@ -302,6 +344,28 @@ local function sweepSpecies(dex)
|
||||
report("threw while playing", dex, name, f, tostring(err))
|
||||
break
|
||||
end
|
||||
-- ------- does it stay on its tile?
|
||||
--
|
||||
-- Stadium's animations were authored for a camera that FOLLOWED the
|
||||
-- Pokemon around a stage; this mode's camera is solved to hold two
|
||||
-- fixed map cells and does not move. So an animation that walks the
|
||||
-- body several of its own heights away does not look dramatic here,
|
||||
-- it looks like the Pokemon is missing -- which is what sending out
|
||||
-- a Farfetch'd did for three and a half seconds.
|
||||
--
|
||||
-- StadiumRig.anchor takes the excess back out, and this is the check
|
||||
-- that it did: measured AFTER the anchor, the same way the mode
|
||||
-- draws it, so what is reported is what a player would actually see.
|
||||
rig:anchor(TRAVEL)
|
||||
local cx, cy, cz = bodyCentreOf(rig)
|
||||
local drift = (((cx - bcx) ^ 2 + (cy - bcy) ^ 2 + (cz - bcz) ^ 2) ^ 0.5)
|
||||
/ rawHeight
|
||||
if drift > TRAVEL * 1.05 then
|
||||
report("animation walks the Pokemon off its tile", dex, name, f,
|
||||
("body centre %.1f body-heights from where it started")
|
||||
:format(drift))
|
||||
end
|
||||
|
||||
local h, bad = bboxOf(rig)
|
||||
if bad then
|
||||
report("posed vertex is NaN or infinite", dex, name, f, "")
|
||||
@@ -310,13 +374,29 @@ local function sweepSpecies(dex)
|
||||
("%.0f units tall against a %.0f-unit bind pose (%.1fx)")
|
||||
:format(h, bind, h / bind))
|
||||
end
|
||||
-- every piece of the Pokemon has to have a texture to be drawn with;
|
||||
-- one that resolves to nothing is a limb that is simply not there
|
||||
-- Every piece of the Pokemon has to have a texture to be drawn with,
|
||||
-- and one that resolves to nothing is a limb that is simply not
|
||||
-- there -- EXCEPT where the source says it has none.
|
||||
--
|
||||
-- StadiumPack stores the texture index one-based (`u16 + 1`), so the
|
||||
-- packer's 0xFFFF "this primitive is untextured" sentinel arrives
|
||||
-- here as 65536. That is 39 primitives across 37 species, 1.6% of the
|
||||
-- set's vertices, and every one of them has all-zero UVs -- they are
|
||||
-- flat-shaded geometry in the original, not a texture that went
|
||||
-- missing. Reported as a finding they were 104,728 lines of noise
|
||||
-- (one per prim per sampled frame) burying two real bugs.
|
||||
--
|
||||
-- Counted rather than dropped: "how much of this set is untextured"
|
||||
-- is worth knowing, and a number that suddenly moves is worth seeing.
|
||||
for i, part in ipairs(rig.parts) do
|
||||
if not part.texture then
|
||||
report("part has no texture", dex, name, f,
|
||||
("prim %d wants texture %s of %d")
|
||||
:format(i, tostring(part.prim.tex), model.texCount or 0))
|
||||
if part.prim.tex == UNTEXTURED then
|
||||
untextured[dex] = true
|
||||
else
|
||||
report("part has no texture", dex, name, f,
|
||||
("prim %d wants texture %s of %d")
|
||||
:format(i, tostring(part.prim.tex), model.texCount or 0))
|
||||
end
|
||||
end
|
||||
end
|
||||
f = f + STEP
|
||||
@@ -395,12 +475,21 @@ local started = os.clock()
|
||||
local steps = 0
|
||||
local staticPose = {}
|
||||
for _, dex in ipairs(list) do
|
||||
local ok, err = pcall(function() steps = steps + sweepSpecies(dex) end)
|
||||
if not ok then
|
||||
report("the sweep itself threw", dex, nil, nil, tostring(err))
|
||||
end
|
||||
local m = Pack.load(dex)
|
||||
if m and m.staticPose then staticPose[#staticPose + 1] = dex end
|
||||
-- SKIPPED, not swept. The packer measures each species' standby loop
|
||||
-- against its own bind pose and flags the ones whose animation data is
|
||||
-- corrupt at source, and StadiumMon declines those outright -- they stand
|
||||
-- as flat battle pics and no frame of them is ever posed in a game. Sweeping
|
||||
-- them anyway produced 356 of the 362 "pose flies apart" findings, which is
|
||||
-- a report that is mostly about Pokemon this mode does not draw.
|
||||
if m and m.staticPose then
|
||||
staticPose[#staticPose + 1] = dex
|
||||
else
|
||||
local ok, err = pcall(function() steps = steps + sweepSpecies(dex) end)
|
||||
if not ok then
|
||||
report("the sweep itself threw", dex, nil, nil, tostring(err))
|
||||
end
|
||||
end
|
||||
if not args.quiet and dex % 10 == 0 then
|
||||
io.write((" ... %d/%d %.0f MB\n")
|
||||
:format(dex, #list, collectgarbage("count") / 1024))
|
||||
@@ -453,8 +542,14 @@ print("")
|
||||
print(("stadium animation QA: %d species, %d posed frames, %.1fs")
|
||||
:format(#list, steps, elapsed))
|
||||
print(("packs: %s"):format(packDir))
|
||||
local nUntextured = 0
|
||||
for _ in pairs(untextured) do nUntextured = nUntextured + 1 end
|
||||
if nUntextured > 0 then
|
||||
print(("species with flat-shaded (untextured) primitives: %d -- expected, "
|
||||
.. "the source has no texture for those"):format(nUntextured))
|
||||
end
|
||||
if #staticPose > 0 then
|
||||
print(("staticPose (declined by the packer, never drawn): %s")
|
||||
print(("staticPose (declined by the packer, never drawn, not swept): %s")
|
||||
:format(table.concat(staticPose, " ")))
|
||||
end
|
||||
print("")
|
||||
|
||||
Reference in New Issue
Block a user