fix(mod-api): fail closed on malformed map blocks

This commit is contained in:
Bo Layer
2026-08-23 03:18:17 -06:00
parent 0147e3d87b
commit c9221daa90
3 changed files with 27 additions and 7 deletions
+9 -2
View File
@@ -272,12 +272,19 @@ mod.hooks:wrap("map.occupancy_allowed", function(next, game, ctx)
end) end)
``` ```
With no wrapper, the hook allocates no context and vanilla behavior is byte-for- With no wrapper, the hook allocates no context and vanilla behavior is
byte unchanged. A throwing wrapper is isolated by the normal hook bus. A nil, unchanged. A throwing wrapper is isolated by the normal hook bus. A nil,
string, number, table, or other malformed final answer fails closed. Disabling string, number, table, or other malformed final answer fails closed. Disabling
or uninstalling the permitting mod therefore restores vanilla ejection without or uninstalling the permitting mod therefore restores vanilla ejection without
changing the S.S. Anne story flag or restoring the ship. changing the S.S. Anne story flag or restoring the ship.
Normal hook-chain ownership applies: a wrapper that does not call `next`
intentionally owns the final answer and does not run lower-priority wrappers.
Permission wrappers must call `next` as shown above to compose. A noncompliant
wrapper that returns false without calling `next` safely denies occupancy and
can suppress downstream permission by this standard rule. A malformed answer
also fails closed and cannot force occupancy.
## Party ordering ## Party ordering
Companion UIs and alternate party screens can call Companion UIs and alternate party screens can call
@@ -56,6 +56,12 @@ true when either downstream or its own narrow rule permits occupancy. The hook
does not replace `MapScripts` registration, merging, or dispatch, and does not does not replace `MapScripts` registration, merging, or dispatch, and does not
change the departure flag or reconstruct the ship. change the departure flag or reconstruct the ship.
As with every wrapper hook, a callback that does not call `next` intentionally
owns the final answer and does not run lower-priority callbacks. Permission
wrappers must call downstream to compose. A false, non-forwarding wrapper
safely denies occupancy and can suppress downstream permission by this normal
rule. A malformed final result also fails closed and cannot permit occupancy.
The call is guarded by `Runtime.wantsHook`, so an empty chain allocates no The call is guarded by `Runtime.wantsHook`, so an empty chain allocates no
context and follows the prior branch exactly. context and follows the prior branch exactly.
@@ -108,7 +114,8 @@ The parity gate must prove:
- one permission wrapper can allow occupancy without removing the ship-erasure - one permission wrapper can allow occupancy without removing the ship-erasure
work or any map handler; work or any map handler;
- multiple cooperative wrappers preserve downstream permission; - multiple cooperative wrappers preserve downstream permission;
- absent, throwing, nil, false, and malformed hook answers fail closed; - absent, throwing, nil, false, malformed, and non-forwarding hook answers fail
closed according to the normal wrapper-chain rule;
- Red, Blue, and Yellow contexts keep their version identity separate; - Red, Blue, and Yellow contexts keep their version identity separate;
- disabling or removing the owner restores vanilla behavior without a save - disabling or removing the owner restores vanilla behavior without a save
migration; migration;
+10 -4
View File
@@ -136,12 +136,18 @@ function WorldAPI:activeBlockAt(mapId, bx, by)
return nil, "invalid block coordinates" return nil, "invalid block coordinates"
end end
local def = map.def local def = map.def
if not def or type(def.width) ~= "number" or type(def.height) ~= "number" if not def or not validBlockCoordinate(def.width) or def.width <= 0
or bx < 0 or by < 0 or bx >= def.width or by >= def.height then or not validBlockCoordinate(def.height) or def.height <= 0 then
return nil, "block unavailable"
end
if bx < 0 or by < 0 or bx >= def.width or by >= def.height then
return nil, "block coordinates out of bounds" return nil, "block coordinates out of bounds"
end end
local blockId = map:blockAt(bx, by) if type(def.blocks) ~= "table" or type(map.blockAt) ~= "function" then
if not validBlockCoordinate(blockId) or blockId < 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
return nil, "block unavailable" return nil, "block unavailable"
end end
return blockId return blockId