mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-25 06:55:29 +02:00
test(mod-api): gate Mew dock seams
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
-- Static privacy gate for the narrow Mew dock engine branch. It checks the
|
||||||
|
-- Git publication set, not ignored local imports: user ROMs and progress
|
||||||
|
-- saves may exist on a developer machine but must never become tracked files.
|
||||||
|
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
local T = require("tests.harness")
|
||||||
|
|
||||||
|
local pipe = io.popen("git ls-files 2>" .. (package.config:sub(1, 1) == "\\" and "nul" or "/dev/null"))
|
||||||
|
local paths = {}
|
||||||
|
if pipe then
|
||||||
|
for path in pipe:lines() do paths[#paths + 1] = path:gsub("\\", "/") end
|
||||||
|
pipe:close()
|
||||||
|
end
|
||||||
|
|
||||||
|
T.check(#paths > 100,
|
||||||
|
"privacy gate inspects a real Git publication set instead of passing vacuously")
|
||||||
|
|
||||||
|
local forbiddenExtensions = {
|
||||||
|
gb = true, gbc = true, gba = true, sav = true, srm = true,
|
||||||
|
rom = true, z64 = true, v64 = true, n64 = true, nds = true,
|
||||||
|
sfc = true, smc = true,
|
||||||
|
}
|
||||||
|
local forbiddenRuntimeRoots = {
|
||||||
|
["save.lua"] = true,
|
||||||
|
["save_blue.lua"] = true,
|
||||||
|
["save_yellow.lua"] = true,
|
||||||
|
["save_gold.lua"] = true,
|
||||||
|
["options.lua"] = true,
|
||||||
|
}
|
||||||
|
|
||||||
|
local binaryLeaks, runtimeLeaks = {}, {}
|
||||||
|
for _, path in ipairs(paths) do
|
||||||
|
local lower = path:lower()
|
||||||
|
local ext = lower:match("%.([^./\\]+)$")
|
||||||
|
if forbiddenExtensions[ext] then binaryLeaks[#binaryLeaks + 1] = path end
|
||||||
|
if forbiddenRuntimeRoots[lower]
|
||||||
|
or lower:match("^saves/")
|
||||||
|
or lower:match("^imports/")
|
||||||
|
or lower:match("^mods%-data/") then
|
||||||
|
runtimeLeaks[#runtimeLeaks + 1] = path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
T.eq(#binaryLeaks, 0,
|
||||||
|
"tracked publication contains no ROM/save binaries: " .. table.concat(binaryLeaks, ", "))
|
||||||
|
T.eq(#runtimeLeaks, 0,
|
||||||
|
"tracked publication contains no runtime save/import data: " .. table.concat(runtimeLeaks, ", "))
|
||||||
|
|
||||||
|
T.finish("mew dock private artifact gate")
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
-- Contract gate for the two narrow Gen 1 seams added for a composable
|
||||||
|
-- post-departure S.S. Anne dock mod. The suite is ROM-free: it drives the
|
||||||
|
-- real hook bus and WorldAPI against hand-written maps and save snapshots.
|
||||||
|
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
love = love or require("tests.love_stub")
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
local Hooks = require("src.mods.Hooks")
|
||||||
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
local WorldAPI = require("src.world.WorldAPI")
|
||||||
|
|
||||||
|
local oldHooks = Runtime.hooks
|
||||||
|
local oldTextBox = package.loaded["src.render.TextBox"]
|
||||||
|
|
||||||
|
package.loaded["src.render.TextBox"] = {
|
||||||
|
new = function(_, text, done) return { text = text, done = done } end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local story = dofile("data/scripts/story3.lua")
|
||||||
|
local VERSIONS = { "red", "blue", "yellow" }
|
||||||
|
|
||||||
|
local function newDock(version, wrappers)
|
||||||
|
local blocks, pushes, warps = {}, {}, {}
|
||||||
|
local rebuilds = 0
|
||||||
|
local save = {
|
||||||
|
version = version,
|
||||||
|
flags = { EVENT_SS_ANNE_LEFT = true },
|
||||||
|
marker = "save-must-not-change",
|
||||||
|
}
|
||||||
|
local map = {
|
||||||
|
id = "VERMILION_DOCK",
|
||||||
|
setBlock = function(_, bx, by, block)
|
||||||
|
blocks[bx .. "," .. by] = block
|
||||||
|
end,
|
||||||
|
renderer = { rebuild = function() rebuilds = rebuilds + 1 end },
|
||||||
|
}
|
||||||
|
local game = {
|
||||||
|
save = save,
|
||||||
|
data = { text = {
|
||||||
|
_VermilionCitySailor1ShipSetSailText = "The ship set sail.",
|
||||||
|
} },
|
||||||
|
stack = { push = function(_, value) pushes[#pushes + 1] = value end },
|
||||||
|
}
|
||||||
|
local ow = {
|
||||||
|
map = map,
|
||||||
|
player = { cellX = 14, cellY = 2, facing = "down" },
|
||||||
|
startWarpTo = function(_, ...)
|
||||||
|
warps[#warps + 1] = { ... }
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
local hooks = Hooks.new()
|
||||||
|
Runtime.hooks = hooks
|
||||||
|
for _, entry in ipairs(wrappers or {}) do
|
||||||
|
hooks:wrap("map.occupancy_allowed", entry.fn, entry.priority or 0,
|
||||||
|
entry.owner)
|
||||||
|
end
|
||||||
|
|
||||||
|
story.VERMILION_DOCK.onEnter(game, ow)
|
||||||
|
if pushes[1] and pushes[1].done then pushes[1].done() end
|
||||||
|
return {
|
||||||
|
blocks = blocks, pushes = pushes, warps = warps, rebuilds = rebuilds,
|
||||||
|
save = save, map = map, game = game, ow = ow, hooks = hooks,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function allowedWrapper(owner, inspect)
|
||||||
|
return {
|
||||||
|
owner = owner,
|
||||||
|
fn = function(nextFn, game, ctx)
|
||||||
|
if inspect then inspect(game, ctx) end
|
||||||
|
local downstream = nextFn(game, ctx)
|
||||||
|
local ownClaim = true
|
||||||
|
return downstream == true or ownClaim == true
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- With no subscriber, the post-departure branch remains byte-for-byte
|
||||||
|
-- vanilla in effect: erase the ship, show its line, and eject the player.
|
||||||
|
do
|
||||||
|
local run = newDock("red")
|
||||||
|
T.eq(run.rebuilds, 1, "vanilla re-entry rebuilds the erased dock")
|
||||||
|
T.eq(run.blocks["5,1"], 1, "vanilla re-entry erases the upper hull")
|
||||||
|
T.eq(run.blocks["8,2"], 13, "vanilla re-entry erases the lower hull")
|
||||||
|
T.eq(#run.pushes, 1, "vanilla re-entry shows the ship-set-sail line")
|
||||||
|
T.eq(run.pushes[1].text, "The ship set sail.",
|
||||||
|
"vanilla re-entry preserves its dialogue")
|
||||||
|
T.eq(run.warps[1] and run.warps[1][1], "VERMILION_CITY",
|
||||||
|
"vanilla re-entry ejects to Vermilion City")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The new permission seam is post-departure only. An ordinary HM01 exit
|
||||||
|
-- must not invoke it or change the existing departure-script path.
|
||||||
|
do
|
||||||
|
local hookCalls, queued = 0, nil
|
||||||
|
local hooks = Hooks.new()
|
||||||
|
Runtime.hooks = hooks
|
||||||
|
hooks:wrap("map.occupancy_allowed", function(nextFn, game, ctx)
|
||||||
|
hookCalls = hookCalls + 1
|
||||||
|
return nextFn(game, ctx)
|
||||||
|
end, 0, "must_not_run")
|
||||||
|
|
||||||
|
local savedMusic = package.loaded["src.core.Music"]
|
||||||
|
package.loaded["src.core.Music"] = {
|
||||||
|
stop = function() end,
|
||||||
|
play = function() end,
|
||||||
|
}
|
||||||
|
local game = {
|
||||||
|
save = { version = "red", flags = { EVENT_GOT_HM01 = true } },
|
||||||
|
data = {},
|
||||||
|
}
|
||||||
|
local ow = {
|
||||||
|
player = { cellX = 14, cellY = 2 },
|
||||||
|
startDustAnim = function(_, _, _, done) if done then done() end end,
|
||||||
|
queueScript = function(_, rows) queued = rows end,
|
||||||
|
}
|
||||||
|
story.VERMILION_DOCK.onEnter(game, ow)
|
||||||
|
package.loaded["src.core.Music"] = savedMusic
|
||||||
|
|
||||||
|
T.eq(hookCalls, 0, "normal HM01 departure never calls the occupancy seam")
|
||||||
|
T.eq(game.save.flags.EVENT_SS_ANNE_LEFT, true,
|
||||||
|
"normal HM01 departure still sets the vanilla event flag")
|
||||||
|
T.check(type(queued) == "table" and #queued > 0,
|
||||||
|
"normal HM01 departure still queues its sail-away script")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- One cooperative claimant may permit occupancy. The callback receives a
|
||||||
|
-- detached data snapshot, not the live overworld, map, player, or save.
|
||||||
|
do
|
||||||
|
local seenGame, seenCtx
|
||||||
|
local run = newDock("red", { allowedWrapper("mew_fixture", function(game, ctx)
|
||||||
|
seenGame = game
|
||||||
|
seenCtx = {
|
||||||
|
mapId = ctx.mapId, reason = ctx.reason, gameVersion = ctx.gameVersion,
|
||||||
|
x = ctx.x, y = ctx.y,
|
||||||
|
}
|
||||||
|
ctx.mapId, ctx.x, ctx.y = "MUTATED", -1, -1
|
||||||
|
end) })
|
||||||
|
T.eq(seenGame, run.game, "occupancy callback receives the live game explicitly")
|
||||||
|
T.same(seenCtx, {
|
||||||
|
mapId = "VERMILION_DOCK", reason = "ss_anne_departed",
|
||||||
|
gameVersion = "red", x = 14, y = 2,
|
||||||
|
}, "occupancy context is the exact detached Red dock snapshot")
|
||||||
|
T.eq(run.ow.map.id, "VERMILION_DOCK", "context mutation cannot change the map")
|
||||||
|
T.eq(run.ow.player.cellX, 14, "context mutation cannot change player X")
|
||||||
|
T.eq(run.ow.player.cellY, 2, "context mutation cannot change player Y")
|
||||||
|
T.eq(run.save.marker, "save-must-not-change", "permission check does not mutate save")
|
||||||
|
T.same(run.save, {
|
||||||
|
version = "red", flags = { EVENT_SS_ANNE_LEFT = true },
|
||||||
|
marker = "save-must-not-change",
|
||||||
|
}, "permission check preserves the complete save snapshot")
|
||||||
|
T.eq(#run.pushes, 0, "an exact true suppresses the vanilla rejection dialog")
|
||||||
|
T.eq(#run.warps, 0, "an exact true permits post-departure dock occupancy")
|
||||||
|
T.eq(run.blocks["5,1"], 1, "permitted occupancy still erases the departed ship")
|
||||||
|
T.eq(run.rebuilds, 1, "permitted occupancy still rebuilds the water layout")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Standard hook composition also means a non-cooperative false/no-next
|
||||||
|
-- wrapper can suppress downstream claims. This remains safe because false
|
||||||
|
-- is denial; it cannot accidentally grant occupancy.
|
||||||
|
do
|
||||||
|
local downstreamCalls = 0
|
||||||
|
local run = newDock("red", {
|
||||||
|
{
|
||||||
|
owner = "denier", priority = 10,
|
||||||
|
fn = function() return false end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
owner = "unreached_claimant", priority = 0,
|
||||||
|
fn = function()
|
||||||
|
downstreamCalls = downstreamCalls + 1
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
T.eq(downstreamCalls, 0, "no-next denial suppresses downstream by hook semantics")
|
||||||
|
T.eq(run.warps[1] and run.warps[1][1], "VERMILION_CITY",
|
||||||
|
"non-cooperative false remains fail-closed")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Cooperative peers all run through next(). A lower-priority peer claim is
|
||||||
|
-- preserved by a higher-priority peer that has no claim of its own.
|
||||||
|
do
|
||||||
|
local calls, contextIdentity = {}, nil
|
||||||
|
local run = newDock("blue", {
|
||||||
|
{
|
||||||
|
owner = "peer_high", priority = 10,
|
||||||
|
fn = function(nextFn, game, ctx)
|
||||||
|
calls[#calls + 1] = "high-before"
|
||||||
|
contextIdentity = ctx
|
||||||
|
local allowed = nextFn(game, ctx)
|
||||||
|
calls[#calls + 1] = "high-after"
|
||||||
|
return allowed == true or false
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
owner = "peer_low", priority = 0,
|
||||||
|
fn = function(nextFn, game, ctx)
|
||||||
|
calls[#calls + 1] = "low"
|
||||||
|
T.eq(ctx, contextIdentity, "peer wrappers share one detached snapshot instance")
|
||||||
|
local allowed = nextFn(game, ctx)
|
||||||
|
local ownClaim = true
|
||||||
|
return allowed == true or ownClaim == true
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
T.same(calls, { "high-before", "low", "high-after" },
|
||||||
|
"multiple peer handlers preserve hook-chain order")
|
||||||
|
T.eq(#run.warps, 0, "a cooperative peer claim survives the whole chain")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Absent, throwing, or malformed callbacks fail closed. Only boolean true
|
||||||
|
-- can turn off ejection; truthy strings/tables/numbers do not grant access.
|
||||||
|
do
|
||||||
|
local malformed = {
|
||||||
|
{ label = "nil", value = nil },
|
||||||
|
{ label = "false", value = false },
|
||||||
|
{ label = "string", value = "yes" },
|
||||||
|
{ label = "number", value = 1 },
|
||||||
|
{ label = "table", value = {} },
|
||||||
|
}
|
||||||
|
for _, case in ipairs(malformed) do
|
||||||
|
local run = newDock("red", { {
|
||||||
|
owner = "malformed_" .. case.label,
|
||||||
|
fn = function() return case.value end,
|
||||||
|
} })
|
||||||
|
T.eq(run.warps[1] and run.warps[1][1], "VERMILION_CITY",
|
||||||
|
"malformed " .. case.label .. " permission fails closed")
|
||||||
|
end
|
||||||
|
|
||||||
|
local beforeNext = newDock("red", { {
|
||||||
|
owner = "throws_before_next",
|
||||||
|
fn = function() error("fixture throws before next", 0) end,
|
||||||
|
} })
|
||||||
|
T.eq(beforeNext.warps[1] and beforeNext.warps[1][1], "VERMILION_CITY",
|
||||||
|
"throwing callback before next fails closed")
|
||||||
|
|
||||||
|
local afterNext = newDock("red", { {
|
||||||
|
owner = "throws_after_next",
|
||||||
|
fn = function(nextFn, game, ctx)
|
||||||
|
nextFn(game, ctx)
|
||||||
|
error("fixture throws after next", 0)
|
||||||
|
end,
|
||||||
|
} })
|
||||||
|
T.eq(afterNext.warps[1] and afterNext.warps[1][1], "VERMILION_CITY",
|
||||||
|
"throwing callback after next keeps the downstream denial")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The context carries one version only. A Red-only claimant must not leak
|
||||||
|
-- access into Blue or Yellow, and each call gets its own snapshot.
|
||||||
|
do
|
||||||
|
local contexts = {}
|
||||||
|
for _, version in ipairs(VERSIONS) do
|
||||||
|
local run = newDock(version, { {
|
||||||
|
owner = "red_only",
|
||||||
|
fn = function(nextFn, game, ctx)
|
||||||
|
contexts[#contexts + 1] = ctx
|
||||||
|
local downstream = nextFn(game, ctx)
|
||||||
|
return downstream == true or ctx.gameVersion == "red"
|
||||||
|
end,
|
||||||
|
} })
|
||||||
|
T.eq(#run.warps == 0, version == "red",
|
||||||
|
version .. " occupancy is decided only by its own version context")
|
||||||
|
end
|
||||||
|
T.eq(contexts[1].gameVersion, "red", "Red context stays Red")
|
||||||
|
T.eq(contexts[2].gameVersion, "blue", "Blue context stays Blue")
|
||||||
|
T.eq(contexts[3].gameVersion, "yellow", "Yellow context stays Yellow")
|
||||||
|
T.check(contexts[1] ~= contexts[2] and contexts[2] ~= contexts[3],
|
||||||
|
"Red, Blue, and Yellow calls do not share context tables")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Removing the owner is the engine's disable/uninstall path. It restores
|
||||||
|
-- vanilla denial immediately and leaves no save flag or serialized state.
|
||||||
|
do
|
||||||
|
local run = newDock("yellow", { allowedWrapper("removable") })
|
||||||
|
T.eq(#run.warps, 0, "installed owner may grant Yellow dock occupancy")
|
||||||
|
run.hooks:removeOwner("removable")
|
||||||
|
local pushes, warps = {}, {}
|
||||||
|
run.game.stack.push = function(_, value) pushes[#pushes + 1] = value end
|
||||||
|
run.ow.startWarpTo = function(_, ... ) warps[#warps + 1] = { ... } end
|
||||||
|
story.VERMILION_DOCK.onEnter(run.game, run.ow)
|
||||||
|
if pushes[1] and pushes[1].done then pushes[1].done() end
|
||||||
|
T.eq(warps[1] and warps[1][1], "VERMILION_CITY",
|
||||||
|
"disabling the owner restores vanilla ejection")
|
||||||
|
T.eq(run.hooks.chains["map.occupancy_allowed"], nil,
|
||||||
|
"uninstall removes the occupancy chain itself")
|
||||||
|
T.eq(run.save.marker, "save-must-not-change",
|
||||||
|
"disable/uninstall writes no persistent permission state")
|
||||||
|
T.same(run.save, {
|
||||||
|
version = "yellow", flags = { EVENT_SS_ANNE_LEFT = true },
|
||||||
|
marker = "save-must-not-change",
|
||||||
|
}, "disable/uninstall preserves the complete Yellow save snapshot")
|
||||||
|
end
|
||||||
|
|
||||||
|
-- activeBlockAt is read-only and fail-closed. A successful call exposes
|
||||||
|
-- only one scalar from the active runtime layout, never its backing table.
|
||||||
|
local function blockApi(version, blockAt)
|
||||||
|
local backing = { 4, 5, 6, 8, 9, 10 }
|
||||||
|
local map = {
|
||||||
|
id = "VERMILION_DOCK",
|
||||||
|
def = { width = 3, height = 2, blocks = backing },
|
||||||
|
blockAt = blockAt or function(_, bx, by)
|
||||||
|
return backing[by * 3 + bx + 1]
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
local world = { isOverworld = true, map = map }
|
||||||
|
local game = {
|
||||||
|
save = { version = version },
|
||||||
|
stack = { states = { world } },
|
||||||
|
overworld = world,
|
||||||
|
}
|
||||||
|
return WorldAPI.new(game, "contract_fixture"), backing, game, map
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
for _, version in ipairs(VERSIONS) do
|
||||||
|
local api, backing = blockApi(version)
|
||||||
|
local block, err = api:activeBlockAt("VERMILION_DOCK", 1, 0)
|
||||||
|
T.eq(block, 5, version .. " reads its active dock block")
|
||||||
|
T.eq(err, nil, version .. " valid active block has no error")
|
||||||
|
block = 99
|
||||||
|
T.eq(backing[2], 5, version .. " scalar result cannot mutate the map")
|
||||||
|
end
|
||||||
|
|
||||||
|
local api = blockApi("red")
|
||||||
|
local wrong, wrongErr = api:activeBlockAt("VERMILION_CITY", 1, 0)
|
||||||
|
T.eq(wrong, nil, "wrong map has no block result")
|
||||||
|
T.eq(wrongErr, "map is not active", "wrong map fails closed explicitly")
|
||||||
|
|
||||||
|
local invalid = {
|
||||||
|
{ "nil x", nil, 0 }, { "string x", "1", 0 }, { "table x", {}, 0 },
|
||||||
|
{ "fraction x", 0.5, 0 }, { "negative infinity x", -math.huge, 0 },
|
||||||
|
{ "infinity y", 0, math.huge }, { "NaN y", 0, 0 / 0 },
|
||||||
|
}
|
||||||
|
for _, case in ipairs(invalid) do
|
||||||
|
local value, err = api:activeBlockAt("VERMILION_DOCK", case[2], case[3])
|
||||||
|
T.eq(value, nil, case[1] .. " returns no block")
|
||||||
|
T.eq(err, "invalid block coordinates", case[1] .. " is rejected by type")
|
||||||
|
end
|
||||||
|
for _, coords in ipairs({ { -1, 0 }, { 0, -1 }, { 3, 0 }, { 0, 2 } }) do
|
||||||
|
local value, err = api:activeBlockAt("VERMILION_DOCK", coords[1], coords[2])
|
||||||
|
T.eq(value, nil, "out-of-bounds coordinate returns no block")
|
||||||
|
T.eq(err, "block coordinates out of bounds", "bounds fail closed explicitly")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local malformed = {
|
||||||
|
{ label = "nil", get = function() return nil end },
|
||||||
|
{ label = "negative", get = function() return -1 end },
|
||||||
|
{ label = "fractional", get = function() return 1.5 end },
|
||||||
|
{ label = "infinite", get = function() return math.huge end },
|
||||||
|
{ label = "NaN", get = function() return 0 / 0 end },
|
||||||
|
{ label = "string", get = function() return "4" end },
|
||||||
|
{ label = "table", get = function() return {} end },
|
||||||
|
{ label = "throwing", get = function() error("bad map", 0) end },
|
||||||
|
}
|
||||||
|
for _, case in ipairs(malformed) do
|
||||||
|
local api = blockApi("red", case.get)
|
||||||
|
local block, err = api:activeBlockAt("VERMILION_DOCK", 0, 0)
|
||||||
|
T.eq(block, nil, "malformed active block " .. case.label .. " returns no value")
|
||||||
|
T.eq(err, "block unavailable",
|
||||||
|
"malformed active block " .. case.label .. " fails closed")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Invalid map shapes are untrusted runtime data too. None may escape as a
|
||||||
|
-- block or raise through the mod facade.
|
||||||
|
do
|
||||||
|
local badDefs = {
|
||||||
|
{ label = "missing def", value = nil },
|
||||||
|
{ label = "missing width", value = { height = 2, blocks = {} } },
|
||||||
|
{ label = "string width", value = { width = "3", height = 2, blocks = {} } },
|
||||||
|
{ label = "fractional width", value = { width = 1.5, height = 2, blocks = {} } },
|
||||||
|
{ label = "nonpositive width", value = { width = 0, height = 2, blocks = {} } },
|
||||||
|
{ label = "infinite height", value = { width = 3, height = math.huge, blocks = {} } },
|
||||||
|
{ label = "missing blocks", value = { width = 3, height = 2 } },
|
||||||
|
{ label = "scalar blocks", value = { width = 3, height = 2, blocks = 4 } },
|
||||||
|
}
|
||||||
|
for _, case in ipairs(badDefs) do
|
||||||
|
local api, _, _, map = blockApi("red")
|
||||||
|
map.def = case.value
|
||||||
|
local block, err = api:activeBlockAt("VERMILION_DOCK", 0, 0)
|
||||||
|
T.eq(block, nil, case.label .. " returns no block")
|
||||||
|
T.eq(err, "block unavailable", case.label .. " fails closed")
|
||||||
|
end
|
||||||
|
|
||||||
|
local api, _, _, map = blockApi("red")
|
||||||
|
map.blockAt = nil
|
||||||
|
local block, err = api:activeBlockAt("VERMILION_DOCK", 0, 0)
|
||||||
|
T.eq(block, nil, "missing blockAt returns no block")
|
||||||
|
T.eq(err, "block unavailable", "missing blockAt fails closed")
|
||||||
|
map.blockAt = "not a function"
|
||||||
|
block, err = api:activeBlockAt("VERMILION_DOCK", 0, 0)
|
||||||
|
T.eq(block, nil, "malformed blockAt returns no block")
|
||||||
|
T.eq(err, "block unavailable", "malformed blockAt fails closed")
|
||||||
|
|
||||||
|
api, _, _, map = blockApi("red")
|
||||||
|
map.def.blocks = {}
|
||||||
|
block, err = api:activeBlockAt("VERMILION_DOCK", 0, 0)
|
||||||
|
T.eq(block, nil, "sparse stored block slot returns no block")
|
||||||
|
T.eq(err, "block unavailable", "sparse stored block slot fails closed")
|
||||||
|
|
||||||
|
api, _, _, map = blockApi("red")
|
||||||
|
map.def.blocks[1] = "4"
|
||||||
|
block, err = api:activeBlockAt("VERMILION_DOCK", 0, 0)
|
||||||
|
T.eq(block, nil, "malformed stored block slot returns no block")
|
||||||
|
T.eq(err, "block unavailable", "malformed stored block slot fails closed")
|
||||||
|
|
||||||
|
api, _, _, map = blockApi("red", function() return 5 end)
|
||||||
|
block, err = api:activeBlockAt("VERMILION_DOCK", 0, 0)
|
||||||
|
T.eq(block, nil, "stored/accessor mismatch returns no block")
|
||||||
|
T.eq(err, "block unavailable", "stored/accessor mismatch fails closed")
|
||||||
|
end
|
||||||
|
|
||||||
|
do
|
||||||
|
local api = WorldAPI.new({ stack = { states = {} } }, "contract_fixture")
|
||||||
|
local block, err = api:activeBlockAt("VERMILION_DOCK", 0, 0)
|
||||||
|
T.eq(block, nil, "no-overworld lookup returns no block")
|
||||||
|
T.eq(err, "no overworld", "no-overworld lookup reports its state")
|
||||||
|
end
|
||||||
|
|
||||||
|
Runtime.hooks = oldHooks
|
||||||
|
package.loaded["src.render.TextBox"] = oldTextBox
|
||||||
|
T.finish("mew dock seam contract")
|
||||||
Reference in New Issue
Block a user