-- MapContext -- the current map's engine state, as a vanilla Gen1 save has to -- carry it (#889). -- -- Continuing a real save never re-loads the map header. LoadMainData -- (engine/menus/save.asm) copies sMainData back into WRAM and then sets -- BIT_NO_PREVIOUS_MAP in wCurMapTileset; LoadMapHeader (home/overworld.asm) -- clears that bit and RETURNS IMMEDIATELY when it was set, so every byte it -- would otherwise have written -- the map header, connections, warps, signs, -- sprites, the tileset header, the map's music -- comes straight out of the -- save file. A cartridge save always has them because the game saved them. -- -- An export from this port did not: GenSave models the player's progress, not -- the engine scratch around it, and left that whole window zero-filled unless -- it had an imported SRAM image to copy from. The game then continued into -- tileset 0 with a $0000 map-data pointer and sound id 0, which is the -- garbled overworld / silent hang users reported after exporting a save that -- began as a New Game in the port. -- -- This module rebuilds the window from the extracted ROM data, replaying the -- same writes LoadMapHeader makes, so a port-origin save boots on hardware. -- The offsets are all relative to wMainDataStart (= sMainData in a .sav) and -- were summed from ram/wram.asm, then checked byte-for-byte against a real -- cartridge save: rebuilding its current map reproduces the bytes it already -- held, everywhere except the stale tails of disabled connection slots (which -- the engine never reads once the connected-map byte is $FF). -- -- Pure Lua, no love.*: shared by the runtime exporter, the CLI and the tests. local MapContext = {} -- wMainDataStart-relative offsets. Anchors: wCurMap is +103 (the offset -- GenSave already decodes the player's map from), and the run from there is -- wCurMap, wCurrentTileBlockMapViewPointer(2), wYCoord, wXCoord, wYBlockCoord, -- wXBlockCoord, wLastMap, wUnusedLastMapWidth, wCurMapHeader -- so the header -- lands at +112, and every later label follows from the declaration sizes in -- ram/wram.asm (MAX_WARP_EVENTS 32, MAX_BG_EVENTS 16, MAX_OBJECT_EVENTS 16, -- SPRITE_SET_LENGTH 11). local O = { mapMusicSoundID = 100, -- wMapMusicSoundID mapMusicROMBank = 101, -- wMapMusicROMBank viewPointer = 104, -- wCurrentTileBlockMapViewPointer (2) yCoord = 106, xCoord = 107, yBlockCoord = 108, xBlockCoord = 109, curMapHeader = 112, -- wCurMapHeader (10) connectionHeaders = 122, -- wNorth/South/West/EastConnectionHeader (4 x 11) mapBackgroundTile = 182, numberOfWarps = 183, warpEntries = 184, -- MAX_WARP_EVENTS x 4 numSigns = 441, signCoords = 442, -- MAX_BG_EVENTS x 2 signTextIDs = 474, -- MAX_BG_EVENTS numSprites = 490, mapSpriteData = 493, -- MAX_OBJECT_EVENTS x 2 mapSpriteExtra = 525, -- MAX_OBJECT_EVENTS x 2 currentMapHeight2 = 557, currentMapWidth2 = 558, tilesetHeader = 564, -- wTilesetBank .. wGrassTile (11) } MapContext.OFFSETS = O local MAX_WARP_EVENTS = 32 local MAX_BG_EVENTS = 16 local MAX_OBJECT_EVENTS = 16 local CONNECTION_STRUCT = 11 -- map_connection_struct (macros/ram.asm) local SPRITE_STRUCT = 16 -- one wSpriteStateData1/2 entry local NUM_SPRITE_STRUCTS = 16 -- wOverworldMap, the tile-block map the view pointer indexes. Recovered from -- the event_displacement formula below applied to a real save's coordinates, -- and it is the same $C6E8 every Gen1 reference quotes. local OVERWORLD_MAP = 0xC6E8 -- Audio header tables start at $4000 in each of the three audio banks, and -- constants/music_constants.asm derives every song id arithmetically from -- that base ("Song ids are calculated by address", music_const: -- (Music_X - SFX_Headers_1) / 3). So the extracted header address IS the id, -- and no separate MapSongBanks extraction is needed. local SFX_HEADERS_BASE = 0x4000 local function soundId(address) if type(address) ~= "number" then return nil end local delta = address - SFX_HEADERS_BASE if delta < 0 or delta % 3 ~= 0 then return nil end local id = delta / 3 if id > 0xFF then return nil end return id end -- LoadMapHeader parses the map's object data (data/maps/objects/*.asm) into -- five separate WRAM arrays. Walk it exactly as the engine does; `bytes` is -- the 1-based array of raw object-data bytes the extractor captured. local function parseObjects(bytes) local pos = 1 local function take() local b = bytes[pos] pos = pos + 1 return b end local out = { warps = {}, signCoords = {}, signTexts = {}, spriteData = {}, spriteExtra = {}, sprites = {} } out.backgroundTile = take() local warpCount = take() if warpCount == nil then return nil end out.warpCount = warpCount for _ = 1, math.min(warpCount, MAX_WARP_EVENTS) * 4 do out.warps[#out.warps + 1] = take() end local signCount = take() if signCount == nil then return nil end out.signCount = signCount for _ = 1, math.min(signCount, MAX_BG_EVENTS) do out.signCoords[#out.signCoords + 1] = take() -- Y out.signCoords[#out.signCoords + 1] = take() -- X out.signTexts[#out.signTexts + 1] = take() end local spriteCount = take() if spriteCount == nil then return nil end out.spriteCount = spriteCount for _ = 1, math.min(spriteCount, MAX_OBJECT_EVENTS) do local picture, mapY, mapX = take(), take(), take() local movement1, movement2, textId = take(), take(), take() if textId == nil then return nil end out.sprites[#out.sprites + 1] = { picture = picture, mapY = mapY, mapX = mapX, movement1 = movement1, } -- wMapSpriteData: movement byte 2, then the text id with its trainer/item -- flag bits masked off (LoadMapHeader's `and $3f`). out.spriteData[#out.spriteData + 1] = movement2 out.spriteData[#out.spriteData + 1] = textId % 0x40 -- BIT_TRAINER ($40) is tested before BIT_ITEM ($80), as LoadMapHeader does if textId % 0x80 >= 0x40 then -- trainer: class, then party id out.spriteExtra[#out.spriteExtra + 1] = take() out.spriteExtra[#out.spriteExtra + 1] = take() elseif textId >= 0x80 then -- item ball: item id, second byte unused out.spriteExtra[#out.spriteExtra + 1] = take() out.spriteExtra[#out.spriteExtra + 1] = 0 else out.spriteExtra[#out.spriteExtra + 1] = 0 out.spriteExtra[#out.spriteExtra + 1] = 0 end end return out end -- build(data, mapId, x, y) -> ctx, err -- -- ctx.writes [wMainDataStart-relative offset] = array of bytes -- ctx.spriteData 512 bytes for sSpriteData (wSpriteStateData1 then 2) -- ctx.tileAnimations the byte sTileAnimations holds -- -- Returns nil plus a reason when the data set predates the extractor fields -- this needs (an older ROM cache), so callers can fall back rather than fail. function MapContext.build(data, mapId, x, y) local map = data and data.maps and data.maps[mapId] if not map then return nil, "unknown map " .. tostring(mapId) end local sram = map.sram if not (sram and sram.header and sram.objects) then return nil, "map cache has no saved-map bytes (re-import the ROM)" end x, y = math.floor(tonumber(x) or 0), math.floor(tonumber(y) or 0) local writes = {} local header = sram.header writes[O.curMapHeader] = header local height, width = header[2], header[3] local connectionFlags = header[10] -- Connections: LoadMapHeader writes $FF over all four connected-map bytes -- and then copies 11 bytes per present direction, north/south/west/east. -- The disabled slots' remaining bytes are never read. local conn = {} for i = 1, 4 * CONNECTION_STRUCT do conn[i] = 0 end for slot = 0, 3 do conn[slot * CONNECTION_STRUCT + 1] = 0xFF end local pos = 1 for slot, flag in ipairs({ 0x08, 0x04, 0x02, 0x01 }) do if math.floor(connectionFlags / flag) % 2 == 1 then for i = 0, CONNECTION_STRUCT - 1 do conn[(slot - 1) * CONNECTION_STRUCT + 1 + i] = (sram.connections or {})[pos + i] or 0 end pos = pos + CONNECTION_STRUCT end end writes[O.connectionHeaders] = conn local parsed = parseObjects(sram.objects) if not parsed then return nil, "malformed object data for " .. tostring(mapId) end writes[O.mapBackgroundTile] = { parsed.backgroundTile } writes[O.numberOfWarps] = { parsed.warpCount } writes[O.warpEntries] = parsed.warps writes[O.numSigns] = { parsed.signCount } writes[O.signCoords] = parsed.signCoords writes[O.signTextIDs] = parsed.signTexts writes[O.numSprites] = { parsed.spriteCount } writes[O.mapSpriteData] = parsed.spriteData writes[O.mapSpriteExtra] = parsed.spriteExtra -- "map height/width in 2x2 tile blocks", doubled at the end of LoadMapHeader writes[O.currentMapHeight2] = { (height * 2) % 256 } writes[O.currentMapWidth2] = { (width * 2) % 256 } -- The tileset header, as predef LoadTilesetHeader would have copied it. local tilesets = data.tilesets or {} local tilesetDef = tilesets[map.tileset] local tileAnimations = 0 if tilesetDef and tilesetDef.header then local row = {} for i = 1, 11 do row[i] = tilesetDef.header[i] end writes[O.tilesetHeader] = row tileAnimations = tilesetDef.header[12] or 0 end -- MapSongBanks: without it the game continues with sound id 0 and audio -- bank 0, which is what actually hangs a Continue on a white screen. local audio = data.audio local songLabel = audio and audio.mapSongs and audio.mapSongs[mapId] local song = songLabel and audio.songs and audio.songs[songLabel] local id = song and soundId(song.address) if id and song.bank then writes[O.mapMusicSoundID] = { id } writes[O.mapMusicROMBank] = { song.bank % 256 } end -- Player position within its block, and the upper-left corner of the view. -- The pointer is the same expression the warp_to tables are assembled with -- (macros/coords.asm event_displacement). writes[O.yBlockCoord] = { y % 2 } writes[O.xBlockCoord] = { x % 2 } local view = OVERWORLD_MAP + 7 + width + (width + 6) * math.floor(y / 2) + math.floor(x / 2) writes[O.viewPointer] = { view % 256, math.floor(view / 256) % 256 } -- sSpriteData. LoadMapHeader zeroes structs 1-15, sets their image index to -- $ff (off screen until the engine places them), then fills in each map -- object's picture id and its map Y/X plus movement byte 1. Struct 0 is the -- player, which SpecialEnterMap rebuilds through ResetPlayerSpriteData. local spriteData = {} for i = 1, NUM_SPRITE_STRUCTS * SPRITE_STRUCT * 2 do spriteData[i] = 0 end local data2 = NUM_SPRITE_STRUCTS * SPRITE_STRUCT for slot = 1, NUM_SPRITE_STRUCTS - 1 do spriteData[slot * SPRITE_STRUCT + 2 + 1] = 0xFF end for index, sprite in ipairs(parsed.sprites) do local base = index * SPRITE_STRUCT spriteData[base + 1] = sprite.picture spriteData[data2 + base + 4 + 1] = sprite.mapY spriteData[data2 + base + 5 + 1] = sprite.mapX spriteData[data2 + base + 6 + 1] = sprite.movement1 end return { writes = writes, spriteData = spriteData, tileAnimations = tileAnimations, } end return MapContext