From d17f0725d80481e47b74ea406a4c4bccbd574295 Mon Sep 17 00:00:00 2001 From: Bo Layer Date: Sun, 23 Aug 2026 03:20:43 -0600 Subject: [PATCH] fix(mod-api): validate active block storage --- data/scripts/story3.lua | 4 ++-- docs/modding.md | 3 +++ docs/rfcs/0013-map-occupancy-and-active-block.md | 4 ++++ src/world/WorldAPI.lua | 7 ++++++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/data/scripts/story3.lua b/data/scripts/story3.lua index 787b0d99..826a7f96 100644 --- a/data/scripts/story3.lua +++ b/data/scripts/story3.lua @@ -913,8 +913,8 @@ M.VERMILION_DOCK = { -- the ship is long gone: erase her right away, and anyone who -- still lands here is sent back out past the guard unless a mod -- explicitly permits this occupied map state. This hook surrounds - -- only the ejection decision; the map's complete onEnter chain has - -- already run and the departed ship remains erased. + -- only the ejection decision; map-script registration and dispatch + -- stay unchanged, and the departed ship remains erased. for _, b in ipairs(DOCK_SHIP_BLOCKS) do ow.map:setBlock(b.bx, b.by, b.water) end diff --git a/docs/modding.md b/docs/modding.md index 8687c7a2..ab0ba7e9 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -247,6 +247,9 @@ or malformed active block returns `nil, "block unavailable"`. The caller must require every expected cell to match before changing presentation. This method is Gen 1-only; Gold callers receive no parity promise for it. +The same unavailable result covers missing or sparse active block storage and +an accessor result that does not match its validated active block slot. + ### Conditional map occupancy `map.occupancy_allowed` is a narrow Gen 1 hook around a map script's vanilla diff --git a/docs/rfcs/0013-map-occupancy-and-active-block.md b/docs/rfcs/0013-map-occupancy-and-active-block.md index cfb796db..64e6aa04 100644 --- a/docs/rfcs/0013-map-occupancy-and-active-block.md +++ b/docs/rfcs/0013-map-occupancy-and-active-block.md @@ -89,6 +89,10 @@ Failure reasons are stable: | Coordinate is negative or outside the active map | `block coordinates out of bounds` | | Active block data is absent or malformed | `block unavailable` | +The block slot and the active map accessor must both contain the same valid +nonnegative integer. Missing or sparse storage and inconsistent accessor data +return `block unavailable`. + Requiring the expected map ID and rejecting all ambiguous input lets a mod compare every cell in its version-specific signature before it calls an existing mutation API. Red, Blue, and Yellow each use their own loaded map diff --git a/src/world/WorldAPI.lua b/src/world/WorldAPI.lua index e204d635..bdab92a3 100644 --- a/src/world/WorldAPI.lua +++ b/src/world/WorldAPI.lua @@ -146,8 +146,13 @@ function WorldAPI:activeBlockAt(mapId, bx, by) if type(def.blocks) ~= "table" or type(map.blockAt) ~= "function" then return nil, "block unavailable" end + local stored = def.blocks[by * def.width + bx + 1] + if not validBlockCoordinate(stored) or stored < 0 then + return nil, "block unavailable" + end local ok, blockId = pcall(map.blockAt, map, bx, by) - if not ok or not validBlockCoordinate(blockId) or blockId < 0 then + if not ok or not validBlockCoordinate(blockId) or blockId < 0 + or blockId ~= stored then return nil, "block unavailable" end return blockId