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)
```
With no wrapper, the hook allocates no context and vanilla behavior is byte-for-
byte unchanged. A throwing wrapper is isolated by the normal hook bus. A nil,
With no wrapper, the hook allocates no context and vanilla behavior is
unchanged. A throwing wrapper is isolated by the normal hook bus. A nil,
string, number, table, or other malformed final answer fails closed. Disabling
or uninstalling the permitting mod therefore restores vanilla ejection without
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
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
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
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
work or any map handler;
- 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;
- disabling or removing the owner restores vanilla behavior without a save
migration;
+10 -4
View File
@@ -136,12 +136,18 @@ function WorldAPI:activeBlockAt(mapId, bx, by)
return nil, "invalid block coordinates"
end
local def = map.def
if not def or type(def.width) ~= "number" or type(def.height) ~= "number"
or bx < 0 or by < 0 or bx >= def.width or by >= def.height then
if not def or not validBlockCoordinate(def.width) or def.width <= 0
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"
end
local blockId = map:blockAt(bx, by)
if not validBlockCoordinate(blockId) or blockId < 0 then
if type(def.blocks) ~= "table" or type(map.blockAt) ~= "function" 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"
end
return blockId