DramaticShapeVoxelMod archival mirror · read-only
eb69bc7db883d525a56b45db5efd8ae45853e097

a rev bump has to actually reach the machines it was bumped for

DramaticShape · 2026-08-08 · parent b05c7265d6

a rev bump has to actually reach the machines it was bumped for

It did not. pending() short-circuited on available(), and available() was
true for any checkout carrying assets/stadium -- so when REV went to 3 for
the shiny variants, such a machine was never pending, was never asked to
rebuild, and quietly went on serving the old set. ready() was false, so
readPack skipped the save-dir cache and read the shipped normal-only packs:
every shiny Pokemon drawn in its ordinary colours, with nothing on screen
saying why. A driver run sitting at "idle 0/151" for twelve thousand frames
is what surfaced it.

pending() is now keyed on ready(), not available(). A checkout with a ROM
spends one loading screen rebuilding a set it had files for; after that it is
current and never pending again. That was the cost the old short-circuit was
avoiding, and it is worth paying once to make a rev bump mean something.

The other half is the players who imported a ROM through the picker rather
than dropping it in baseroms/. beginFrom builds from the bytes and never
keeps them, so on a rev bump there is no ROM to rebuild from -- and with
available() keyed on ready(), the STADIUM rungs would have vanished off the
options row entirely. usable() now separates "these packs are readable" from
"these packs are current": format and count, deliberately not rev. Stale
packs keep the mode working and keep the rungs offered; only the recolour
waits for a rebuild. Losing the shinies until then is a blemish, losing the
mode is not.

readPack orders the two accordingly: a current cache always wins, a stale one
wins only when there is no shipped set to prefer instead -- so a cache from
an extractor rev we have since fixed cannot shadow good files, while a player
whose only copy IS that cache still gets Pokemon on the field.

Verified by rolling the marker back to rev 2 and launching: ready=false,
usable=true, available=true, pending=true, and the build runs unprompted.

Changed files

lib/StadiumInstall.lua | 42 +++++++++++++++++++++++++++++++++---------
 lib/StadiumPack.lua    | 16 ++++++++++++++--
 tests/shiny_shots.lua  |  9 +++++++++
 3 files changed, 56 insertions(+), 11 deletions(-)

Diff

diff --git a/lib/StadiumInstall.lua b/lib/StadiumInstall.lua
index d1ec135..d706437 100644
--- a/lib/StadiumInstall.lua
+++ b/lib/StadiumInstall.lua
@@ -189,22 +189,46 @@ local function shipped()
   return true
 end
 
--- Whether the STADIUM rungs can be offered at all: either the packs have been
--- built from the player's ROM, or the mod folder already carries a set.
+-- Whether the packs on disk can be READ, even if they are not current.
+--
+-- Format and count, but deliberately NOT rev. The distinction matters on an
+-- upgrade: a rev bump means the packs are out of date, not that they are
+-- unreadable, and treating the two the same is what would make the STADIUM
+-- rungs disappear off the options row for anyone whose cache predates it.
+-- Losing the recolour until a rebuild is a blemish; losing the mode is not.
+function StadiumInstall.usable()
+  local m = readMarker()
+  return (m ~= nil and m.format == StadiumInstall.FORMAT
+          and m.count == StadiumInstall.COUNT) and true or false
+end
+
+-- Whether the STADIUM rungs can be offered at all: the packs have been built
+-- from the player's ROM (current or merely readable), or the mod folder
+-- already carries a set.
 function StadiumInstall.available()
   if StadiumInstall.ready() then return true end
+  if StadiumInstall.usable() then return true end
   return shipped()
 end
 
--- Whether there is work to do: something to build from, and nothing usable
--- yet.
+-- Whether there is work to do: a ROM to build from, and no CURRENT set.
+--
+-- Keyed on ready() rather than available(), and that is the whole upgrade
+-- story. It used to short-circuit on available(), which meant a checkout
+-- carrying assets/stadium was never pending -- so when REV went to 3 for the
+-- shiny variants, such a machine did not rebuild, was not asked to, and
+-- quietly kept serving the old set: every shiny Pokemon drawn in its
+-- ordinary colours, with nothing on screen to say why. That is exactly what
+-- happened here, and it took a driver run sitting at "idle 0/151" to notice.
 --
