fix some issues, fail gracefully
This commit is contained in:
+65
-11
@@ -115,6 +115,10 @@ end
|
||||
function Stadium.begin(arena)
|
||||
Stadium.finish()
|
||||
if not Stadium.enabled() then return false end
|
||||
-- a new fight gets its own first complaint: `reported` is a one-shot so the
|
||||
-- console is not filled sixty times a second, but latched for the whole
|
||||
-- process it would swallow every failure after the first one ever
|
||||
Stadium.reported = false
|
||||
session = {
|
||||
arena = arena,
|
||||
groundY = 0,
|
||||
@@ -345,6 +349,13 @@ function Stadium.update(dt, battle, groundY)
|
||||
end
|
||||
|
||||
mon:setSpecies(dex)
|
||||
-- and tell the pack cache this one is standing there, every frame. Its
|
||||
-- eviction order is keyed on LOADS, and a side only loads when its
|
||||
-- species changes -- so without this a Pokemon that has been out for a
|
||||
-- few turns is the least recently loaded thing in the cache and gets its
|
||||
-- textures released out from under it the moment a fifth species enters
|
||||
-- the battle (see StadiumPack.keep).
|
||||
if mon.species then StadiumPack.keep(mon.species) end
|
||||
mon.visible = (mon.rig ~= nil) and onField(battle, side, mon)
|
||||
and not (battler and battler.substituteHP)
|
||||
-- cleared up front, so a side that has just lost its rig cannot leave
|
||||
@@ -361,10 +372,16 @@ function Stadium.update(dt, battle, groundY)
|
||||
local cell = arena[side]
|
||||
local other = arena[side == "player" and "enemy" or "player"]
|
||||
if cell and other then
|
||||
mon.model_matrix = mon:matrix(cell[1], session.groundY, cell[2],
|
||||
other[1] - cell[1],
|
||||
other[2] - cell[2])
|
||||
mon:build()
|
||||
-- posed and skinned inside the same guard the draws use: this is
|
||||
-- where a bad track or a released texture is first touched, and a
|
||||
-- throw here would take the OTHER side's update with it (the
|
||||
-- caller wraps this whole function in one pcall)
|
||||
Stadium.guard(side, mon, "build", function()
|
||||
mon.model_matrix = mon:matrix(cell[1], session.groundY, cell[2],
|
||||
other[1] - cell[1],
|
||||
other[2] - cell[2])
|
||||
mon:build()
|
||||
end)
|
||||
else
|
||||
mon.model_matrix = nil
|
||||
end
|
||||
@@ -384,12 +401,46 @@ end
|
||||
-- flash are all already set. StadiumRig turns the wireframe and the glass
|
||||
-- mask off around its own draws and puts them back.
|
||||
|
||||
-- ------- one model going wrong is not both
|
||||
--
|
||||
-- These two draws used to be a bare loop inside the caller's single pcall,
|
||||
-- which had two consequences and both were bad. A throw on the FIRST side
|
||||
-- skipped the second, so one broken Pokemon took its opponent off the screen
|
||||
-- with it. And nothing recorded that it had happened, so the same throw came
|
||||
-- back every frame for the rest of the fight -- the mode's own fallback (that
|
||||
-- side draws its flat pic instead) was sitting right there and never reached,
|
||||
-- because falling back needs somebody to decide the model is not working.
|
||||
--
|
||||
-- So each side is drawn inside its own pcall, and a side that throws is
|
||||
-- RETIRED: its rig is released, which is exactly the state a species with no
|
||||
-- pack is in, and OverworldBattle renders a billboard for it from the next
|
||||
-- frame on. The fight carries on with a flat Pokemon instead of a missing
|
||||
-- one, which is the difference the player actually sees.
|
||||
-- On the TABLE rather than a local, because Stadium.update calls it and sits
|
||||
-- above this line: a local would still be nil there.
|
||||
function Stadium.guard(side, mon, what, fn)
|
||||
local ok, err = pcall(fn)
|
||||
if ok then return true end
|
||||
Stadium.report(err)
|
||||
-- release rather than merely hide: the rig holds meshes and texture
|
||||
-- references, and whatever went wrong with them is not going to be better
|
||||
-- next frame. setSpecies rebuilds from scratch if this Pokemon is sent out
|
||||
-- again later.
|
||||
if mon.rig then pcall(mon.release, mon) end
|
||||
mon.rig, mon.visible, mon.model_matrix = nil, false, nil
|
||||
if session then session.broken = session.broken or {} end
|
||||
if session then session.broken[side] = what end
|
||||
return false
|
||||
end
|
||||
|
||||
function Stadium.draw(pull)
|
||||
if not session then return end
|
||||
for _, side in ipairs({ "enemy", "player" }) do
|
||||
local mon = session[side]
|
||||
if mon.rig and mon.visible and mon.model_matrix then
|
||||
mon.rig:draw(mon.model_matrix, pull)
|
||||
Stadium.guard(side, mon, "draw", function()
|
||||
mon.rig:draw(mon.model_matrix, pull)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -402,7 +453,9 @@ function Stadium.cast(shadowMap)
|
||||
for _, side in ipairs({ "enemy", "player" }) do
|
||||
local mon = session[side]
|
||||
if mon.rig and mon.visible and mon.model_matrix then
|
||||
mon.rig:caster(shadowMap, mon.model_matrix)
|
||||
Stadium.guard(side, mon, "cast", function()
|
||||
mon.rig:caster(shadowMap, mon.model_matrix)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -561,13 +614,14 @@ end
|
||||
-- model is indistinguishable from an invisible one -- so the first failure
|
||||
-- of a battle says so, once, and the rest of the fight carries on without
|
||||
-- it.
|
||||
local reported = false
|
||||
Stadium.reported = false
|
||||
|
||||
function Stadium.report(err)
|
||||
if reported then return end
|
||||
reported = true
|
||||
V.mod.log:warn("stadium: a model failed to draw: %s -- this battle runs "
|
||||
.. "without it", tostring(err))
|
||||
if Stadium.reported then return end
|
||||
Stadium.reported = true
|
||||
V.mod.log:warn("stadium: a model failed and was retired for this battle: "
|
||||
.. "%s -- that Pokemon falls back to its flat battle pic, "
|
||||
.. "and its opponent is unaffected", tostring(err))
|
||||
end
|
||||
|
||||
-- DS_STADIUM_DEBUG=1 prints what each side resolved to once a second, which
|
||||
|
||||
@@ -106,6 +106,18 @@ StadiumMon.HOVER_CAP = 0.5
|
||||
-- same rate.
|
||||
StadiumMon.FPS = StadiumPack.FPS
|
||||
|
||||
-- How far an animation may carry the Pokemon off its tile, in the Pokemon's
|
||||
-- own body-heights, before the excess is taken back out (StadiumRig.anchor).
|
||||
--
|
||||
-- Measured against the frame rather than chosen by eye. A mon is drawn
|
||||
-- REF_HEIGHT world pixels tall and the GB frame holds about 38 world pixels
|
||||
-- at the far cell, with the foe's feet on row 56 of 144 -- so there is
|
||||
-- roughly one body-height of room above it and about one and a half either
|
||||
-- side. Three quarters of a height keeps every part of a travelling Pokemon
|
||||
-- inside that with a margin, and leaves the 83 species that never reach it
|
||||
-- untouched.
|
||||
StadiumMon.TRAVEL = 0.75
|
||||
|
||||
-- ------- the animation the fight is asking for
|
||||
--
|
||||
-- Each entry says which context slot to look up, whether it loops, and
|
||||
@@ -378,6 +390,10 @@ function StadiumMon:build()
|
||||
-- self.anim is nil while a species has nothing to play, and pose() reads
|
||||
-- that as "the bind pose", which is exactly what is wanted
|
||||
self.rig:pose(self.anim, self.time * StadiumMon.FPS, self.loop)
|
||||
-- and then back onto the tile, because these animations were authored for
|
||||
-- a camera that followed the Pokemon and this one does not move (see
|
||||
-- StadiumRig.anchor)
|
||||
self.rig:anchor(StadiumMon.TRAVEL)
|
||||
self.rig:skin(self.yaw or 0)
|
||||
-- no clock of its own: the texture animation rides the frame pose() just
|
||||
-- resolved, which is what keeps a blink inside its standby loop and a
|
||||
|
||||
@@ -477,11 +477,38 @@ local function touch(species)
|
||||
if slot.image and slot.image.release then
|
||||
pcall(slot.image.release, slot.image)
|
||||
end
|
||||
-- CLEARED, not just released. A released Image is still a truthy
|
||||
-- value, and `image()` below hands back whatever is in this field
|
||||
-- without looking at it -- so leaving the corpse here meant the next
|
||||
-- ask returned a dead object, which reached mesh:setTexture and threw
|
||||
-- "Cannot use object after it has been released" from inside the
|
||||
-- scene pass. Nil means the next ask decodes it again, which is the
|
||||
-- whole point of the slot being lazy.
|
||||
slot.image = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Say that this species is IN USE, so the cache does not evict it.
|
||||
--
|
||||
-- The eviction order above is a least-recently-LOADED list, not a
|
||||
-- least-recently-used one: `touch` runs from `load`, and `load` is only
|
||||
-- reached when a side's species CHANGES (StadiumMon.setSpecies returns early
|
||||
-- otherwise). A Pokemon that stands on the field for several turns therefore
|
||||
-- never refreshes its position, drifts to the front of the queue, and is
|
||||
-- evicted -- its textures released -- while it is still being drawn sixty
|
||||
-- times a second. That is what a fifth species entering a battle did: call
|
||||
-- out a Clefairy and whatever had been standing longest lost its textures
|
||||
-- mid-fight.
|
||||
--
|
||||
-- So the mode says, every frame, which two species are actually standing
|
||||
-- there (see Stadium.update). With KEEP at 4 and two sides, the two in use
|
||||
-- are always the two most recent and cannot reach the front of the queue.
|
||||
function StadiumPack.keep(species)
|
||||
if species and cache[species] then touch(species) end
|
||||
end
|
||||
|
||||
-- Whether a pack for this species is on disk at all. Cheap enough to ask
|
||||
-- before a battle commits to the mode, and the honest test: a mod
|
||||
-- installed without its assets folder must decline rather than error.
|
||||
|
||||
@@ -110,6 +110,9 @@ function StadiumRig.new(model)
|
||||
-- what the pose walk last answered, so a frame that neither moved the
|
||||
-- animation nor turned the model can skip the whole thing
|
||||
poseKey = nil,
|
||||
-- scratch for the body-centre estimate (see anchor), kept on the rig so
|
||||
-- a per-frame measurement allocates nothing
|
||||
cx = {}, cy = {}, cz = {},
|
||||
}, StadiumRig)
|
||||
|
||||
-- One mesh per primitive: a primitive is already "the triangles sharing
|
||||
@@ -131,6 +134,9 @@ function StadiumRig.new(model)
|
||||
pcall(mesh.setVertexMap, mesh, prim.index)
|
||||
self.parts[i] = { mesh = mesh, rows = rows, prim = prim }
|
||||
end
|
||||
-- the spot the animations are measured against, taken while there is no
|
||||
-- pose to overwrite (see measureBind)
|
||||
pcall(self.measureBind, self)
|
||||
return self
|
||||
end
|
||||
|
||||
@@ -402,6 +408,118 @@ function StadiumRig:pose(anim, frame, wrap)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------- keeping the Pokemon on its own tile
|
||||
--
|
||||
-- Stadium's animations MOVE the Pokemon, and they move it a long way. Half
|
||||
-- the set's send-out entrances walk the body more than its own height off
|
||||
-- the spot it started on; Dewgong's faint travels nearly ten body-heights,
|
||||
-- and its entrance seven and a half. Every one of them ends exactly where it
|
||||
-- began, because that game framed each Pokemon with a camera of its OWN that
|
||||
-- followed the performance around a stage.
|
||||
--
|
||||
-- This mode has one camera, solved to put two named map cells at two fixed
|
||||
-- points in a 160x144 frame (BattleCam), and a Pokemon that travels seven
|
||||
-- body-heights out of that frame is simply GONE -- which is what sending out
|
||||
-- a Farfetch'd looked like: an empty tile for three and a half seconds,
|
||||
-- while its animation played somewhere off to the left of the shot.
|
||||
--
|
||||
-- So the bulk travel is taken back out. The pose is measured, and whatever
|
||||
-- has carried the body further than `limit` from where the bind pose put it
|
||||
-- is subtracted from every bone.
|
||||
--
|
||||
-- ------- why a LIMIT and not an anchor
|
||||
--
|
||||
-- Pinning the body outright would flatten the animations into mime: a lunge,
|
||||
-- a hop, a recoil and a collapse are all the body moving, and they are the
|
||||
-- part worth having. What breaks the shot is not motion, it is EXCURSION --
|
||||
-- and the two are told apart by how far. Inside the limit nothing is touched
|
||||
-- at all, so the 83 species whose animations stay put are bit-for-bit what
|
||||
-- they were; past it the excess alone is removed, so a big move still reads
|
||||
-- as big and still comes back to the tile it left.
|
||||
--
|
||||
-- ------- and why the MEDIAN bone
|
||||
--
|
||||
-- The centre is the median bone origin on each axis, not the mean and not
|
||||
-- the root. The mean is dragged by exactly the thing that must not count --
|
||||
-- Farfetch'd's five-bone trail streaks three thousand units out while the
|
||||
-- bird stays put -- and the root is a bone like any other, which several
|
||||
-- species animate independently of the body hanging off it. The median is
|
||||
-- the position most of the skeleton agrees on, and a handful of bones flung
|
||||
-- anywhere cannot move it.
|
||||
|
||||
-- The body centre of the pose currently in drawM.
|
||||
local function centre(self, n)
|
||||
-- made on demand as well as in new(), so the probes and the QA sweep --
|
||||
-- which build a rig with no meshes by hand, because pose() needs none --
|
||||
-- can measure without having to know about this scratch
|
||||
local xs, ys, zs = self.cx, self.cy, self.cz
|
||||
if not xs then
|
||||
xs, ys, zs = {}, {}, {}
|
||||
self.cx, self.cy, self.cz = xs, ys, zs
|
||||
end
|
||||
local d = self.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
|
||||
for i = n + 1, #xs do xs[i], ys[i], zs[i] = nil, nil, nil end
|
||||
table.sort(xs) table.sort(ys) table.sort(zs)
|
||||
local h = floor(n / 2) + 1
|
||||
return xs[h], ys[h], zs[h]
|
||||
end
|
||||
|
||||
-- Where the BIND pose puts it -- the spot every animation is measured
|
||||
-- against. Cached on the shared MODEL, because it is a fact about the model
|
||||
-- and not about this instance of it.
|
||||
--
|
||||
-- Called once, from new(), and deliberately not lazily from anchor(): taking
|
||||
-- this measurement means POSING the bind pose, which would overwrite the
|
||||
-- animated pose anchor() was called to correct. Doing it while the rig is
|
||||
-- still being built is the one moment there is no pose to lose.
|
||||
function StadiumRig:measureBind()
|
||||
local model = self.model
|
||||
if model.bindCX then return end
|
||||
self:pose(nil, 0, false)
|
||||
model.bindCX, model.bindCY, model.bindCZ = centre(self, model.boneCount)
|
||||
end
|
||||
|
||||
-- Pull the pose back toward the tile. `limit` is in the Pokemon's own
|
||||
-- body-heights; nil or a non-positive value leaves the pose exactly as posed.
|
||||
function StadiumRig:anchor(limit)
|
||||
if not (limit and limit > 0) then return end
|
||||
local model = self.model
|
||||
local n = model.boneCount
|
||||
-- the vertices are in RAW units, before the model_root scale that
|
||||
-- model.height is measured after
|
||||
local root = model.rootScale
|
||||
if not (root and root > 0) then root = 1 end
|
||||
local h = (model.height or 0) / root
|
||||
if not (h > 0) then return end
|
||||
|
||||
local bx, by, bz = model.bindCX, model.bindCY, model.bindCZ
|
||||
if not bx then return end -- never measured; leave the pose alone
|
||||
local x, y, z = centre(self, n)
|
||||
local dx, dy, dz = x - bx, y - by, z - bz
|
||||
local dist = (dx * dx + dy * dy + dz * dz) ^ 0.5
|
||||
local allow = limit * h
|
||||
if dist <= allow or dist <= 0 then return end
|
||||
|
||||
-- the EXCESS only: what is inside the limit stays, so the motion keeps its
|
||||
-- shape and only the part that would leave the frame is removed
|
||||
local k = (dist - allow) / dist
|
||||
local ox, oy, oz = dx * k, dy * k, dz * k
|
||||
local pivot, drw = self.pivotM, self.drawM
|
||||
for b = 1, n do
|
||||
local o = (b - 1) * 12
|
||||
pivot[o + 4] = pivot[o + 4] - ox
|
||||
pivot[o + 8] = pivot[o + 8] - oy
|
||||
pivot[o + 12] = pivot[o + 12] - oz
|
||||
drw[o + 4] = drw[o + 4] - ox
|
||||
drw[o + 8] = drw[o + 8] - oy
|
||||
drw[o + 12] = drw[o + 12] - oz
|
||||
end
|
||||
end
|
||||
|
||||
-- ------- the skin
|
||||
--
|
||||
-- Every vertex through its one bone's draw matrix, and its normal through
|
||||
|
||||
Reference in New Issue
Block a user