-- Parity test (#805): ESCAPE ROPE / DIG / TELEPORT must land on an outdoor -- fly-warp cell, and must re-point the LAST_MAP memory at it. -- -- pret: ItemUseEscapeRope (engine/items/item_effects.asm) sets BIT_FLY_WARP -- and BIT_ESCAPE_WARP, and LoadSpecialWarpData's .usedFlyWarp path -- (engine/overworld/special_warps.asm) warps to wLastBlackoutMap with the -- landing cell read from FlyWarpDataPtr. wLastBlackoutMap is ALWAYS an -- outdoor map: SetLastBlackoutMap (engine/events/set_blackout_map.asm) -- copies wLastMap, and WarpFound2 (home/overworld.asm) only writes wLastMap -- when CheckIfInOutsideMap passes. PrepareForSpecialWarp -- (engine/overworld/special_warps.asm) then does `ld [wLastMap], a` with -- that destination for every fly/escape warp that is not a dungeon warp. -- -- The port broke both halves. A .sav import stamps lastHeal from wherever -- the cartridge was saved (src/save_convert/SaveConvert.lua mergeDefaults, -- which records no outdoor), so a save made inside Seafoam Islands made -- ESCAPE ROPE warp the player back into that cave; and the teleport branch -- skipped rememberOutdoor, so the first LAST_MAP exit after the rope still -- resolved against the dungeon door walked in through. -- -- Self-contained; run via `luajit tests/parity_escape_rope_bug805.lua`. package.path = "./?.lua;./?/init.lua;" .. package.path if not _G.love then _G.love = require("tests.love_stub") end local Data = require("src.core.Data") if not (Data.maps and Data.maps.PALLET_TOWN) then Data:load() end local S = require("tests.harness").suite("parity escape rope #805") local check, eq = S.check, S.eq require("src.render.Font").load(Data) local Game = require("src.core.Game") local Input = require("src.core.Input") local StateStack = require("src.core.StateStack") local Renderer = require("src.render.Renderer") local SaveData = require("src.core.SaveData") local Pokemon = require("src.pokemon.Pokemon") local Map = require("src.world.Map") local FieldDefaults = require("src.world.FieldDefaults") local OW = require("src.world.OverworldController") Game.data = Data Game.input = Input; Input:init() Game.renderer = Renderer; Renderer:init() Game.stack = StateStack; StateStack:init() Game.save = SaveData.newGame() Game.save.party = { Pokemon.new(Data, "SQUIRTLE", 20) } -- The player is deep in a cave, having walked in from the outdoor door -- cell the LAST_MAP exits still point at. Game.stack:push(OW, "SEAFOAM_ISLANDS_B2F", 5, 5, "down") local ow = Game.stack:top() Game.overworld = ow -- Capture the warp target instead of running the real Transition. local dest local realStart = ow.startWarpTo ow.startWarpTo = function(self, mapId, x, y, facing, onDone, opts) dest = { map = mapId, x = x, y = y } self.arriveWarp = nil self.transitioning = false end local outsideTilesets = FieldDefaults.field(Data, "outsideTilesets") local function isOutside(mapId) local def = Data.maps[mapId] return def ~= nil and Map.isOutside(def, outsideTilesets) end local flyWarps = Data.field.flyWarps or {} local bootHeal = SaveData.defaultHeal(Data.field.boot) -- --------------------------------------------------------------- 1. healthy -- A save healed by a nurse records the outdoor town alongside the interior -- heal cell; the rope lands on that town's FlyWarpDataPtr cell. Game.save.lastHeal = { map = "VIRIDIAN_POKECENTER", x = 3, y = 3, outdoor = { id = "VIRIDIAN_CITY", x = 23, y = 27 } } ow:rememberOutdoor("ROUTE_23", 8, 60) -- the Victory Road door walked in from dest = nil ow:warpToHealPoint(nil, { arrive = "teleport" }) check(flyWarps.VIRIDIAN_CITY ~= nil, "Viridian City has a fly warp cell") eq(dest.map, "VIRIDIAN_CITY", "healthy heal record: rope lands on the town") eq(dest.x, flyWarps.VIRIDIAN_CITY.x, "rope lands on the FlyWarpDataPtr x") eq(dest.y, flyWarps.VIRIDIAN_CITY.y, "rope lands on the FlyWarpDataPtr y") -- 3. PrepareForSpecialWarp: the destination becomes the new wLastMap, so a -- LAST_MAP exit taken after the rope resolves against the town just landed -- in, not the dungeon door from before. eq(Game.save.lastOutdoor.id, "VIRIDIAN_CITY", "teleport warp re-points wLastMap at the destination (#805)") eq(Game.save.lastOutdoor.x, dest.x, "wLastMap x follows the landing cell") eq(Game.save.lastOutdoor.y, dest.y, "wLastMap y follows the landing cell") -- --------------------------------------------------------------- 2. imported -- Exactly what SaveConvert stamps for a cartridge save made in a cave: the -- player's own cell, no outdoor town. wLastBlackoutMap can never name an -- indoor map, so this record is unusable and falls back to the boot heal -- town (vanilla's zero-filled wLastBlackoutMap is map 0, Pallet Town). Game.save.lastHeal = { map = "SEAFOAM_ISLANDS_B2F", x = 5, y = 5 } ow:rememberOutdoor("ROUTE_23", 8, 60) dest = nil ow:warpToHealPoint(nil, { arrive = "teleport" }) check(not isOutside("SEAFOAM_ISLANDS_B2F"), "Seafoam Islands B2F is not an outside map") check(dest.map ~= "SEAFOAM_ISLANDS_B2F", "imported heal record does not dump the rope back in the cave (#805)") check(isOutside(dest.map), "escape-warp destination is always an outside map") eq(dest.map, bootHeal.map, "unusable heal record falls back to the boot town") eq(dest.x, bootHeal.x, "boot-town fallback keeps its landing x") eq(dest.y, bootHeal.y, "boot-town fallback keeps its landing y") eq(Game.save.lastOutdoor.id, bootHeal.map, "fallback landing is remembered as wLastMap too") -- --------------------------------------------------------------- 3. blackout -- ResetStatusAndHalveMoneyOnBlackout (engine/events/black_out.asm:39-43) SETs Game.save.lastHeal = { map = "VIRIDIAN_POKECENTER", x = 3, y = 3, outdoor = { id = "VIRIDIAN_CITY", x = 23, y = 27 } } ow:rememberOutdoor("ROUTE_23", 8, 60) dest = nil ow:warpToHealPoint() eq(dest.map, "VIRIDIAN_CITY", "blackout lands outside, on the town (#2077)") eq(dest.x, flyWarps.VIRIDIAN_CITY.x, "blackout lands on the FlyWarpDataPtr x") eq(dest.y, flyWarps.VIRIDIAN_CITY.y, "blackout lands on the FlyWarpDataPtr y") eq(Game.save.lastOutdoor.id, "VIRIDIAN_CITY", "blackout re-points wLastMap at the town it landed in") eq(Game.save.lastOutdoor.x, dest.x, "blackout wLastMap x follows the landing") eq(Game.save.lastOutdoor.y, dest.y, "blackout wLastMap y follows the landing") ow.startWarpTo = realStart S.finish()