--- A checkout that already carries a set is NOT pending. Building anyway would
--- be correct and would also mean a ten-second loading screen on the first run
--- of every checkout, to arrive at the files that were already sitting there.
+-- The cost this trades away is real and was the original reason: a checkout
+-- with a ROM now spends one loading screen rebuilding a set it already had
+-- files for. Once. After that ready() is true and it is not pending again --
+-- and what it buys is that a rev bump actually reaches the people it was
+-- bumped for.
 function StadiumInstall.pending()
-  if StadiumInstall.available() then return false end
-  return StadiumInstall.romPresent()
+  if not StadiumInstall.romPresent() then return false end
+  return not StadiumInstall.ready()
 end
 
 function StadiumInstall.forget()
diff --git a/lib/StadiumPack.lua b/lib/StadiumPack.lua
index d37041a..e958938 100644
--- a/lib/StadiumPack.lua
+++ b/lib/StadiumPack.lua
@@ -78,15 +78,27 @@ local function readPack(species, shiny)
   -- half-written folder must not be read at all. Required lazily: Install
   -- requires this module at load, so the reverse edge cannot be taken then.
   local rel = packName(StadiumPack.CACHE_DIR, species, shiny)
+  local install = V.require("StadiumInstall")
+  local mod = V.mod
+  local haveShipped = false
+  if mod and mod.read then
+    local okS, b = pcall(mod.read, mod, packName(StadiumPack.DIR, species, shiny))
+    haveShipped = okS and type(b) == "string" and #b > 4
+  end
+  -- A CURRENT cache always wins. A stale one (readable, but built by an older
+  -- extractor) wins only when there is no shipped set to prefer instead --
+  -- that ordering is what stops a cache from an extractor rev we have since
+  -- fixed shadowing good files, while still leaving something on screen for a
+  -- player whose only copy IS that cache. A half-written folder is caught by
+  -- the marker and satisfies neither.
   if love and love.filesystem and love.filesystem.getInfo
-     and V.require("StadiumInstall").ready() then
+     and (install.ready() or (install.usable() and not haveShipped)) then
     local okInfo, info = pcall(love.filesystem.getInfo, rel, "file")
     if okInfo and info then
       local ok, bytes = pcall(love.filesystem.read, rel)
       if ok and type(bytes) == "string" and #bytes > 4 then return bytes end
     end
   end
-  local mod = V.mod
   if not (mod and mod.read) then return nil end
   local ok, bytes = pcall(mod.read, mod,
                           packName(StadiumPack.DIR, species, shiny))
diff --git a/tests/shiny_shots.lua b/tests/shiny_shots.lua
index 7839842..50c6bd6 100644
--- a/tests/shiny_shots.lua
+++ b/tests/shiny_shots.lua
@@ -58,6 +58,15 @@ return function(game)
   -- unrecoloured Pokemon. Calling begin/step directly is both faster and
   -- honest about what is being tested, which is the extraction, not the
   -- screen that usually triggers it.
+  -- the upgrade question, asked before anything is built: with a stale
+  -- marker on disk, does this machine know it has work to do?
+  U.log(("upgrade check: ready=%s usable=%s available=%s pending=%s rom=%s")
+        :format(tostring(StadiumInstall.ready()),
+                tostring(StadiumInstall.usable()),
+                tostring(StadiumInstall.available()),
+                tostring(StadiumInstall.pending()),
+                tostring(StadiumInstall.romPresent())))
+
   if not StadiumInstall.ready() then
     local ok, err = StadiumInstall.begin()
     U.log(("stadium build: begin=%s %s"):format(tostring(ok), tostring(err or "")))