-- Gate for the Gen 1 module facades a gen2compat mod's require resolves to -- (src/mods/Gen2Compat.lua). -- -- The rules this file holds, all of them load-bearing for a Gen 1 follower mod -- running on Gold: -- * every served name hands back one stable table, and the ones backed by a -- Gen 2 module ARE that module, so a monkey-patch lands where Gold runs; -- * the Game facade is a live proxy, not a snapshot, and aliases the two -- names Gold spells differently (overworld / writeOptions) plus the one -- data table that was renamed (sprites); -- * src/world/gen2/Follower.lua keeps a file-local named exactly -- `shouldSpawn`, shared by update and onMapEntered, because that upvalue -- NAME is what three separate follower mods rewrite through -- debug.setupvalue; -- * World:step ticks the follower and the overworld facade unconditionally, -- and World:interact dispatches through the facade when one is installed. -- -- ROM-free: the world here is hand-built, the way tests/gen2_world_test.lua -- builds one. package.path = "./?.lua;./?/init.lua;" .. package.path love = love or require("tests.love_stub") local S = require("tests.harness").suite("gen2 mod facade") local check, eq = S.check, S.eq local Gen2Compat = require("src.mods.Gen2Compat") local Follower = require("src.world.gen2.Follower") local Gen2Map = require("src.world.gen2.Map") local Gen2Npc = require("src.world.gen2.Npc") local Player = require("src.world.gen2.Player") local World = require("src.world.gen2.World") -- ------- 1. every served name resolves, once, to a table local SERVED = { "src.core.Game", "src.world.NPC", "src.world.Collision", "src.world.FieldDefaults", "src.pokemon.Boxes", "src.world.OverworldController", "src.world.PikachuFollower", "src.world.Map", "src.world.WorldAPI", "src.ui.PartyMenu", "src.ui.BoxMenu", "src.ui.StartMenu", "src.ui.OptionsMenu", "src.battle.BattleState", "src.script.ScriptRunner", } for _, name in ipairs(SERVED) do check(Gen2Compat.serves(name), "served: " .. name) local a = Gen2Compat.resolve(name, "fixture") eq(type(a), "table", "resolves to a table: " .. name) check(a == Gen2Compat.resolve(name, "fixture"), "one stable table for the run: " .. name) end -- the aliased ones ARE the Gen 2 module, so a mod's patch lands on the table -- Gold runs rather than on a copy eq(Gen2Compat.resolve("src.world.PikachuFollower"), Follower, "PikachuFollower is src/world/gen2/Follower.lua itself") eq(Gen2Compat.resolve("src.world.Map"), Gen2Map, "Map is the Gen 2 Map") eq(Gen2Compat.resolve("src.world.NPC"), Gen2Npc, "NPC is the Gen 2 NPC, so getmetatable(npc) == the module the mod required") -- Gen 1's BoxMenu is Bill's PC TOP MENU; Gold's counterpart is PcMenu, and -- the Gen 2 BoxMenu is the withdraw/deposit LIST Gen 1 builds inline. Aimed -- at the wrong one, a mod appending a row appends it to an object with no -- row list at all. eq(Gen2Compat.resolve("src.ui.BoxMenu"), require("src.ui.gen2.PcMenu"), "BoxMenu is Gold's PC top menu, not its box list") -- Gold has no BattleState.newWild, and inventing one that took a species and a -- level would be the silent wrong answer this whole layer exists to avoid. check(Gen2Compat.resolve("src.battle.BattleState").newWild == nil, "no invented newWild on the Gen 2 battle screen") -- ------- 1b. the coverage table, which the modkit checker reads eq(Gen2Compat.COVERAGE_VERSION, 1, "the coverage contract is versioned") local names = Gen2Compat.modules() eq(#names, #SERVED, "every served name is in the coverage listing") for _, name in ipairs(names) do local row = Gen2Compat.coverage(name) check(row ~= nil, "coverage for " .. name) eq(row.module, name, "coverage names itself: " .. name) check(row.kind == "facade" or row.kind == "alias", "coverage kind is facade or alias: " .. name) for member, status in pairs(row.members) do check(status == "backed" or status == "warned" or status == "absent", ("%s.%s carries one of the three statuses"):format(name, member)) end end eq(Gen2Compat.coverage("src.pokemon.Boxes").kind, "facade", "Boxes is a facade over the Gen 2 module, not an alias") eq(Gen2Compat.memberStatus("src.pokemon.Boxes", "deposit"), "backed", "the deposit override is published as backed") eq(Gen2Compat.memberStatus("src.battle.BattleState", "makeSafari"), "absent", "makeSafari is published absent, which is what the wilds mod probes for") eq(Gen2Compat.memberStatus("src.world.Collision", "load"), "warned", "Collision.load is present, answers nil and says so") eq(Gen2Compat.coverage("nope.nope"), nil, "an unserved name has no coverage") -- a table per call, so a consumer cannot mutate the adapter's own record local first = Gen2Compat.coverage("src.world.Collision") first.members.canMove = "absent" eq(Gen2Compat.memberStatus("src.world.Collision", "canMove"), "backed", "coverage hands back a fresh table each call") -- ------- 2. the Game facade proxies a LIVE game local persisted = 0 local liveGame = { world = { tag = "the world" }, save = { party = {} }, stack = { tag = "the stack" }, data = { gen2Sprites = { SPRITE_CHRIS = { id = "SPRITE_CHRIS" } }, gen2Maps = { TEST_MAP = { id = "TEST_MAP" } }, gen2Constants = { specialOrder = {} }, pokemon = { PIDGEY = { name = "PIDGEY" } } }, persistOptions = function() persisted = persisted + 1 end, } local current = nil Gen2Compat.bind(function() return current end) local Game = Gen2Compat.resolve("src.core.Game", "fixture") eq(Game.save, nil, "captured before a game exists, the facade reads nil") current = liveGame eq(Game.save, liveGame.save, "and fills in the moment one is wired") eq(Game.overworld, liveGame.world, "overworld is the Gen 1 name for .world") eq(Game.stack, liveGame.stack, "the stack is the real one, so a patch of " .. "stack.push reaches the engine") eq(Game.data.pokemon.PIDGEY.name, "PIDGEY", "data forwards") eq(Game.data.sprites, liveGame.data.gen2Sprites, "data.sprites is the Gen 1 name for gen2Sprites") eq(Game.data.field, nil, "data.field has no Gen 2 backing and says so") Game.writeOptions(Game) eq(persisted, 1, "writeOptions is persistOptions, called with the live game") Game._fixtureStamp = true eq(liveGame._fixtureStamp, true, "a stamp written through the facade lands on " .. "the live game") -- the four Gen 1 members Gold has no counterpart for are NAMED, not answered eq(Game.renderer, nil, "Game.renderer is absent, not the real Renderer: half " .. "its surface would answer and the other half silently no-op") eq(Game.load, nil, "Game:load is absent; calling it would re-run Gold's boot") eq(Game.bootConfig, nil, "Game:bootConfig is absent") eq(Game.makeTitleState, nil, "Game:makeTitleState is absent") eq(Game.data.constants, nil, "data.constants is NOT routed to gen2Constants, " .. "which is the cart's ordered name lists and a different thing entirely") eq(Game.data.maps, liveGame.data.gen2Maps, "data.maps is the Gen 1 name") eq(Game.logicSpeed(), 1, "logicSpeed never returns below 1 on Gold") eq(type(Game.fixedStep), "table", "fixedStep is the shared singleton") -- ------- 3. NPC: the Gen 1 constructor shape, a Gen 2 entity out local NPCFacade = Gen2Compat.resolve("src.world.NPC", "fixture") local sheet = "assets/fixture/follower.png" local sprites = { SPRITE_PIKACHU = { id = "SPRITE_PIKACHU", image = sheet, frames = 6, walker = true } } local trailer = NPCFacade.new({ gen2Sprites = sprites }, "TEST_MAP", { index = 241, name = "TRAILER_1", sprite = "SPRITE_PIKACHU", movement = "STAY", range = "NONE", x = 3, y = 4, }) eq(getmetatable(trailer), Gen2Npc, "the trailer is a real Gen 2 NPC, so the " .. "Gen 2 draw list poses it") eq(trailer.cellX, 3, "cell carried over") eq(trailer.spriteId, "SPRITE_PIKACHU", "Gen 1's spriteId stamp is kept") check(not trailer.fixedFacing, "STAY must not map to STILL: a trailer that " .. "cannot turn is not a follower") eq(trailer.kind, "stand", "and it never wanders off on its own") eq(type(trailer.pose), "function", "Gen 1's pose contract exists to be wrapped") -- ------- 4. Collision, Boxes, the map vocabulary local Collision = Gen2Compat.resolve("src.world.Collision", "fixture") eq(Collision.DELTA, Gen2Map.DELTA, "one DELTA table, not a copy") local tx, ty = Collision.target(2, 2, "right") eq(tx, 3, "target x") eq(ty, 2, "target y") trailer.passable = true eq(Collision.occupied({ trailer }, 3, 4, nil), nil, "a passable entity never blocks a step") local Boxes = Gen2Compat.resolve("src.pokemon.Boxes", "fixture") local save = { currentBox = 2 } local boxes = Boxes.ensure(save) check(save.boxes ~= nil, "ensure MATERIALISES save.boxes, because Gen 1's " .. "callers index it straight afterwards") eq(#boxes, require("src.core.gen2.Boxes").NUM_BOXES, "all of Gold's boxes") eq(Boxes.active(save), save.boxes[2], "active is the current box") -- COUNT / CAPACITY used to be missing outright, so `for i = 1, Boxes.COUNT` -- raised "'for' limit must be a number" eq(Boxes.COUNT, require("src.core.gen2.Boxes").NUM_BOXES, "Boxes.COUNT") eq(Boxes.CAPACITY, require("src.core.gen2.Boxes").MONS_PER_BOX, "Boxes.CAPACITY") -- the inherited Gen 2 deposit(save, partyIndex, boxIndex) would index -- save.party with a MON TABLE, return false plus "There is no POKeMON there." -- and drop the mon -- which reads exactly like "every box is full" local mon = { species = "PIDGEY" } eq(Boxes.deposit(save, mon), 2, "deposit takes a MON and answers a box number") eq(save.boxes[2][1], mon, "and the mon is in the save, not in a detached table") -- ensure clamps currentBox, which is load bearing: Boxes2.box hands back a -- fresh DETACHED table for an index outside 1..NUM_BOXES local wild = { currentBox = 99 } local wildBoxes = Boxes.ensure(wild) eq(wild.currentBox, Boxes.COUNT, "ensure clamps currentBox into range") eq(Boxes.active(wild), wildBoxes[Boxes.COUNT], "so active is a box in the save") eq(type(Gen2Map.isCounterCell), "function", "Map:isCounterCell exists") eq(type(Gen2Map.warpAtCell), "function", "Map:warpAtCell exists") check(Gen2Map.isOutside({ environment = "TOWN" }), "a town is outside") check(not Gen2Map.isOutside({ environment = "INDOOR" }), "a house is not") eq(type(Player.facingCell), "function", "Player:facingCell exists") -- the Gen 1 module-level statics, which a facade could not have served -- because a mod calls them on world.map for _, name in ipairs({ "blockAt", "setBlock", "tileAt", "isDoorTileCell", "isWarpTileCell", "signAtCell" }) do eq(type(Gen2Map[name]), "function", "Map:" .. name .. " exists") end check(Gen2Map.isOutdoor({ environment = "ROUTE" }), "a route is outdoor") check(Gen2Map.inRegion({ id = "GOLDENROD_CITY" }, nil, "GOLDENROD"), "inRegion falls back to the id prefix, which is honest on either cache") -- a sprite-name test would answer false for every real boulder on Gold check(Gen2Map.isPushable({ movement = 0x19 }), "STRENGTH_BOULDER is pushable") check(not Gen2Map.isPushable({ sprite = "SPRITE_BOULDER" }), "and the Gen 1 sprite name is not what decides it") -- absent, not answered: Gold's fly points are landmark spawns and its ghost -- battles are Kanto content eq(Gen2Map.isFlyTown, nil, "Map.isFlyTown stays absent on Gold") eq(Gen2Map.ghostBattles, nil, "Map.ghostBattles stays absent on Gold") -- ------- 4b. FieldDefaults answers only what Gold genuinely shares local FD = Gen2Compat.resolve("src.world.FieldDefaults", "fixture") eq(FD.CONSTANTS.world.stepFrames, 16, "stepFrames is 16 on both generations") eq(FD.CONSTANTS.world.turnFrames, 4, "and turnFrames is 4") eq(FD.CONSTANTS.encounterBuckets, nil, "the Gen 1 wild-slot spread is Kanto and must not roll on Gold") eq(FD.CONSTANTS.hmBadges, nil, "nor may Kanto's HM badge gates") eq(FD.world(nil, "stepFrames"), 16, "world() answers the shared keys") eq(FD.world(nil, "poisonStepInterval"), nil, "and refuses the ones Gold's StepEvents do not read from a table") eq(FD.FIELD, nil, "FIELD is absent: every leaf of it is Kanto") eq(FD.seed({}), nil, "seed never writes Kanto's field record into a Gold cache") -- Gen 1's fieldValue is VARIADIC; a fixed arity dropped every deeper path eq(FD.fieldValue(nil, "playerSprites", "walk"), World.PLAYER_SPRITE, "the one data.field path Gold can answer") eq(FD.fieldValue(nil, "playerSprites"), nil, "and not the whole table, which a mod would index .surf on") -- ------- 5. the follower's shouldSpawn upvalue, by name local function upvalueIndex(fn, wanted) local i = 1 while true do local name = debug.getupvalue(fn, i) if not name then return nil end if name == wanted then return i end i = i + 1 end end local updateIdx = upvalueIndex(Follower.update, "shouldSpawn") local enterIdx = upvalueIndex(Follower.onMapEntered, "shouldSpawn") check(updateIdx ~= nil, "Follower.update closes over a local named shouldSpawn") check(enterIdx ~= nil, "and so does Follower.onMapEntered") -- the supported way in writes the SAME cell, so a mod that uses the setter and -- one that rewrites the upvalue cannot end up with two different predicates local sentinel = function() return false end local restore = Follower.setShouldSpawn(sentinel) eq(select(2, debug.getupvalue(Follower.update, updateIdx)), sentinel, "setShouldSpawn writes the upvalue debug.setupvalue would have") Follower.setShouldSpawn(restore) -- ------- 6. patch it and the follower spawns, trails and survives a rebuild local function fixtureWorld() local game = { data = {}, save = { party = {} } } local world = World.new(game) world.maps = { TEST_MAP = { id = "TEST_MAP", group = 1, map = 2, width = 4, height = 4, blocks = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }, objects = {}, warps = {} }, } -- every cell walkable: one block id whose whole quad is COLL_FLOOR local tileset = { collision = { [2] = { 0, 0, 0, 0 } } } world.map = Gen2Map.new(world.maps.TEST_MAP, tileset) world.sprites = { SPRITE_PIKACHU = { id = "SPRITE_PIKACHU", image = sheet, frames = 6, walker = true } } world.player = Player.new(4, 4, "down", { id = "SPRITE_CHRIS", image = sheet, frames = 6, walker = true }) world.npcs, world.entities, world.ghosts = {}, { world.player }, {} return world, game end local world, game = fixtureWorld() local always = function() return true end debug.setupvalue(Follower.update, updateIdx, always) -- the two closures share one upvalue cell under 5.1, which is what lets a mod -- patch either one and suppress both -- assert it rather than assume it eq(select(2, debug.getupvalue(Follower.onMapEntered, enterIdx)), always, "update and onMapEntered share the shouldSpawn cell") Follower.onMapEntered(game, world, nil, true) local npc = Follower.current(world) check(npc ~= nil, "the follower spawns once shouldSpawn says yes") check(npc.passable, "and never blocks the player") eq(npc.cellX, 4, "parked on the player for a fresh map load") -- Two committed steps right. The first hands the follower the cell it is -- already standing on (it spawned under the player), the second is the one it -- has to walk -- and the goal is taken on the COMMIT, while targetX is still -- set, not on the landing, which is what keeps the gap at one cell. local function commit(tx) world.player.targetX, world.player.moving = tx, true Follower.update(game, world) world.player.cellX, world.player.targetX = tx, nil world.player.moving = false end commit(5) commit(6) eq(npc.goalX, 5, "the goal is the cell the player VACATED, one behind") check(npc.moving, "and the follower took a step toward it") eq(npc.targetX, 5, "one cell, in the right direction") eq(npc.facing, "right", "facing the way it walks") -- a seamless rebuild is what used to wipe a mod-inserted entity world.map.def.objects = {} world:rebuildPeople({ seamless = true }) check(Follower.current(world) ~= nil, "a guest survives rebuildPeople, so a follower does not vanish at the top " .. "of the hour") eq(#world.npcs, 1, "and is listed exactly once") -- ------- 7. World:step ticks it, World:interact dispatches through the facade local ticked = 0 world.stepBody = function() end local realUpdate = Follower.update Follower.update = function() ticked = ticked + 1 end world:step() Follower.update = realUpdate eq(ticked, 1, "World:step calls Follower.update after the body, which is the " .. "one per-frame driver every Gen 1 follower mod wraps") local OC = Gen2Compat.resolve("src.world.OverworldController", "fixture") world.interactBody = function() return "vanilla" end eq(world:interact(), "vanilla", "with nothing patched, interact is the body") local vanillaInteract = OC.interact OC.interact = function(w) return "wrapped:" .. tostring(vanillaInteract(w)) end eq(world:interact(), "wrapped:vanilla", "a replaced OverworldController.interact is what the A press dispatches to") OC.interact = vanillaInteract local owTicks = 0 local vanillaUpdate = OC.update OC.update = function() owTicks = owTicks + 1 end world:step() OC.update = vanillaUpdate eq(owTicks, 1, "and a replaced OverworldController.update ticks once a frame") world:step() eq(owTicks, 1, "restored, it costs one comparison and does not run") -- talkTo is the other dispatch a follower mod wraps, and it used to be a warn -- saying the wrapper would never run local talked = nil world.player.facing = "down" world.player.cellX, world.player.cellY = 4, 4 local guest = Gen2Npc.new("TEST_MAP", { index = 9, x = 4, y = 5, movement = Gen2Npc.MOVE.STANDING_UP }, { id = "SPRITE_CHRIS", image = sheet, frames = 6, walker = true }) table.insert(world.npcs, guest) world.interactBody = nil world.busy = function() return false end world.vm = { lastTalked = 0, start = function() return true end, running = function() return false end } local vanillaTalk = OC.talkTo OC.talkTo = function(_w, npc) talked = npc return true end eq(world:interactBody(), true, "a replaced talkTo intercepts the A press") eq(talked, guest, "and receives the object Gold resolved") OC.talkTo = vanillaTalk -- ------- 8. the follower's three general members, which were nil calls Follower.onMapEntered(game, world, nil, true) local trailer = Follower.current(world) check(trailer ~= nil, "a follower to hide") eq(Follower.at(world, trailer.cellX, trailer.cellY), trailer, "Follower.at finds a standing follower, which is the interact hook's test") trailer.moving = true eq(Follower.at(world, trailer.cellX, trailer.cellY), nil, "and not a moving one, which is between two cells") trailer.moving = false local drawn = 0 for _, e in ipairs(world.entities) do if e == trailer then drawn = drawn + 1 end end eq(drawn, 1, "it is on the draw list to begin with") Follower.setVisible(world, false) drawn = 0 for _, e in ipairs(world.entities) do if e == trailer then drawn = drawn + 1 end end eq(drawn, 0, "setVisible(false) drops it from the DRAW list") check(Follower.current(world) == trailer, "and leaves it in the UPDATE list, so it hides in place and keeps trailing") Follower.setVisible(world, true) Follower.setVisible(world, true) drawn = 0 for _, e in ipairs(world.entities) do if e == trailer then drawn = drawn + 1 end end eq(drawn, 1, "and re-adding is idempotent rather than doubling the entity") -- the Gen 1 name for the trail, by reference: a reset through either name has -- to move the live one check(world.pikachuTrail == world.followerTrail, "ow.pikachuTrail and world.followerTrail are one table") -- ------- 9. the NPC instance surface Gen 1 mods pose and draw through local posed = { trailer:pose() } eq(#posed, 7, "pose keeps Gen 1's seven-value contract") eq(posed[4], trailer.facing, "facing is the fourth") eq(type(Gen2Npc.marching), "nil", "marching is a FIELD, not a method") -- Gen 2 saw moving with no targetX, sat still, then assigned cellX = nil and -- every later read blew up a frame downstream trailer.marching, trailer.progress, trailer.stepFrames = true, 0, 2 trailer.moving = false local wasX, wasY = trailer.cellX, trailer.cellY trailer:update(world.map, world.entities) check(trailer.moving, "a marching NPC animates in place") trailer:update(world.map, world.entities) eq(trailer.cellX, wasX, "and never leaves its cell") eq(trailer.cellY, wasY, "on either axis") check(not trailer.marching, "the cycle ends itself after one step's frames") trailer.stepFrames = nil -- ------- 10. Collision.canMove answers what Gold's own player is told -- the surf exception: without it the facade tells a surfing mod every water -- cell is blocked, one line before Gold rides onto it. 0x20 is COLL_WATER's -- row in Permissions' table. local waterMap = Gen2Map.new(world.maps.TEST_MAP, { collision = { [2] = { 0x20, 0x20, 0x20, 0x20 } } }) local walker = { cellX = 1, cellY = 1 } local surfer = { cellX = 1, cellY = 1, surfing = true } eq(select(2, Collision.canMove(waterMap, {}, walker, "right")), "tile", "water refuses a walker") check(Collision.canMove(waterMap, {}, surfer, "right"), "and carries a surfer, which map:isWalkableCell alone never says") -- GetMovementPermissions' side-wall rule (Map:stepPermitted): a facade that -- omits it says yes where Gold bumps, which is what ends an Ice Path slide local stubMap = { inBounds = function() return true end, isWalkableCell = function() return true end, cellCollision = function() return 0 end, stepPermitted = function() return false end, } eq(select(2, Collision.canMove(stubMap, {}, walker, "up")), "tile", "and a step the neighbour's wall kind forbids is refused as 'tile'") -- a mod's OWN movement.collision hook has to see its own canMove call local Runtime = require("src.mods.Runtime") local realHooks = Runtime.hooks local seen = nil Runtime.hooks = { chains = { ["movement.collision"] = true }, call = function(_self, name, vanilla, allowed, ctx) if name ~= "movement.collision" then return vanilla(allowed, ctx) end seen = ctx return not allowed end, } check(Collision.canMove(waterMap, {}, walker, "right"), "the movement.collision chain runs inside the facade's canMove") eq(seen and seen.reason, "tile", "with the ctx keys both generations use") eq(seen and seen.toX, 2, "including the target cell") Runtime.hooks = realHooks eq(Collision.load({}), nil, "Collision.load is a NAMED no-op, never a silent accept: Gold has no " .. "tile-pair table for the mod's intent to land in") -- ------- 11. ScriptRunner: the pure half forwards, the rest is a handle local SR = Gen2Compat.resolve("src.script.ScriptRunner", "fixture") eq(SR.scanLabels({ { "label", "top" }, { "wait", 1 } }).top, 1, "scanLabels is the real Gen 1 function, pure over the mod's own rows") -- with Gen 1's default lookup a script of show_text / wait / warp validates -- CLEAN on Gold and then every row is skipped at run time liveGame.data.commands = { ["fixture:beep"] = {} } eq(#SR.validate({ { "show_text", "x" } }), 1, "validate resolves against Gold's OWN command registry, so a Gen 1 built-in " .. "is reported rather than passed") eq(#SR.validate({ { "fixture:beep" } }), 0, "and a registered mod verb passes") eq(SR.new(nil, world).isRunning, SR.new(nil, world).isRunning, "the handle carries the query half of the one world.vm") eq(SR.new(nil, world):resume(), nil, "resume is refused, not forwarded: the " .. "World already drives the VM and a second call dispatches twice") eq(SR.new(nil, world):update(), nil, "and so is update, which would double-decrement every pause") -- ow.runner, the field a mod guards on before acting. nil there is FALSEY, -- so a mod concludes NO SCRIPT IS RUNNING while one is. check(world.runner ~= nil, "the World carries a runner shim") eq(world.runner:isRunning(), world:scriptRunning(), "and answers the Gen 1 query") -- ------- 12. a monkey-patch on a proxied class reads back as ITSELF -- -- The write-through proxy answered its own override first, so -- `PartyMenu.new = wrapper` read back as the facade's function (the patch was -- invisible) and the override then called the class member the write had -- already replaced -- straight back into the wrapper. local Party2 = require("src.ui.gen2.PartyMenu") local PM = Gen2Compat.resolve("src.ui.PartyMenu", "fixture") local facadeNew = PM.new local wrapped = 0 local wrapper = function(...) wrapped = wrapped + 1 return facadeNew(...) end PM.new = wrapper check(rawequal(PM.new, wrapper), "a mod's write to a proxied member reads " .. "back as the mod's own value, so the patch is visible") check(rawequal(Party2.new, wrapper), "and still lands on the class Gold pushes") liveGame.stack.top = function() return nil end liveGame.stack.pop = function() end local switched = nil local pressed = {} liveGame.input = { wasPressed = function(_, b) return pressed[b] end } local pidgey = { species = "PIDGEY" } local menu = PM.new(liveGame, { party = { pidgey }, onSwitch = function(mon) switched = mon end }) eq(wrapped, 1, "the wrapper ran exactly once: the override calls the " .. "constructor it CAPTURED, not the patched class member") PM.new = nil eq(PM.new, nil, "a nil write is honoured rather than resurrecting the override") PM.new = facadeNew -- Gen 1 fires onSwitch on A for this construction (src/ui/PartyMenu.lua:569); -- Gold's field submenu swallowed the press and only CANCEL could ever answer. check(not menu.wantsSubmenu, "onSwitch outside battle builds the DIRECT list") pressed.a = true menu:update(1 / 60) eq(switched, pidgey, "and A on the row fires onSwitch with the mon") liveGame.input = nil -- the same shape on the battle screen, whose overrides are stamped onto Gold's -- class so an instance answers them local BS = Gen2Compat.resolve("src.battle.BattleState", "fixture") local Battle2 = require("src.ui.gen2.BattleState") local facadeSay = BS.say local sayPatch = function(self, text) return facadeSay(self, text) end BS.say = sayPatch check(rawequal(BS.say, sayPatch), "BattleState.say reads back as the patch") check(rawequal(Battle2.say, sayPatch), "and an instance dispatches to it") BS.say = facadeSay -- ------- 13. the overworld facade over a LIVE world local ow = Gen2Compat.resolve("src.world.OverworldController", "fixture") local owWorld = fixtureWorld() owWorld.map.def.tileset = "TS" owWorld.maps.OTHER_MAP = { id = "OTHER_MAP", group = 1, map = 3, width = 4, height = 4, blocks = { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }, objects = {}, warps = {}, tileset = "TS" } owWorld.tilesets = { TS = { collision = { [2] = { 0, 0, 0, 0 } } } } -- Map copies def.connections at construction (src/world/gen2/Map.lua:26) owWorld.map.connections = { west = { map = "OTHER_MAP", offset = 0 } } liveGame.world = owWorld -- objectId 0 is the PLAYER and 1 is wLastTalked (World:objectEntity): the old -- `(def.index or 0) + 1` gave the player -- which has no .def -- objectId 1, -- so the canonical Gen 1 call walked the last-talked NPC and returned true. local talkNpc = Gen2Npc.new("TEST_MAP", { index = 1, x = 1, y = 1, movement = Gen2Npc.MOVE.STANDING_UP }, { id = "SPRITE_CHRIS", image = sheet, frames = 6, walker = true }) table.insert(owWorld.npcs, talkNpc) owWorld.talkNpc = talkNpc check(ow.scriptMove(owWorld.player, "up", 1), "scriptMove takes the player") eq(owWorld.moveState.objectId, 0, "as objectId 0, not the last-talked NPC") owWorld.moveState = nil check(ow.scriptMove(talkNpc, "up", 1), "and a mapped object") eq(owWorld.moveState.objectId, 2, "as def.index + 1") owWorld.moveState = nil local moved, why = ow.scriptMove({ cellX = 1, cellY = 1 }, "up", 1) eq(moved, nil, "an entity with neither is REFUSED, never moved by proxy") check(type(why) == "string" and #why > 0, "with a reason") eq(ow.npcByIndex(1), talkNpc, "npcByIndex maps the Gen 1 index to index + 1") eq(ow.npcByIndex(0), nil, "and index 0 -- which names no object on either " .. "generation -- is nil, not Gold's wLastTalked") -- Gen 1 returns dest DEF, tileset def, x, y, conn -- (src/world/OverworldController.lua:1404); three values with a map ID first -- shifted every name in `local dest, ts, x, y = ...` eq(select("#", ow.connectionLanding("left")), 5, "connectionLanding keeps Gen 1's five-value shape") local dest, ts, cx, cy, conn = ow.connectionLanding("left") eq(type(dest), "table", "dest is the map DEF, so dest.width reads") eq(dest.width, 4, "with the destination's own size") check(Gen2Map.defPassable(dest, ts, cx, cy, false), "and the tileset def is the second value, which is what the very next " .. "Map.defPassable call takes") eq(type(conn), "table", "the connection record is the fifth") -- COVERAGE has to match what a read actually answers: these eight were -- published backed and every one of them read nil local owCoverage = Gen2Compat.coverage("src.world.OverworldController") for member, status in pairs(owCoverage.members) do if status == "backed" then check(ow[member] ~= nil, "published backed and answers: OverworldController." .. member) end end eq(ow.player, owWorld.player, "ow.player is the live player, the way Gen 1's " .. "module IS the live state (src/core/Game.lua:87)") eq(ow.map, owWorld.map, "and ow.map the live map") eq(ow.npcs, owWorld.npcs, "and the lists are the world's own") local swap = Player.new(1, 1, "down", { id = "SPRITE_CHRIS", image = sheet, frames = 6, walker = true }) ow.player = swap eq(owWorld.player, swap, "a write to a live name moves the world, not a " .. "shadow copy on the facade") ow.player = owWorld.player -- Gold's neighbour rows carry `id`, Gen 1's carry `map`: answered, every -- nb.map read is nil and a scan matches nothing eq(ow.neighbors, nil, "neighbors answers nil rather than a list of the wrong " .. "shape") eq(Gen2Compat.memberStatus("src.world.OverworldController", "neighbors"), "warned", "and coverage says so") eq(Follower.shouldSpawn, nil, "Follower.shouldSpawn is a file-local") eq(Gen2Compat.memberStatus("src.world.PikachuFollower", "shouldSpawn"), "absent", "so coverage publishes it absent, with setShouldSpawn as the way in") -- leave the process as we found it: these tables are singletons debug.setupvalue(Follower.update, updateIdx, function() return false end) S.finish()