diff --git a/docs/modding.md b/docs/modding.md index 73c8d853..8687c7a2 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -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 diff --git a/docs/rfcs/0013-map-occupancy-and-active-block.md b/docs/rfcs/0013-map-occupancy-and-active-block.md index 1066ddc3..cfb796db 100644 --- a/docs/rfcs/0013-map-occupancy-and-active-block.md +++ b/docs/rfcs/0013-map-occupancy-and-active-block.md @@ -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; diff --git a/src/world/WorldAPI.lua b/src/world/WorldAPI.lua index a7abd4f3..e204d635 100644 --- a/src/world/WorldAPI.lua +++ b/src/world/WorldAPI.lua @@ -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