local Rom = {} Rom.__index = Rom local BANK_SIZE = 0x4000 function Rom.new(data) assert(type(data) == "string", "ROM data must be a string") return setmetatable({ data = data }, Rom) end function Rom.offset(bank, address) if bank == 0 then assert(address >= 0 and address < BANK_SIZE, ("ROM0 address out of range: $%04X"):format(address)) return address end assert(address >= BANK_SIZE and address < BANK_SIZE * 2, ("bank %02X address out of range: $%04X"):format(bank, address)) return bank * BANK_SIZE + address - BANK_SIZE end function Rom:byte(bank, address) local value = self.data:byte(Rom.offset(bank, address) + 1) assert(value, ("ROM read past end at %02X:%04X"):format(bank, address)) return value end function Rom:word(bank, address) return self:byte(bank, address) + self:byte(bank, address + 1) * 0x100 end function Rom:bytes(bank, address, length) local first = Rom.offset(bank, address) + 1 local last = first + length - 1 assert(last <= #self.data, ("ROM read past end at %02X:%04X + %d"):format(bank, address, length)) local out = {} for index = 1, length do out[index] = self.data:byte(first + index - 1) end return out end function Rom:decodeText(raw, charmap, stop) local out = {} stop = stop or 0x50 for _, value in ipairs(raw) do if value == stop then break end out[#out + 1] = charmap[tostring(value)] or ("{BYTE:%02X}"):format(value) end return table.concat(out) end function Rom:readString(bank, address, charmap, stop, maxLength) local out = {} stop = stop or 0x50 maxLength = maxLength or 4096 for offset = 0, maxLength - 1 do local value = self:byte(bank, address + offset) if value == stop then return table.concat(out), offset + 1 end out[#out + 1] = charmap[tostring(value)] or ("{BYTE:%02X}"):format(value) end error(("unterminated string at %02X:%04X"):format(bank, address)) end function Rom.bcd(raw) local value = 0 for _, byte in ipairs(raw) do value = value * 100 + math.floor(byte / 16) * 10 + byte % 16 end return value end local BitReader = {} BitReader.__index = BitReader function BitReader.new(data) return setmetatable({ data = data, byte = 1, bit = 7 }, BitReader) end function BitReader:read(count) local value = 0 for _ = 1, count or 1 do local byte = self.data[self.byte] if not byte then error("compressed picture ended unexpectedly") end value = value * 2 + math.floor(byte / 2 ^ self.bit) % 2 self.bit = self.bit - 1 if self.bit < 0 then self.byte = self.byte + 1 self.bit = 7 end end return value end local function fillPicPlane(reader, width) local mode = reader:read() local groupCount = width * width * 0x20 local groups = {} while #groups < groupCount do if mode ~= 0 then while #groups < groupCount do local group = reader:read(2) if group == 0 then break end groups[#groups + 1] = group end else local prefix = 0 while reader:read() ~= 0 do prefix = prefix + 1 if prefix >= 16 then error("invalid compressed picture zero run") end end local zeroCount = 2 ^ (prefix + 1) - 1 + reader:read(prefix + 1) for _ = 1, math.min(zeroCount, groupCount - #groups) do groups[#groups + 1] = 0 end end mode = 1 - mode end local reordered = {} for y = 0, width - 1 do for x = 0, width * 8 - 1 do for group = 0, 3 do local source = (y * 4 + group) * width * 8 + x reordered[#reordered + 1] = groups[source + 1] end end end local packed = {} for index = 0, width * width * 8 - 1 do local start = index * 4 packed[index + 1] = reordered[start + 1] * 0x40 + reordered[start + 2] * 0x10 + reordered[start + 3] * 4 + reordered[start + 4] end return packed end local PIC_CODES = { { 0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5, 0xF, 0xE, 0xC, 0xD, 0x8, 0x9, 0xB, 0xA }, { 0xF, 0xE, 0xC, 0xD, 0x8, 0x9, 0xB, 0xA, 0x0, 0x1, 0x3, 0x2, 0x7, 0x6, 0x4, 0x5 }, } local function unfilterPicPlane(plane, width) for x = 0, width * 8 - 1 do local bit = 0 for y = 0, width - 1 do local index = y * width * 8 + x + 1 local high = PIC_CODES[bit + 1][math.floor(plane[index] / 16) + 1] bit = high % 2 local low = PIC_CODES[bit + 1][plane[index] % 16 + 1] bit = low % 2 plane[index] = high * 16 + low end end end local function transposePicTiles(data, width) local tileCount = width * width for index = 0, tileCount - 1 do local other = (index * width + math.floor(index / width)) % tileCount if index < other then for offset = 1, 16 do local left = index * 16 + offset local right = other * 16 + offset data[left], data[right] = data[right], data[left] end end end end function Rom.decompressPic(data) local reader = BitReader.new(data) local width, height = reader:read(4), reader:read(4) if width == 0 or width ~= height then error(("compressed picture is not a non-empty square (%dx%d)") :format(width, height)) end local order = reader:read() local planes = {} planes[order + 1] = fillPicPlane(reader, width) local mode = reader:read() if mode ~= 0 then mode = mode + reader:read() end planes[(1 - order) + 1] = fillPicPlane(reader, width) unfilterPicPlane(planes[order + 1], width) if mode ~= 1 then unfilterPicPlane(planes[(1 - order) + 1], width) end if mode ~= 0 then for index = 1, width * width * 8 do planes[(1 - order) + 1][index] = bit.bxor(planes[(1 - order) + 1][index], planes[order + 1][index]) end end local output = {} for index = 1, width * width * 8 do output[#output + 1] = planes[1][index] output[#output + 1] = planes[2][index] end transposePicTiles(output, width) return output, width end -- pokegold's "lz3" compression (home/decompress.asm), used for Gen 2 -- graphics (tilesets, Pokemon pics, title screen art, ...). `data` is a Lua -- array of bytes (as returned by Rom:bytes) or a raw string; returns a Lua -- array of decompressed bytes. Ported instruction-for-instruction against -- pokegold's Decompress routine and cross-checked against tools/lzcompress.c -- (--uncompress path), which is the canonical reference for this format. local LZ_END = 0xFF local LZ_LITERAL = 0 local LZ_ITERATE = 1 local LZ_ALTERNATE = 2 local LZ_ZERO = 3 local LZ_FLIP = 5 local LZ_REVERSE = 6 local LZ_LONG = 7 local function flipBits(value) local flipped = 0 for bitIndex = 0, 7 do flipped = flipped + math.floor(value / 2 ^ bitIndex) % 2 * 2 ^ (7 - bitIndex) end return flipped end function Rom.decompressLz3(data) local bytes = data if type(data) == "string" then bytes = {} for index = 1, #data do bytes[index] = data:byte(index) end end local pos = 1 local function nextByte() local value = bytes[pos] if not value then error("lz3 stream ended unexpectedly") end pos = pos + 1 return value end local out = {} while true do local first = bytes[pos] if not first then error("lz3 stream ended without a terminator") end pos = pos + 1 if first == LZ_END then break end local command, length if math.floor(first / 0x20) == LZ_LONG then -- 111xxxyy yyyyyyyy: xxx is the real command, yy.. is a 10-bit length command = math.floor(first / 4) % 8 local high = first % 4 length = high * 0x100 + nextByte() + 1 else command = math.floor(first / 0x20) length = first % 0x20 + 1 end if command == LZ_LITERAL then for _ = 1, length do out[#out + 1] = nextByte() end elseif command == LZ_ITERATE then local value = nextByte() for _ = 1, length do out[#out + 1] = value end elseif command == LZ_ALTERNATE then local a, b = nextByte(), nextByte() for index = 1, length do out[#out + 1] = (index % 2 == 1) and a or b end elseif command == LZ_ZERO then for _ = 1, length do out[#out + 1] = 0 end else -- Lookback commands (LZ_REPEAT/LZ_FLIP/LZ_REVERSE, and the unused id -- 7 which the hardware routine falls through to LZ_REPEAT for). -- A high-bit offset byte is a 7-bit negative lookback from the -- current output position; otherwise it is a 15-bit absolute offset -- from the start of the output buffer (two bytes, big-endian). local offsetByte = nextByte() local from if offsetByte >= 0x80 then from = #out - (offsetByte % 0x80) else from = offsetByte * 0x100 + nextByte() + 1 end if command == LZ_FLIP then for index = 0, length - 1 do out[#out + 1] = flipBits(out[from + index]) end elseif command == LZ_REVERSE then for index = 0, length - 1 do out[#out + 1] = out[from - index] end else for index = 0, length - 1 do out[#out + 1] = out[from + index] end end end end return out end return Rom