mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 03:02:39 +02:00
The new experience (#201)
* new launcher and save converts and pipeline * fixing bugs
This commit is contained in:
@@ -0,0 +1,813 @@
|
||||
-- Vanilla Gen1 (Red/Blue, international) raw SRAM save (32768 bytes) <->
|
||||
-- this project's save.lua shape (src/core/SaveData.lua / SaveData.newGame).
|
||||
--
|
||||
-- Pure Lua, no love.* dependency -- runs under plain luajit for the CLI
|
||||
-- (tools/save_convert/convert.lua) and headless tests alike.
|
||||
--
|
||||
-- Every offset below was derived mechanically from the authoritative
|
||||
-- source (../pokered/ram/wram.asm, ram/sram.asm, macros/ram.asm) and
|
||||
-- cross-checked against three independently well-known Gen1 save
|
||||
-- addresses: money @ 0x25F3, badges @ 0x2602, party data @ 0x2F2C --
|
||||
-- all three fall out exactly right from the single sPlayerName anchor
|
||||
-- below, strong triangulated confirmation the whole chain (SRAM bank 1
|
||||
-- layout, wMainData field order, party_struct/box_struct sizes) is right.
|
||||
--
|
||||
-- SRAM layout (32768 bytes = 4 banks x 8192): bank 0 is sprite buffers +
|
||||
-- Hall of Fame (not modeled -- see "explicitly out of scope" in the
|
||||
-- save-converter plan); bank 1 is "Save Data" (sPlayerName through
|
||||
-- sMainDataCheckSum); banks 2/3 are the 12 PC boxes (6 each) + checksums.
|
||||
--
|
||||
-- Fields with no equivalent in save.lua (current sprite/animation state,
|
||||
-- connection-header cache, Day Care, Safari Zone, HOF roster) are
|
||||
-- intentionally not modeled: on export, encode() starts from the
|
||||
-- ORIGINAL imported bytes as a template when available (GenSave.decode
|
||||
-- stashes them) so that scratch state round-trips untouched instead of
|
||||
-- being invented; with no template (a save that originated in this
|
||||
-- project) those bytes stay zero-filled, which is safe because the real
|
||||
-- game regenerates all of it from wCurMap on the next map load anyway.
|
||||
|
||||
local bit = require("bit")
|
||||
|
||||
local GenSave = {}
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Absolute byte offsets (0-based, matching a raw 32768-byte .sav file)
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
local NAME_LENGTH = 11
|
||||
local PARTY_LENGTH = 6
|
||||
local MONS_PER_BOX = 20
|
||||
local NUM_BADGES = 8
|
||||
local BOX_STRUCT_SIZE = 33 -- Species,HP,Level,Status,Type1,Type2,CatchRate,
|
||||
-- Moves x4,OTID,Exp x3,HPExp,AtkExp,DefExp,
|
||||
-- SpdExp,SpcExp,DVs,PP x4 (macros/ram.asm box_struct)
|
||||
local PARTY_STRUCT_SIZE = 44 -- box_struct + Level + Stats x5 (party_struct)
|
||||
local BOX_REGION_SIZE = 1 + (MONS_PER_BOX + 1) + MONS_PER_BOX * BOX_STRUCT_SIZE
|
||||
+ MONS_PER_BOX * NAME_LENGTH + MONS_PER_BOX * NAME_LENGTH -- 1122
|
||||
|
||||
local O = {}
|
||||
O.playerName = 9624 -- sPlayerName (11B) = 0x2598
|
||||
O.mainData = O.playerName + NAME_LENGTH -- sMainData (wMainDataStart mirror)
|
||||
O.pokedexOwned = O.mainData + 0 -- 19B (flag_array 151)
|
||||
O.pokedexSeen = O.mainData + 19 -- 19B
|
||||
O.numBagItems = O.mainData + 38 -- 1B
|
||||
O.bagItems = O.mainData + 39 -- 41B (20 x (id,qty) + $FF term)
|
||||
O.money = O.mainData + 80 -- 3B BCD = 0x25F3
|
||||
O.rivalName = O.mainData + 83 -- 11B
|
||||
O.options = O.mainData + 94 -- 1B
|
||||
O.badges = O.mainData + 95 -- 1B = 0x2602
|
||||
O.playerId = O.mainData + 98 -- 2B (big-endian)
|
||||
O.curMap = O.mainData + 103 -- 1B
|
||||
O.yCoord = O.mainData + 106 -- 1B
|
||||
O.xCoord = O.mainData + 107 -- 1B
|
||||
O.lastMap = O.mainData + 110 -- 1B
|
||||
O.numPcItems = O.mainData + 579 -- 1B
|
||||
O.pcItems = O.mainData + 580 -- 101B (50 x (id,qty) + $FF term)
|
||||
O.currentBoxNum = O.mainData + 681 -- 1B (bits 0-6: box 0-11, bit 7: unused here)
|
||||
O.coins = O.mainData + 685 -- 2B BCD
|
||||
O.eventFlags = O.mainData + 1104 -- 320B (flag_array NUM_EVENTS = 2560 bits)
|
||||
-- Play time (wPlayTimeHours/Maxed/Minutes/Seconds/Frames) lives INSIDE the
|
||||
-- sMainData window (wMainDataStart..wMainDataEnd is copied verbatim into
|
||||
-- SRAM), 1866 bytes past wMainDataStart -- reached from the checksum-verified
|
||||
-- wEventFlags anchor: 320 (event flag_array) + 293 (the wGrassRate/enemy-party
|
||||
-- battle UNION) + 66 + 66 (wEnemyMonOT/Nicks, 6 x NAME_LENGTH each; the
|
||||
-- rgbds FOR n,1,PARTY_LENGTH+1 loop is end-exclusive => 6 mons, not 7) + 2
|
||||
-- (wTrainerHeaderPtr) + 6 (ds) + 1 (wOpponentAfterWrongAnswer) + 1
|
||||
-- (wCurMapScript) + 7 (ds) = 762. Confirmed on the real fixture: those five
|
||||
-- bytes read 201h 30m 07s, a sane completed-save clock.
|
||||
O.playTimeHours = O.mainData + 1866 -- 1B
|
||||
O.playTimeMaxed = O.mainData + 1867 -- 1B (set once past 255h)
|
||||
O.playTimeMinutes = O.mainData + 1868 -- 1B (0-59)
|
||||
O.playTimeSeconds = O.mainData + 1869 -- 1B (0-59)
|
||||
O.playTimeFrames = O.mainData + 1870 -- 1B (0-59, 1/60s ticks)
|
||||
O.mainDataSize = 1929 -- wMainDataEnd - wMainDataStart
|
||||
|
||||
O.spriteData = O.mainData + O.mainDataSize
|
||||
O.spriteDataSize = 512 -- 2 x 16 sprites x 16B
|
||||
|
||||
O.partyData = O.spriteData + O.spriteDataSize -- = 0x2F2C
|
||||
O.partyCount = O.partyData
|
||||
O.partySpecies = O.partyData + 1 -- 7B (PARTY_LENGTH+1)
|
||||
O.partyMons = O.partyData + 8 -- 6 x 44B
|
||||
O.partyMonOT = O.partyData + 8 + PARTY_LENGTH * PARTY_STRUCT_SIZE
|
||||
O.partyMonNicks = O.partyMonOT + PARTY_LENGTH * NAME_LENGTH
|
||||
O.partyDataSize = 1 + (PARTY_LENGTH + 1) + PARTY_LENGTH * PARTY_STRUCT_SIZE
|
||||
+ PARTY_LENGTH * NAME_LENGTH + PARTY_LENGTH * NAME_LENGTH -- 404
|
||||
|
||||
O.curBoxData = O.partyData + O.partyDataSize
|
||||
O.boxCount = O.curBoxData
|
||||
O.boxSpecies = O.curBoxData + 1 -- 21B
|
||||
O.boxMons = O.curBoxData + 22 -- 20 x 33B
|
||||
O.boxMonOT = O.curBoxData + 22 + MONS_PER_BOX * BOX_STRUCT_SIZE
|
||||
O.boxMonNicks = O.boxMonOT + MONS_PER_BOX * NAME_LENGTH
|
||||
|
||||
O.checksumStart = O.playerName
|
||||
O.checksumEnd = O.curBoxData + BOX_REGION_SIZE + 1 -- + sTileAnimations (1B)
|
||||
O.mainChecksum = O.checksumEnd -- 1B
|
||||
|
||||
O.box1 = 16384 -- bank 2 start
|
||||
O.boxBank2Checksum = O.box1 + 6 * BOX_REGION_SIZE
|
||||
O.boxBank2IndividualChecksums = O.boxBank2Checksum + 1 -- 6B
|
||||
O.box7 = 24576 -- bank 3 start
|
||||
O.boxBank3Checksum = O.box7 + 6 * BOX_REGION_SIZE
|
||||
O.boxBank3IndividualChecksums = O.boxBank3Checksum + 1 -- 6B
|
||||
|
||||
GenSave.OFFSETS = O
|
||||
GenSave.BOX_REGION_SIZE = BOX_REGION_SIZE
|
||||
GenSave.SAVE_SIZE = 32768
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Byte-level helpers. `bytes` is a 32768-byte Lua string (1-based
|
||||
-- indexing, so byte offset N is string position N+1); `buf` for writing
|
||||
-- is a 32768-entry array of 1-char strings, joined at the end.
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
local function u8(bytes, off) return bytes:byte(off + 1) end
|
||||
local function u16be(bytes, off) return u8(bytes, off) * 256 + u8(bytes, off + 1) end
|
||||
local function u24be(bytes, off)
|
||||
return u8(bytes, off) * 65536 + u8(bytes, off + 1) * 256 + u8(bytes, off + 2)
|
||||
end
|
||||
|
||||
local function setByte(buf, off, v)
|
||||
buf[off + 1] = string.char(bit.band(v, 0xFF))
|
||||
end
|
||||
local function setU16be(buf, off, v)
|
||||
setByte(buf, off, bit.band(bit.rshift(v, 8), 0xFF))
|
||||
setByte(buf, off + 1, bit.band(v, 0xFF))
|
||||
end
|
||||
local function setU24be(buf, off, v)
|
||||
setByte(buf, off, bit.band(bit.rshift(v, 16), 0xFF))
|
||||
setByte(buf, off + 1, bit.band(bit.rshift(v, 8), 0xFF))
|
||||
setByte(buf, off + 2, bit.band(v, 0xFF))
|
||||
end
|
||||
local function setBcd(buf, off, nbytes, v)
|
||||
for i = nbytes - 1, 0, -1 do
|
||||
local d = v % 100
|
||||
v = math.floor(v / 100)
|
||||
setByte(buf, off + i, math.floor(d / 10) * 16 + (d % 10))
|
||||
end
|
||||
end
|
||||
local function readBcd(bytes, off, nbytes)
|
||||
local n = 0
|
||||
for i = 0, nbytes - 1 do
|
||||
local b = u8(bytes, off + i)
|
||||
n = n * 100 + math.floor(b / 16) * 10 + (b % 16)
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
-- CalcCheckSum (engine/menus/save.asm): complement of the additive sum
|
||||
local function checksum(bytes, from, to)
|
||||
local sum = 0
|
||||
for i = from, to - 1 do sum = bit.band(sum + u8(bytes, i), 0xFF) end
|
||||
return bit.band(bit.bnot(sum), 0xFF)
|
||||
end
|
||||
|
||||
-- flag_array packs LSB-first within each byte (bit 0 of byte 0 = index 0).
|
||||
-- This is pokered's runtime FlagAction convention (home/predef macros): it
|
||||
-- takes flag number N, addresses byte N/8, and builds the mask by rotating
|
||||
-- a 1 left N%8 times starting from bit 0 -- i.e. flag N%8==0 is the LSB.
|
||||
-- Same convention PKHeX uses for Gen1 dex/event flags (FlagUtil.GetFlag:
|
||||
-- data[ofs + bit/8] >> (bit%8) & 1). Cross-validated against the real save:
|
||||
-- ZAPDOS (dex 145) is physically boxed there, so its owned/seen flag must be
|
||||
-- set; only the LSB reading returns it set (MSB-first spuriously drops
|
||||
-- exactly that one bit at the byte-18 boundary), yielding a complete 151/151
|
||||
-- dex. The prior MSB-first code round-tripped self-consistently but decoded
|
||||
-- every flag_array (pokedex AND event flags) to the wrong bit.
|
||||
local function bitGet(bytes, base, index)
|
||||
local byteOff = base + math.floor(index / 8)
|
||||
local b = u8(bytes, byteOff)
|
||||
return bit.band(bit.rshift(b, index % 8), 1) == 1
|
||||
end
|
||||
|
||||
-- Set one bit directly in `buf` (0-based flag index into a flag_array
|
||||
-- starting at `base`), preserving every other bit already in that byte --
|
||||
-- template bytes (see encode()'s header note) survive for bits this pass
|
||||
-- never explicitly touches, e.g. event-flag bits with no known name
|
||||
-- sharing a byte with ones that do.
|
||||
local function bitSet(buf, base, index, value)
|
||||
local byteOff = base + math.floor(index / 8)
|
||||
local bitIdx = index % 8
|
||||
local cur = buf[byteOff + 1] and buf[byteOff + 1]:byte() or 0
|
||||
local mask = bit.lshift(1, bitIdx)
|
||||
setByte(buf, byteOff, value and bit.bor(cur, mask) or bit.band(cur, bit.bnot(mask)))
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Text (fixed-length name fields: charmap-encoded, "@" ($50) terminated,
|
||||
-- $50-padded after the terminator). setCharmap(cm) must be called once
|
||||
-- before decode/encode (src/save_convert/data/charmap.lua's shape).
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
local charmap
|
||||
|
||||
function GenSave.setCharmap(cm) charmap = cm end
|
||||
|
||||
local function decodeName(bytes, off, len)
|
||||
local out = {}
|
||||
for i = 0, len - 1 do
|
||||
local b = u8(bytes, off + i)
|
||||
if b == 0x50 then break end
|
||||
out[#out + 1] = charmap.byByte[b] or "?"
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
local function encodeName(buf, off, len, text)
|
||||
local i, pos = 0, 1
|
||||
while i < len - 1 and pos <= #text do
|
||||
-- a bracketed control token (e.g. "<DOT>", from decodeName reading a
|
||||
-- byte with no plain-glyph mapping) is ONE game character despite
|
||||
-- being several text bytes here; match it as a whole unit first, or
|
||||
-- it would fall through to per-byte matching and turn into "?" x5
|
||||
local bracket = text:match("^(<[^<>]*>)", pos)
|
||||
local ch, clen
|
||||
if bracket and charmap.byToken[bracket] then
|
||||
ch, clen = bracket, #bracket
|
||||
else
|
||||
local b0 = text:byte(pos)
|
||||
clen = (b0 < 0x80 and 1) or (b0 < 0xE0 and 2) or (b0 < 0xF0 and 3) or 4
|
||||
ch = text:sub(pos, pos + clen - 1)
|
||||
end
|
||||
setByte(buf, off + i, charmap.byToken[ch] or charmap.byToken["?"] or 0x50)
|
||||
i, pos = i + 1, pos + clen
|
||||
end
|
||||
-- Write exactly ONE $50 terminator and then STOP. The bytes after it are
|
||||
-- left untouched: when encoding over a template they stay as the original
|
||||
-- save's post-terminator padding (so an unchanged name round-trips
|
||||
-- byte-identical), and on a templateless export they stay zero-filled. The
|
||||
-- game reads a name only up to the first $50, so whatever follows is inert.
|
||||
if i < len then setByte(buf, off + i, 0x50) end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- DV / PP packing (box_struct DVs, PP)
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
-- DVs:: dw, packed as byte0=(Attack<<4)|Defense, byte1=(Speed<<4)|Special;
|
||||
-- HP DV is derived, not stored, from each stat DV's low bit.
|
||||
local function decodeDVs(bytes, off)
|
||||
local b0, b1 = u8(bytes, off), u8(bytes, off + 1)
|
||||
local atk, def = bit.rshift(b0, 4), bit.band(b0, 0xF)
|
||||
local spe, spc = bit.rshift(b1, 4), bit.band(b1, 0xF)
|
||||
local hp = bit.bor(bit.lshift(bit.band(atk, 1), 3), bit.lshift(bit.band(def, 1), 2),
|
||||
bit.lshift(bit.band(spe, 1), 1), bit.band(spc, 1))
|
||||
return { hp = hp, attack = atk, defense = def, speed = spe, special = spc }
|
||||
end
|
||||
|
||||
local function encodeDVs(buf, off, dvs)
|
||||
setByte(buf, off, bit.bor(bit.lshift(bit.band(dvs.attack or 0, 0xF), 4), bit.band(dvs.defense or 0, 0xF)))
|
||||
setByte(buf, off + 1, bit.bor(bit.lshift(bit.band(dvs.speed or 0, 0xF), 4), bit.band(dvs.special or 0, 0xF)))
|
||||
end
|
||||
|
||||
-- PP byte: top 2 bits = PP Up count (0-3), bottom 6 bits = current PP
|
||||
local function decodePPByte(b) return bit.band(b, 0x3F), bit.rshift(b, 6) end
|
||||
local function encodePPByte(pp, ppUps) return bit.bor(bit.lshift(bit.band(ppUps or 0, 3), 6), bit.band(pp or 0, 0x3F)) end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Crosswalks (built once from `data` = {pokemon=,moves=,items=,maps=})
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
-- pokered constants/type_constants.asm PHYSICAL/SPECIAL block; stable,
|
||||
-- not worth a dedicated extractor for 15 names.
|
||||
local TYPE_BY_INDEX = {
|
||||
[0] = "NORMAL", [1] = "FIGHTING", [2] = "FLYING", [3] = "POISON",
|
||||
[4] = "GROUND", [5] = "ROCK", [6] = "BIRD", [7] = "BUG", [8] = "GHOST",
|
||||
[20] = "FIRE", [21] = "WATER", [22] = "GRASS", [23] = "ELECTRIC",
|
||||
[24] = "PSYCHIC_TYPE", [25] = "ICE", [26] = "DRAGON",
|
||||
}
|
||||
local TYPE_INDEX = {}
|
||||
for i, name in pairs(TYPE_BY_INDEX) do TYPE_INDEX[name] = i end
|
||||
|
||||
-- Badge bit order (constants/ram_constants.asm BIT_BOULDERBADGE=0 ..
|
||||
-- BIT_EARTHBADGE=7); this project stores badges as truthy
|
||||
-- save.inventory[id] entries, not a flag or a separate bitmask
|
||||
-- (src/inventory/Badges.lua Badges.list's VANILLA order matches exactly).
|
||||
local BADGE_BY_BIT = {
|
||||
[0] = "BOULDERBADGE", [1] = "CASCADEBADGE", [2] = "THUNDERBADGE",
|
||||
[3] = "RAINBOWBADGE", [4] = "SOULBADGE", [5] = "MARSHBADGE",
|
||||
[6] = "VOLCANOBADGE", [7] = "EARTHBADGE",
|
||||
}
|
||||
local BADGE_BY_BIT_SET = {}
|
||||
for _, name in pairs(BADGE_BY_BIT) do BADGE_BY_BIT_SET[name] = true end
|
||||
|
||||
-- STATUS_* bits (constants/battle_constants.asm): 0-2 sleep-turns-left,
|
||||
-- 3 PSN, 4 BRN, 5 FRZ, 6 PAR
|
||||
local STATUS_BIT = { PSN = 3, BRN = 4, FRZ = 5, PAR = 6 }
|
||||
local function decodeStatus(b)
|
||||
if bit.band(b, 7) > 0 then return "SLP" end
|
||||
for name, bitIdx in pairs(STATUS_BIT) do
|
||||
if bit.band(b, bit.lshift(1, bitIdx)) ~= 0 then return name end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local function encodeStatus(status)
|
||||
if status == "SLP" then return 7 end
|
||||
if status and STATUS_BIT[status] then return bit.lshift(1, STATUS_BIT[status]) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local function buildIndexCrosswalk(defs)
|
||||
local byIndex, byId = {}, {}
|
||||
for id, def in pairs(defs or {}) do
|
||||
if def.index ~= nil then
|
||||
byIndex[def.index] = id
|
||||
byId[id] = def.index
|
||||
end
|
||||
end
|
||||
return byIndex, byId
|
||||
end
|
||||
|
||||
-- Pokedex bit position: NATIONAL DEX NUMBER (1-151), NOT the internal ROM
|
||||
-- species byte (`def.index`, used for party/box mon structs) -- these are
|
||||
-- two completely different Gen1 numbering schemes (the whole "MissingNo"
|
||||
-- phenomenon is dex-number vs internal-index mismatches). This project's
|
||||
-- generated data has no dedicated dex-number field, but every pokemon.lua
|
||||
-- entry's `source` documents its extraction origin as "ROM:BaseStats[N]",
|
||||
-- and BaseStats is declared in dex order in the disassembly -- verified
|
||||
-- directly against 5 species (BULBASAUR->[1], CHARMANDER->[4],
|
||||
-- SQUIRTLE->[7], PIKACHU->[25], MEWTWO->[150], all exactly their real
|
||||
-- national dex numbers) before relying on it here.
|
||||
local function buildDexCrosswalk(defs)
|
||||
local byDex, dexOf = {}, {}
|
||||
for id, def in pairs(defs or {}) do
|
||||
local n = def.source and tonumber(def.source:match("BaseStats%[(%d+)%]"))
|
||||
if n then
|
||||
byDex[n] = id
|
||||
dexOf[id] = n
|
||||
end
|
||||
end
|
||||
return byDex, dexOf
|
||||
end
|
||||
|
||||
-- TM/HM item entries carry no `index` (data/generated/items.lua extracts
|
||||
-- them by move/slot, not by their place in the raw item-constant table),
|
||||
-- so buildIndexCrosswalk alone would silently drop every TM/HM from the
|
||||
-- bag/PC on encode. Their real item ids ARE derivable: pokered's
|
||||
-- constants/item_constants.asm declares "HM_\1: the item id, starting at
|
||||
-- $C4" and "TM_\1: the item id, starting at $C9" for slot 1, incrementing
|
||||
-- per slot -- i.e. HM01=196+.. , TM01=201+(number-1).
|
||||
local function addMachineIndices(defs, byIndex, byId)
|
||||
for id, def in pairs(defs or {}) do
|
||||
if byId[id] == nil and def.machine and def.machine.number then
|
||||
local base = def.machine.kind == "HM" and 195 or 200
|
||||
local idx = base + def.machine.number
|
||||
byIndex[idx] = id
|
||||
byId[id] = idx
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function GenSave.crosswalks(data)
|
||||
local pokemonByIndex, pokemonIndex = buildIndexCrosswalk(data.pokemon)
|
||||
local movesByIndex, movesIndex = buildIndexCrosswalk(data.moves)
|
||||
local itemsByIndex, itemsIndex = buildIndexCrosswalk(data.items)
|
||||
addMachineIndices(data.items, itemsByIndex, itemsIndex)
|
||||
local mapsByIndex, mapsIndex = buildIndexCrosswalk(data.maps)
|
||||
local pokemonByDex, pokemonDex = buildDexCrosswalk(data.pokemon)
|
||||
return {
|
||||
pokemonByIndex = pokemonByIndex, pokemonIndex = pokemonIndex,
|
||||
pokemonByDex = pokemonByDex, pokemonDex = pokemonDex,
|
||||
movesByIndex = movesByIndex, movesIndex = movesIndex,
|
||||
itemsByIndex = itemsByIndex, itemsIndex = itemsIndex,
|
||||
mapsByIndex = mapsByIndex, mapsIndex = mapsIndex,
|
||||
speciesDefs = data.pokemon or {},
|
||||
}
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Mon struct (box_struct is a byte-for-byte prefix of party_struct;
|
||||
-- decodeMon reads the box_struct fields, then Level+Stats if isParty).
|
||||
-- Type1/Type2 are read for nothing (this project derives type from
|
||||
-- species) but re-derived from data.pokemon[species].types on encode.
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
local function decodeMon(bytes, off, isParty, cw)
|
||||
local speciesIdx = u8(bytes, off)
|
||||
if speciesIdx == 0 then return nil end -- empty slot
|
||||
local species = cw.pokemonByIndex[speciesIdx]
|
||||
local hp = u16be(bytes, off + 1)
|
||||
local boxLevel = u8(bytes, off + 3)
|
||||
local status = decodeStatus(u8(bytes, off + 4))
|
||||
local catchRate = u8(bytes, off + 7)
|
||||
local moves = {}
|
||||
for i = 0, 3 do
|
||||
local moveIdx = u8(bytes, off + 8 + i)
|
||||
if moveIdx > 0 then
|
||||
local pp, ppUps = decodePPByte(u8(bytes, off + 29 + i))
|
||||
moves[#moves + 1] = { id = cw.movesByIndex[moveIdx], pp = pp, ppUps = ppUps }
|
||||
end
|
||||
end
|
||||
local otId = u16be(bytes, off + 12)
|
||||
local exp = u24be(bytes, off + 14)
|
||||
local statExp = {
|
||||
hp = u16be(bytes, off + 17), attack = u16be(bytes, off + 19),
|
||||
defense = u16be(bytes, off + 21), speed = u16be(bytes, off + 23),
|
||||
special = u16be(bytes, off + 25),
|
||||
}
|
||||
local dvs = decodeDVs(bytes, off + 27)
|
||||
local mon = {
|
||||
species = species, exp = exp, dvs = dvs, statExp = statExp,
|
||||
hp = hp, status = status, moves = moves, otId = otId,
|
||||
catchRate = catchRate, level = boxLevel,
|
||||
-- Type1/Type2 as physically stored. This project derives type from species
|
||||
-- for gameplay, but the raw bytes are captured so encode() can reproduce
|
||||
-- them verbatim: some real saves (traded/tampered mons) carry type values
|
||||
-- that do not match the ROM base stats, and re-deriving would corrupt them.
|
||||
typeBytes = { u8(bytes, off + 5), u8(bytes, off + 6) },
|
||||
}
|
||||
if isParty then
|
||||
mon.level = u8(bytes, off + 33)
|
||||
mon.stats = {
|
||||
hp = u16be(bytes, off + 34), attack = u16be(bytes, off + 36),
|
||||
defense = u16be(bytes, off + 38), speed = u16be(bytes, off + 40),
|
||||
special = u16be(bytes, off + 42),
|
||||
}
|
||||
end
|
||||
return mon
|
||||
end
|
||||
|
||||
local function encodeMon(buf, off, mon, isParty, cw)
|
||||
if not mon then
|
||||
setByte(buf, off, 0)
|
||||
return
|
||||
end
|
||||
setByte(buf, off, cw.pokemonIndex[mon.species] or 0)
|
||||
setU16be(buf, off + 1, mon.hp or 0)
|
||||
setByte(buf, off + 3, mon.level or 1)
|
||||
setByte(buf, off + 4, encodeStatus(mon.status))
|
||||
local def = cw.speciesDefs[mon.species]
|
||||
if mon.typeBytes then
|
||||
-- reproduce the exact stored type bytes captured on decode (faithful
|
||||
-- byte round-trip); fresh, engine-built mons have none and derive below.
|
||||
setByte(buf, off + 5, mon.typeBytes[1] or 0)
|
||||
setByte(buf, off + 6, mon.typeBytes[2] or 0)
|
||||
else
|
||||
local t = (def and def.types) or {}
|
||||
setByte(buf, off + 5, TYPE_INDEX[t[1]] or 0)
|
||||
setByte(buf, off + 6, TYPE_INDEX[t[2] or t[1]] or 0)
|
||||
end
|
||||
setByte(buf, off + 7, mon.catchRate or (def and def.catchRate) or 0)
|
||||
for i = 0, 3 do
|
||||
local mv = mon.moves and mon.moves[i + 1]
|
||||
setByte(buf, off + 8 + i, mv and (cw.movesIndex[mv.id] or 0) or 0)
|
||||
setByte(buf, off + 29 + i, mv and encodePPByte(mv.pp, mv.ppUps) or 0)
|
||||
end
|
||||
setU16be(buf, off + 12, mon.otId or 0)
|
||||
setU24be(buf, off + 14, mon.exp or 0)
|
||||
local se = mon.statExp or {}
|
||||
setU16be(buf, off + 17, se.hp or 0)
|
||||
setU16be(buf, off + 19, se.attack or 0)
|
||||
setU16be(buf, off + 21, se.defense or 0)
|
||||
setU16be(buf, off + 23, se.speed or 0)
|
||||
setU16be(buf, off + 25, se.special or 0)
|
||||
encodeDVs(buf, off + 27, mon.dvs or {})
|
||||
if isParty then
|
||||
setByte(buf, off + 33, mon.level or 1)
|
||||
local st = mon.stats or {}
|
||||
setU16be(buf, off + 34, st.hp or 0)
|
||||
setU16be(buf, off + 36, st.attack or 0)
|
||||
setU16be(buf, off + 38, st.defense or 0)
|
||||
setU16be(buf, off + 40, st.speed or 0)
|
||||
setU16be(buf, off + 42, st.special or 0)
|
||||
end
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Bag / PC items: (id, qty) byte pairs, $FF-terminated
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
local function decodeItemList(bytes, off, capacity, cw)
|
||||
local inventory, order = {}, {}
|
||||
for i = 0, capacity - 1 do
|
||||
local idByte = u8(bytes, off + i * 2)
|
||||
if idByte == 0xFF then break end
|
||||
local qty = u8(bytes, off + i * 2 + 1)
|
||||
local id = cw.itemsByIndex[idByte]
|
||||
if id then
|
||||
inventory[id] = qty
|
||||
order[#order + 1] = id
|
||||
end
|
||||
end
|
||||
return inventory, order
|
||||
end
|
||||
|
||||
local function encodeItemList(buf, off, capacity, inventory, order, cw)
|
||||
local i = 0
|
||||
local seen = {}
|
||||
local function put(id, qty)
|
||||
if i >= capacity or not qty or qty <= 0 then return end
|
||||
local idByte = cw.itemsIndex[id]
|
||||
if not idByte then return end
|
||||
setByte(buf, off + i * 2, idByte)
|
||||
setByte(buf, off + i * 2 + 1, math.min(qty, 99))
|
||||
i = i + 1
|
||||
seen[id] = true
|
||||
end
|
||||
for _, id in ipairs(order or {}) do
|
||||
if inventory[id] and not seen[id] then put(id, inventory[id]) end
|
||||
end
|
||||
for id, qty in pairs(inventory or {}) do
|
||||
if not seen[id] then put(id, qty) end
|
||||
end
|
||||
setByte(buf, off + i * 2, 0xFF)
|
||||
return i -- count actually written (badges etc. in `inventory` that
|
||||
-- aren't real items are silently skipped by put(), so this
|
||||
-- can be less than #inventory -- see the wNumBagItems caller)
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- decode: raw 32768-byte SRAM string -> save.lua-shaped table
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
function GenSave.decode(bytes, data, opts)
|
||||
assert(#bytes == GenSave.SAVE_SIZE, "expected a 32768-byte save")
|
||||
local cw = GenSave.crosswalks(data)
|
||||
local warnings = {}
|
||||
local function warn(msg) warnings[#warnings + 1] = msg end
|
||||
|
||||
if checksum(bytes, O.checksumStart, O.checksumEnd) ~= u8(bytes, O.mainChecksum) then
|
||||
warn("main data checksum mismatch (importing anyway)")
|
||||
end
|
||||
|
||||
local save = {
|
||||
meta = { format = "gen1_import" },
|
||||
player = {
|
||||
name = decodeName(bytes, O.playerName, NAME_LENGTH),
|
||||
rival = decodeName(bytes, O.rivalName, NAME_LENGTH),
|
||||
id = u16be(bytes, O.playerId),
|
||||
},
|
||||
money = readBcd(bytes, O.money, 3),
|
||||
coins = readBcd(bytes, O.coins, 2),
|
||||
inventory = {},
|
||||
pcItems = {},
|
||||
pokedex = { seen = {}, owned = {} },
|
||||
flags = {},
|
||||
party = {},
|
||||
boxes = {},
|
||||
currentBox = 1,
|
||||
}
|
||||
|
||||
-- pokedex (own/seen, flag_array NUM_POKEMON: bit 0 = dex #1)
|
||||
for dex, species in pairs(cw.pokemonByDex) do
|
||||
local bitIdx = dex - 1
|
||||
if bitGet(bytes, O.pokedexOwned, bitIdx) then save.pokedex.owned[species] = true end
|
||||
if bitGet(bytes, O.pokedexSeen, bitIdx) then save.pokedex.seen[species] = true end
|
||||
end
|
||||
|
||||
save.inventory, save.bagOrder = decodeItemList(bytes, O.bagItems, 20, cw)
|
||||
save.pcItems, save.pcOrder = decodeItemList(bytes, O.pcItems, 50, cw)
|
||||
|
||||
-- badges: truthy save.inventory[id] entries (src/inventory/Badges.lua),
|
||||
-- set AFTER decodeItemList since that call replaces save.inventory
|
||||
local badgesByte = u8(bytes, O.badges)
|
||||
for i = 0, NUM_BADGES - 1 do
|
||||
if bit.band(badgesByte, bit.lshift(1, i)) ~= 0 then
|
||||
save.inventory[BADGE_BY_BIT[i]] = 1
|
||||
end
|
||||
end
|
||||
|
||||
-- party
|
||||
local partyCount = u8(bytes, O.partyCount)
|
||||
for i = 0, math.min(partyCount, PARTY_LENGTH) - 1 do
|
||||
local mon = decodeMon(bytes, O.partyMons + i * PARTY_STRUCT_SIZE, true, cw)
|
||||
if mon then
|
||||
mon.ot = decodeName(bytes, O.partyMonOT + i * NAME_LENGTH, NAME_LENGTH)
|
||||
mon.nickname = decodeName(bytes, O.partyMonNicks + i * NAME_LENGTH, NAME_LENGTH)
|
||||
save.party[#save.party + 1] = mon
|
||||
end
|
||||
end
|
||||
|
||||
-- current box (bank 1) + the 11 stored boxes (banks 2/3)
|
||||
for i = 1, 12 do save.boxes[i] = {} end
|
||||
local function decodeBoxRegion(base, boxNum)
|
||||
local count = u8(bytes, base)
|
||||
for i = 0, math.min(count, MONS_PER_BOX) - 1 do
|
||||
local mon = decodeMon(bytes, base + 22 + i * BOX_STRUCT_SIZE, false, cw)
|
||||
if mon then
|
||||
mon.ot = decodeName(bytes, base + 22 + MONS_PER_BOX * BOX_STRUCT_SIZE + i * NAME_LENGTH, NAME_LENGTH)
|
||||
mon.nickname = decodeName(bytes, base + 22 + MONS_PER_BOX * (BOX_STRUCT_SIZE + NAME_LENGTH) + i * NAME_LENGTH, NAME_LENGTH)
|
||||
table.insert(save.boxes[boxNum], mon)
|
||||
end
|
||||
end
|
||||
end
|
||||
local curBoxNum = bit.band(u8(bytes, O.currentBoxNum), 0x7F) -- 0-based box index
|
||||
curBoxNum = math.max(1, math.min(12, curBoxNum + 1))
|
||||
decodeBoxRegion(O.curBoxData, curBoxNum)
|
||||
for b = 1, 6 do
|
||||
if b + 0 ~= curBoxNum then decodeBoxRegion(O.box1 + (b - 1) * BOX_REGION_SIZE, b) end
|
||||
end
|
||||
for b = 7, 12 do
|
||||
if b ~= curBoxNum then decodeBoxRegion(O.box7 + (b - 7) * BOX_REGION_SIZE, b) end
|
||||
end
|
||||
save.currentBox = curBoxNum
|
||||
|
||||
-- event flags (only bits with a known name are decoded)
|
||||
local events = data.eventFlags
|
||||
if events then
|
||||
for bitIdx, name in pairs(events.byBit) do
|
||||
if bitGet(bytes, O.eventFlags, bitIdx) then save.flags[name] = true end
|
||||
end
|
||||
end
|
||||
|
||||
-- map + position
|
||||
local mapIdx = u8(bytes, O.curMap)
|
||||
local mapId = cw.mapsByIndex[mapIdx]
|
||||
local y, x = u8(bytes, O.yCoord), u8(bytes, O.xCoord)
|
||||
if mapId then
|
||||
save.player.map, save.player.x, save.player.y = mapId, x, y
|
||||
else
|
||||
warn(("unknown map index %d, defaulting spawn"):format(mapIdx))
|
||||
end
|
||||
local lastMapIdx = u8(bytes, O.lastMap)
|
||||
local lastMapId = cw.mapsByIndex[lastMapIdx]
|
||||
if lastMapId then save.lastOutdoor = { id = lastMapId } end
|
||||
|
||||
-- play time: this project stores save.playTime as a single float of
|
||||
-- SECONDS (src/core/Game.lua accumulates dt each frame; StartMenu /
|
||||
-- TrainerCard / TitleState render it H:MM via t/3600 and (t/60)%60).
|
||||
-- Fold the Gen1 H/M/S/F fields into that one number; frames are 1/60s
|
||||
-- sub-second ticks, kept as a fraction so an export recovers them exactly.
|
||||
save.playTime = u8(bytes, O.playTimeHours) * 3600
|
||||
+ u8(bytes, O.playTimeMinutes) * 60
|
||||
+ u8(bytes, O.playTimeSeconds)
|
||||
+ u8(bytes, O.playTimeFrames) / 60
|
||||
|
||||
save.warnings = warnings
|
||||
save.rawImport = bytes -- template for a later encode(); see file header
|
||||
return save
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- encode: save.lua-shaped table -> raw 32768-byte SRAM string
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
function GenSave.encode(save, data, template)
|
||||
local cw = GenSave.crosswalks(data)
|
||||
local src = template or save.rawImport
|
||||
local buf = {}
|
||||
if src then
|
||||
for i = 1, GenSave.SAVE_SIZE do buf[i] = src:sub(i, i) end
|
||||
else
|
||||
local zero = string.char(0)
|
||||
for i = 1, GenSave.SAVE_SIZE do buf[i] = zero end
|
||||
end
|
||||
|
||||
encodeName(buf, O.playerName, NAME_LENGTH, (save.player and save.player.name) or "RED")
|
||||
encodeName(buf, O.rivalName, NAME_LENGTH, (save.player and save.player.rival) or "BLUE")
|
||||
setU16be(buf, O.playerId, (save.player and save.player.id) or 0)
|
||||
setBcd(buf, O.money, 3, math.min(save.money or 0, 999999))
|
||||
setBcd(buf, O.coins, 2, math.min(save.coins or 0, 9999))
|
||||
|
||||
local badgesByte = 0
|
||||
for bitIdx, name in pairs(BADGE_BY_BIT) do
|
||||
if save.inventory and save.inventory[name] then
|
||||
badgesByte = bit.bor(badgesByte, bit.lshift(1, bitIdx))
|
||||
end
|
||||
end
|
||||
setByte(buf, O.badges, badgesByte)
|
||||
|
||||
for dex, species in pairs(cw.pokemonByDex) do
|
||||
local bitIdx = dex - 1
|
||||
bitSet(buf, O.pokedexOwned, bitIdx,
|
||||
(save.pokedex and save.pokedex.owned and save.pokedex.owned[species]) and true or false)
|
||||
bitSet(buf, O.pokedexSeen, bitIdx,
|
||||
(save.pokedex and save.pokedex.seen and save.pokedex.seen[species]) and true or false)
|
||||
end
|
||||
|
||||
-- Badges occupy real item IDs in data/generated/items.lua (Gen1's item
|
||||
-- ID space includes them, $01-$08, for the "got the BOULDERBADGE!"
|
||||
-- text display), but this project's save.lua stores them as truthy
|
||||
-- save.inventory[id] entries alongside actual bag items (see the badge
|
||||
-- block above and src/inventory/Badges.lua) -- a real save NEVER
|
||||
-- writes them into wBagItems (they only ever live in wObtainedBadges,
|
||||
-- already encoded above), so they must be filtered out here or they'd
|
||||
-- corrupt the bag with bogus "badge items".
|
||||
local bagInventory = {}
|
||||
for id, qty in pairs(save.inventory or {}) do
|
||||
if not BADGE_BY_BIT_SET[id] then bagInventory[id] = qty end
|
||||
end
|
||||
local bagN = encodeItemList(buf, O.bagItems, 20, bagInventory, save.bagOrder, cw)
|
||||
setByte(buf, O.numBagItems, bagN)
|
||||
local pcN = encodeItemList(buf, O.pcItems, 50, save.pcItems or {}, save.pcOrder, cw)
|
||||
setByte(buf, O.numPcItems, pcN)
|
||||
|
||||
local events = data.eventFlags
|
||||
if events and save.flags then
|
||||
for name in pairs(save.flags) do
|
||||
local bitIdx = events.byName[name]
|
||||
if bitIdx then bitSet(buf, O.eventFlags, bitIdx, true) end
|
||||
end
|
||||
end
|
||||
|
||||
-- party
|
||||
local party = save.party or {}
|
||||
local partyN = math.min(#party, PARTY_LENGTH)
|
||||
setByte(buf, O.partyCount, partyN)
|
||||
for i = 0, partyN - 1 do
|
||||
local mon = party[i + 1]
|
||||
encodeMon(buf, O.partyMons + i * PARTY_STRUCT_SIZE, mon, true, cw)
|
||||
setByte(buf, O.partySpecies + i, cw.pokemonIndex[mon.species] or 0)
|
||||
encodeName(buf, O.partyMonOT + i * NAME_LENGTH, NAME_LENGTH,
|
||||
mon.ot or (save.player and save.player.name) or "RED")
|
||||
encodeName(buf, O.partyMonNicks + i * NAME_LENGTH, NAME_LENGTH,
|
||||
mon.nickname or mon.species or "")
|
||||
end
|
||||
-- $FF-terminate the species index list right after the last real mon. The
|
||||
-- struct, OT-name and nickname bytes of the empty slots past partyN are left
|
||||
-- exactly as the template holds them (original stale data -> byte-identical
|
||||
-- round-trip) or zero on a fresh export -- the game never reads past the
|
||||
-- count, so this matches how it leaves those bytes itself.
|
||||
setByte(buf, O.partySpecies + partyN, 0xFF)
|
||||
|
||||
-- boxes: current box mirrors save.currentBox into sCurBoxData; all 12
|
||||
-- also get written into their bank-2/3 slot (sCurBoxData is a working
|
||||
-- copy the real game keeps in sync on every PC visit, so keeping both
|
||||
-- copies consistent here matches that invariant)
|
||||
local function encodeBoxRegion(base, mons)
|
||||
local n = math.min(#mons, MONS_PER_BOX)
|
||||
setByte(buf, base, n)
|
||||
for i = 0, n - 1 do
|
||||
local mon = mons[i + 1]
|
||||
encodeMon(buf, base + 22 + i * BOX_STRUCT_SIZE, mon, false, cw)
|
||||
setByte(buf, base + 1 + i, cw.pokemonIndex[mon.species] or 0)
|
||||
encodeName(buf, base + 22 + MONS_PER_BOX * BOX_STRUCT_SIZE + i * NAME_LENGTH, NAME_LENGTH,
|
||||
mon.ot or (save.player and save.player.name) or "RED")
|
||||
encodeName(buf, base + 22 + MONS_PER_BOX * (BOX_STRUCT_SIZE + NAME_LENGTH) + i * NAME_LENGTH, NAME_LENGTH,
|
||||
mon.nickname or mon.species or "")
|
||||
end
|
||||
-- $FF-terminate the species list after the last real mon; empty slots past
|
||||
-- n keep their template bytes (byte-identical round-trip) or zero (fresh
|
||||
-- export), just as the game leaves stale box data untouched past the count.
|
||||
setByte(buf, base + 1 + n, 0xFF)
|
||||
end
|
||||
local boxes = save.boxes or {}
|
||||
local curBoxNum = math.max(1, math.min(12, save.currentBox or 1))
|
||||
encodeBoxRegion(O.curBoxData, boxes[curBoxNum] or {})
|
||||
-- bit 7 of wCurBoxNum is the "box system initialized" flag, not part of the
|
||||
-- 0-11 index; preserve it from the template, or set it on a templateless
|
||||
-- export (any save we emit has an initialized box system).
|
||||
local prevBoxByte = buf[O.currentBoxNum + 1]
|
||||
local boxHiBit = (src and prevBoxByte) and bit.band(prevBoxByte:byte(), 0x80) or 0x80
|
||||
setByte(buf, O.currentBoxNum, bit.bor(bit.band(curBoxNum - 1, 0x7F), boxHiBit))
|
||||
for b = 1, 6 do encodeBoxRegion(O.box1 + (b - 1) * BOX_REGION_SIZE, boxes[b] or {}) end
|
||||
for b = 7, 12 do encodeBoxRegion(O.box7 + (b - 7) * BOX_REGION_SIZE, boxes[b] or {}) end
|
||||
|
||||
-- map + position
|
||||
if save.player and save.player.map then
|
||||
setByte(buf, O.curMap, cw.mapsIndex[save.player.map] or 0)
|
||||
setByte(buf, O.yCoord, save.player.y or 0)
|
||||
setByte(buf, O.xCoord, save.player.x or 0)
|
||||
end
|
||||
if save.lastOutdoor and save.lastOutdoor.id then
|
||||
setByte(buf, O.lastMap, cw.mapsIndex[save.lastOutdoor.id] or 0)
|
||||
end
|
||||
|
||||
-- play time: split save.playTime (seconds) back into H/M/S/F. The real
|
||||
-- game freezes the clock at 255h and sets wPlayTimeMaxed once past it, so
|
||||
-- mirror that cap rather than letting hours overflow a single byte.
|
||||
local totalFrames = math.floor((save.playTime or 0) * 60 + 0.5)
|
||||
local hours = math.floor(totalFrames / 216000) -- 3600s * 60 frames
|
||||
if hours > 255 then
|
||||
setByte(buf, O.playTimeHours, 255)
|
||||
setByte(buf, O.playTimeMaxed, 1)
|
||||
setByte(buf, O.playTimeMinutes, 59)
|
||||
setByte(buf, O.playTimeSeconds, 59)
|
||||
setByte(buf, O.playTimeFrames, 59)
|
||||
else
|
||||
local rem = totalFrames - hours * 216000
|
||||
local mins = math.floor(rem / 3600); rem = rem - mins * 3600
|
||||
local secs = math.floor(rem / 60)
|
||||
setByte(buf, O.playTimeHours, hours)
|
||||
setByte(buf, O.playTimeMaxed, 0)
|
||||
setByte(buf, O.playTimeMinutes, mins)
|
||||
setByte(buf, O.playTimeSeconds, secs)
|
||||
setByte(buf, O.playTimeFrames, rem - secs * 60)
|
||||
end
|
||||
|
||||
local out = table.concat(buf)
|
||||
-- checksums, computed last over the now-final bytes
|
||||
local outBuf = {}
|
||||
for i = 1, #out do outBuf[i] = out:sub(i, i) end
|
||||
setByte(outBuf, O.mainChecksum, checksum(out, O.checksumStart, O.checksumEnd))
|
||||
-- Per pokered (engine/menus/save.asm SaveSAVtoSRAM / CalcCheckSum): each box
|
||||
-- gets its own checksum, and the bank aggregate is CalcCheckSum over the
|
||||
-- ENTIRE six-box region (6 x 1122 bytes), not a sum of the six box sums.
|
||||
local function boxChecksum(base) return checksum(out, base, base + BOX_REGION_SIZE) end
|
||||
for b = 0, 5 do
|
||||
setByte(outBuf, O.boxBank2IndividualChecksums + b,
|
||||
boxChecksum(O.box1 + b * BOX_REGION_SIZE))
|
||||
end
|
||||
setByte(outBuf, O.boxBank2Checksum,
|
||||
checksum(out, O.box1, O.box1 + 6 * BOX_REGION_SIZE))
|
||||
for b = 0, 5 do
|
||||
setByte(outBuf, O.boxBank3IndividualChecksums + b,
|
||||
boxChecksum(O.box7 + b * BOX_REGION_SIZE))
|
||||
end
|
||||
setByte(outBuf, O.boxBank3Checksum,
|
||||
checksum(out, O.box7, O.box7 + 6 * BOX_REGION_SIZE))
|
||||
|
||||
return table.concat(outBuf)
|
||||
end
|
||||
|
||||
return GenSave
|
||||
@@ -0,0 +1,189 @@
|
||||
-- SaveConvert -- the runtime-facing entry point the launcher UI calls to
|
||||
-- turn a vanilla Gen1 (Red/Blue, international) battery save into this
|
||||
-- project's in-memory save table, and back out to a raw .sav image.
|
||||
--
|
||||
-- This is the ONE place the engine, the tests and the CLI
|
||||
-- (tools/save_convert/convert.lua) share: the GenSave codec, the crosswalk
|
||||
-- data loading, the merge over new-game defaults, and the version tag all
|
||||
-- live here so every consumer behaves identically.
|
||||
--
|
||||
-- Pure Lua, no love.* dependency at require time: GenSave and the crosswalk
|
||||
-- tables load through `require`, exactly how src/core/Data.lua pulls the
|
||||
-- generated modules -- which resolves under both plain luajit (package.path
|
||||
-- "./?.lua") for the headless CLI/tests and love.filesystem for a fused
|
||||
-- build, with an OS-path fallback for odd working directories. The only
|
||||
-- place `love` is referenced is inside a guarded fallback, so running under
|
||||
-- stock Lua never touches it.
|
||||
|
||||
local GenSave = require("src.save_convert.GenSave")
|
||||
|
||||
local SaveConvert = {}
|
||||
|
||||
SaveConvert.SAVE_SIZE = GenSave.SAVE_SIZE
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Crosswalk data loading (cached). Mirrors src/core/Data.lua: prefer
|
||||
-- `require` (works headless via package.path and fused via love's package
|
||||
-- searcher); fall back to love.filesystem.load, then a plain dofile, for
|
||||
-- the rare case a host has an unusual cwd or module path.
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
-- { require-module-path, os-relative-file-path } for each table the codec
|
||||
-- needs. pokemon/moves/items/maps come from the shared generated data;
|
||||
-- charmap/event_flags are the save-convert-specific crosswalks.
|
||||
local DATA_MODULES = {
|
||||
pokemon = { "data.generated.pokemon", "data/generated/pokemon.lua" },
|
||||
moves = { "data.generated.moves", "data/generated/moves.lua" },
|
||||
items = { "data.generated.items", "data/generated/items.lua" },
|
||||
maps = { "data.generated.maps", "data/generated/maps.lua" },
|
||||
charmap = { "src.save_convert.data.charmap", "src/save_convert/data/charmap.lua" },
|
||||
eventFlags = { "src.save_convert.data.event_flags", "src/save_convert/data/event_flags.lua" },
|
||||
}
|
||||
|
||||
local function loadTable(requirePath, filePath)
|
||||
local ok, mod = pcall(require, requirePath)
|
||||
if ok and type(mod) == "table" then return mod end
|
||||
-- fused build with an unexpected module path: read straight off the
|
||||
-- mounted filesystem (love is a global here, only ever touched when it
|
||||
-- actually exists -- stock Lua never reaches this branch)
|
||||
if love and love.filesystem and love.filesystem.getInfo
|
||||
and love.filesystem.getInfo(filePath) then
|
||||
local chunk = love.filesystem.load(filePath)
|
||||
if chunk then
|
||||
local m = chunk()
|
||||
if type(m) == "table" then return m end
|
||||
end
|
||||
end
|
||||
local chunk = loadfile(filePath)
|
||||
if chunk then
|
||||
local m = chunk()
|
||||
if type(m) == "table" then return m end
|
||||
end
|
||||
return nil, ("cannot load save-convert data module %q (tried require %q and file %q)")
|
||||
:format(requirePath, requirePath, filePath)
|
||||
end
|
||||
|
||||
local crosswalk -- { pokemon=, moves=, items=, maps=, eventFlags= }
|
||||
local charmapReady
|
||||
|
||||
local function ensureData()
|
||||
if not crosswalk then
|
||||
local data = {}
|
||||
for key, spec in pairs(DATA_MODULES) do
|
||||
if key ~= "charmap" then
|
||||
local mod, e = loadTable(spec[1], spec[2])
|
||||
if not mod then return nil, e end
|
||||
data[key] = mod
|
||||
end
|
||||
end
|
||||
crosswalk = data
|
||||
end
|
||||
if not charmapReady then
|
||||
local cm, err = loadTable(DATA_MODULES.charmap[1], DATA_MODULES.charmap[2])
|
||||
if not cm then return nil, err end
|
||||
GenSave.setCharmap(cm)
|
||||
charmapReady = true
|
||||
end
|
||||
return crosswalk
|
||||
end
|
||||
|
||||
-- Exposed for the CLI/tests so they can share the exact data set the codec
|
||||
-- uses (and so a caller can pre-warm the cache). Returns data, err.
|
||||
function SaveConvert.loadData()
|
||||
return ensureData()
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- new-game default skeleton the decoded fields merge on top of. Carried
|
||||
-- verbatim from tools/save_convert/convert.lua so the CLI and the runtime
|
||||
-- produce a byte-identical save table for the same input.
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
local function defaultsSave()
|
||||
return {
|
||||
meta = { format = "gen1_import", mods = {} },
|
||||
defeatedTrainers = {},
|
||||
repelSteps = 0,
|
||||
modData = {},
|
||||
options = {
|
||||
textSpeed = 3, animations = true, battleStyle = "shift",
|
||||
ruleset = "gen1_faithful", musicVol = 7, sfxVol = 7, musicFilter = 0,
|
||||
speed = 1, colors = "gbc", tilt = 0, gbcfx = 0,
|
||||
videoMode = "windowed", mods = {},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
-- Merge a GenSave.decode() result over the new-game defaults, exactly the
|
||||
-- way convert.lua did, then stamp the requested version. The 32768-byte
|
||||
-- import template GenSave stashes as `rawImport` and the decode `warnings`
|
||||
-- are dropped here: neither belongs in a serialized slot file (a fresh
|
||||
-- export always starts zero-filled -- see GenSave.lua's header).
|
||||
local function mergeDefaults(decoded, version)
|
||||
decoded.warnings = nil
|
||||
decoded.rawImport = nil
|
||||
local save = defaultsSave()
|
||||
for k, v in pairs(decoded) do save[k] = v end
|
||||
save.lastHeal = { map = save.player.map, x = save.player.x, y = save.player.y }
|
||||
save.lastOutdoor = save.lastOutdoor or { id = save.player.map }
|
||||
if version ~= nil then
|
||||
save.meta = save.meta or {}
|
||||
save.meta.version = version
|
||||
end
|
||||
return save
|
||||
end
|
||||
SaveConvert.mergeDefaults = mergeDefaults
|
||||
|
||||
-- ------------------------------------------------------------------
|
||||
-- Public API
|
||||
-- ------------------------------------------------------------------
|
||||
|
||||
-- importSav(bytes, version) -> saveTable, err
|
||||
-- bytes: the raw 32768-byte SRAM string. Validates size and the main-data
|
||||
-- checksum, decodes through GenSave, and returns a save table fully merged
|
||||
-- over the new-game defaults and tagged with `version`, ready to hand to
|
||||
-- SaveSerializer.encode for a slot file. On any failure returns nil + a
|
||||
-- message (never raises).
|
||||
function SaveConvert.importSav(bytes, version)
|
||||
if type(bytes) ~= "string" then
|
||||
return nil, "expected raw save bytes as a string"
|
||||
end
|
||||
if #bytes ~= GenSave.SAVE_SIZE then
|
||||
return nil, ("save must be %d bytes, got %d"):format(GenSave.SAVE_SIZE, #bytes)
|
||||
end
|
||||
local data, derr = ensureData()
|
||||
if not data then return nil, derr end
|
||||
|
||||
local ok, decoded = pcall(GenSave.decode, bytes, data)
|
||||
if not ok then return nil, "decode failed: " .. tostring(decoded) end
|
||||
|
||||
-- checksum validation: GenSave.decode records a warning rather than
|
||||
-- throwing (so it can still read a foreign/corrupt save), but for the
|
||||
-- runtime import path a bad main-data checksum means the file is not a
|
||||
-- trustworthy save, so reject it.
|
||||
for _, w in ipairs(decoded.warnings or {}) do
|
||||
if tostring(w):find("checksum") then
|
||||
return nil, "save data checksum invalid (" .. tostring(w) .. ")"
|
||||
end
|
||||
end
|
||||
|
||||
return mergeDefaults(decoded, version)
|
||||
end
|
||||
|
||||
-- exportSav(saveTable) -> bytes, err
|
||||
-- Encodes a save table back to a raw 32768-byte SRAM image. Template-aware:
|
||||
-- if the table still carries the stashed import template (saveTable.rawImport)
|
||||
-- GenSave reproduces every unmodeled region from it; otherwise those regions
|
||||
-- are zero-filled. On failure returns nil + a message (never raises).
|
||||
function SaveConvert.exportSav(saveTable)
|
||||
if type(saveTable) ~= "table" then
|
||||
return nil, "expected a save table"
|
||||
end
|
||||
local data, derr = ensureData()
|
||||
if not data then return nil, derr end
|
||||
local ok, bytes = pcall(GenSave.encode, saveTable, data, nil)
|
||||
if not ok then return nil, "encode failed: " .. tostring(bytes) end
|
||||
return bytes
|
||||
end
|
||||
|
||||
return SaveConvert
|
||||
@@ -0,0 +1,579 @@
|
||||
-- Generated by tools/build_data.py. DO NOT EDIT.
|
||||
-- Gen1 text byte <-> glyph/token charmap (fixed-length name
|
||||
-- fields: player/rival/OT names, nicknames -- box/party mon
|
||||
-- names are NUL-free, '@' ($50) terminated, space-padded).
|
||||
-- byToken's key is the literal glyph for ordinary characters
|
||||
-- ("A", "é", ...) or a bracketed control token
|
||||
-- ("<PLAYER>", "@") for terminators/substitutions -- only
|
||||
-- the plain single-glyph entries are meaningful inside a name.
|
||||
return {
|
||||
byByte = {
|
||||
[0] = "<NULL>",
|
||||
[5] = "ガ",
|
||||
[6] = "ギ",
|
||||
[7] = "グ",
|
||||
[8] = "ゲ",
|
||||
[9] = "ゴ",
|
||||
[10] = "ザ",
|
||||
[11] = "ジ",
|
||||
[12] = "ズ",
|
||||
[13] = "ゼ",
|
||||
[14] = "ゾ",
|
||||
[15] = "ダ",
|
||||
[16] = "ヂ",
|
||||
[17] = "ヅ",
|
||||
[18] = "デ",
|
||||
[19] = "ド",
|
||||
[25] = "バ",
|
||||
[26] = "ビ",
|
||||
[27] = "ブ",
|
||||
[28] = "ボ",
|
||||
[38] = "が",
|
||||
[39] = "ぎ",
|
||||
[40] = "ぐ",
|
||||
[41] = "げ",
|
||||
[42] = "ご",
|
||||
[43] = "ざ",
|
||||
[44] = "じ",
|
||||
[45] = "ず",
|
||||
[46] = "ぜ",
|
||||
[47] = "ぞ",
|
||||
[48] = "だ",
|
||||
[49] = "ぢ",
|
||||
[50] = "づ",
|
||||
[51] = "で",
|
||||
[52] = "ど",
|
||||
[58] = "ば",
|
||||
[59] = "び",
|
||||
[60] = "ぶ",
|
||||
[61] = "べ",
|
||||
[62] = "ぼ",
|
||||
[64] = "パ",
|
||||
[65] = "ピ",
|
||||
[66] = "プ",
|
||||
[67] = "ポ",
|
||||
[68] = "ぱ",
|
||||
[69] = "ぴ",
|
||||
[70] = "ぷ",
|
||||
[71] = "ぺ",
|
||||
[72] = "ぽ",
|
||||
[73] = "<PAGE>",
|
||||
[74] = "<PKMN>",
|
||||
[75] = "<_CONT>",
|
||||
[76] = "<SCROLL>",
|
||||
[78] = "<NEXT>",
|
||||
[79] = "<LINE>",
|
||||
[80] = "@",
|
||||
[81] = "<PARA>",
|
||||
[82] = "<PLAYER>",
|
||||
[83] = "<RIVAL>",
|
||||
[84] = "#",
|
||||
[85] = "<CONT>",
|
||||
[86] = "<……>",
|
||||
[87] = "<DONE>",
|
||||
[88] = "<PROMPT>",
|
||||
[89] = "<TARGET>",
|
||||
[90] = "<USER>",
|
||||
[91] = "<PC>",
|
||||
[92] = "<TM>",
|
||||
[93] = "<TRAINER>",
|
||||
[94] = "<ROCKET>",
|
||||
[95] = "<DEXEND>",
|
||||
[96] = "<BOLD_A>",
|
||||
[97] = "<BOLD_B>",
|
||||
[98] = "<BOLD_C>",
|
||||
[99] = "<BOLD_D>",
|
||||
[100] = "<BOLD_E>",
|
||||
[101] = "<BOLD_F>",
|
||||
[102] = "<BOLD_G>",
|
||||
[103] = "<BOLD_H>",
|
||||
[104] = "<BOLD_I>",
|
||||
[105] = "<BOLD_V>",
|
||||
[106] = "<BOLD_S>",
|
||||
[107] = "<BOLD_L>",
|
||||
[108] = "<BOLD_M>",
|
||||
[109] = "<COLON>",
|
||||
[110] = "ぃ",
|
||||
[111] = "ぅ",
|
||||
[112] = "‘",
|
||||
[113] = "’",
|
||||
[114] = "“",
|
||||
[115] = "”",
|
||||
[116] = "·",
|
||||
[117] = "…",
|
||||
[118] = "ぁ",
|
||||
[119] = "ぇ",
|
||||
[120] = "ぉ",
|
||||
[121] = "┌",
|
||||
[122] = "─",
|
||||
[123] = "┐",
|
||||
[124] = "│",
|
||||
[125] = "└",
|
||||
[126] = "┘",
|
||||
[127] = " ",
|
||||
[128] = "A",
|
||||
[129] = "B",
|
||||
[130] = "C",
|
||||
[131] = "D",
|
||||
[132] = "E",
|
||||
[133] = "F",
|
||||
[134] = "G",
|
||||
[135] = "H",
|
||||
[136] = "I",
|
||||
[137] = "J",
|
||||
[138] = "K",
|
||||
[139] = "L",
|
||||
[140] = "M",
|
||||
[141] = "N",
|
||||
[142] = "O",
|
||||
[143] = "P",
|
||||
[144] = "Q",
|
||||
[145] = "R",
|
||||
[146] = "S",
|
||||
[147] = "T",
|
||||
[148] = "U",
|
||||
[149] = "V",
|
||||
[150] = "W",
|
||||
[151] = "X",
|
||||
[152] = "Y",
|
||||
[153] = "Z",
|
||||
[154] = "(",
|
||||
[155] = ")",
|
||||
[156] = ":",
|
||||
[157] = ";",
|
||||
[158] = "[",
|
||||
[159] = "]",
|
||||
[160] = "a",
|
||||
[161] = "b",
|
||||
[162] = "c",
|
||||
[163] = "d",
|
||||
[164] = "e",
|
||||
[165] = "f",
|
||||
[166] = "g",
|
||||
[167] = "h",
|
||||
[168] = "i",
|
||||
[169] = "j",
|
||||
[170] = "k",
|
||||
[171] = "l",
|
||||
[172] = "m",
|
||||
[173] = "n",
|
||||
[174] = "o",
|
||||
[175] = "p",
|
||||
[176] = "q",
|
||||
[177] = "r",
|
||||
[178] = "s",
|
||||
[179] = "t",
|
||||
[180] = "u",
|
||||
[181] = "v",
|
||||
[182] = "w",
|
||||
[183] = "x",
|
||||
[184] = "y",
|
||||
[185] = "z",
|
||||
[186] = "é",
|
||||
[187] = "'d",
|
||||
[188] = "'l",
|
||||
[189] = "'s",
|
||||
[190] = "'t",
|
||||
[191] = "'v",
|
||||
[192] = "た",
|
||||
[193] = "ち",
|
||||
[194] = "つ",
|
||||
[195] = "て",
|
||||
[196] = "と",
|
||||
[197] = "な",
|
||||
[198] = "に",
|
||||
[199] = "ぬ",
|
||||
[200] = "ね",
|
||||
[201] = "の",
|
||||
[202] = "は",
|
||||
[203] = "ひ",
|
||||
[204] = "ふ",
|
||||
[205] = "へ",
|
||||
[206] = "ほ",
|
||||
[207] = "ま",
|
||||
[208] = "み",
|
||||
[209] = "む",
|
||||
[210] = "め",
|
||||
[211] = "も",
|
||||
[212] = "や",
|
||||
[213] = "ゆ",
|
||||
[214] = "よ",
|
||||
[215] = "ら",
|
||||
[216] = "り",
|
||||
[217] = "る",
|
||||
[218] = "れ",
|
||||
[219] = "ろ",
|
||||
[220] = "わ",
|
||||
[221] = "を",
|
||||
[222] = "ん",
|
||||
[223] = "っ",
|
||||
[224] = "'",
|
||||
[225] = "<PK>",
|
||||
[226] = "<MN>",
|
||||
[227] = "-",
|
||||
[228] = "'r",
|
||||
[229] = "'m",
|
||||
[230] = "?",
|
||||
[231] = "!",
|
||||
[232] = ".",
|
||||
[233] = "ァ",
|
||||
[234] = "ゥ",
|
||||
[235] = "ェ",
|
||||
[236] = "▷",
|
||||
[237] = "▲",
|
||||
[238] = "▼",
|
||||
[239] = "♂",
|
||||
[240] = "<ED>",
|
||||
[241] = "×",
|
||||
[242] = "<DOT>",
|
||||
[243] = "/",
|
||||
[244] = ",",
|
||||
[245] = "♀",
|
||||
[246] = "0",
|
||||
[247] = "1",
|
||||
[248] = "2",
|
||||
[249] = "3",
|
||||
[250] = "4",
|
||||
[251] = "5",
|
||||
[252] = "6",
|
||||
[253] = "7",
|
||||
[254] = "8",
|
||||
[255] = "9",
|
||||
},
|
||||
byToken = {
|
||||
[" "] = 127,
|
||||
["!"] = 231,
|
||||
["#"] = 84,
|
||||
["'"] = 224,
|
||||
["'d"] = 187,
|
||||
["'l"] = 188,
|
||||
["'m"] = 229,
|
||||
["'r"] = 228,
|
||||
["'s"] = 189,
|
||||
["'t"] = 190,
|
||||
["'v"] = 191,
|
||||
["("] = 154,
|
||||
[")"] = 155,
|
||||
[","] = 244,
|
||||
["-"] = 227,
|
||||
["."] = 232,
|
||||
["/"] = 243,
|
||||
["0"] = 246,
|
||||
["1"] = 247,
|
||||
["2"] = 248,
|
||||
["3"] = 249,
|
||||
["4"] = 250,
|
||||
["5"] = 251,
|
||||
["6"] = 252,
|
||||
["7"] = 253,
|
||||
["8"] = 254,
|
||||
["9"] = 255,
|
||||
[":"] = 156,
|
||||
[";"] = 157,
|
||||
["<BOLD_A>"] = 96,
|
||||
["<BOLD_B>"] = 97,
|
||||
["<BOLD_C>"] = 98,
|
||||
["<BOLD_D>"] = 99,
|
||||
["<BOLD_E>"] = 100,
|
||||
["<BOLD_F>"] = 101,
|
||||
["<BOLD_G>"] = 102,
|
||||
["<BOLD_H>"] = 103,
|
||||
["<BOLD_I>"] = 104,
|
||||
["<BOLD_L>"] = 107,
|
||||
["<BOLD_M>"] = 108,
|
||||
["<BOLD_P>"] = 114,
|
||||
["<BOLD_S>"] = 106,
|
||||
["<BOLD_V>"] = 105,
|
||||
["<COLON>"] = 109,
|
||||
["<CONT>"] = 85,
|
||||
["<DEXEND>"] = 95,
|
||||
["<DONE>"] = 87,
|
||||
["<DOT>"] = 242,
|
||||
["<ED>"] = 240,
|
||||
["<ID>"] = 115,
|
||||
["<LINE>"] = 79,
|
||||
["<LV>"] = 110,
|
||||
["<MN>"] = 226,
|
||||
["<NEXT>"] = 78,
|
||||
["<NULL>"] = 0,
|
||||
["<PAGE>"] = 73,
|
||||
["<PARA>"] = 81,
|
||||
["<PC>"] = 91,
|
||||
["<PK>"] = 225,
|
||||
["<PKMN>"] = 74,
|
||||
["<PLAYER>"] = 82,
|
||||
["<PROMPT>"] = 88,
|
||||
["<RIVAL>"] = 83,
|
||||
["<ROCKET>"] = 94,
|
||||
["<SCROLL>"] = 76,
|
||||
["<TARGET>"] = 89,
|
||||
["<TM>"] = 92,
|
||||
["<TRAINER>"] = 93,
|
||||
["<USER>"] = 90,
|
||||
["<_CONT>"] = 75,
|
||||
["<to>"] = 112,
|
||||
["<……>"] = 86,
|
||||
["?"] = 230,
|
||||
["@"] = 80,
|
||||
A = 128,
|
||||
B = 129,
|
||||
C = 130,
|
||||
D = 131,
|
||||
E = 132,
|
||||
F = 133,
|
||||
G = 134,
|
||||
H = 135,
|
||||
I = 136,
|
||||
J = 137,
|
||||
K = 138,
|
||||
L = 139,
|
||||
M = 140,
|
||||
N = 141,
|
||||
O = 142,
|
||||
P = 143,
|
||||
Q = 144,
|
||||
R = 145,
|
||||
S = 146,
|
||||
T = 147,
|
||||
U = 148,
|
||||
V = 149,
|
||||
W = 150,
|
||||
X = 151,
|
||||
Y = 152,
|
||||
Z = 153,
|
||||
["["] = 158,
|
||||
["]"] = 159,
|
||||
a = 160,
|
||||
b = 161,
|
||||
c = 162,
|
||||
d = 163,
|
||||
e = 164,
|
||||
f = 165,
|
||||
g = 166,
|
||||
h = 167,
|
||||
i = 168,
|
||||
j = 169,
|
||||
k = 170,
|
||||
l = 171,
|
||||
m = 172,
|
||||
n = 173,
|
||||
o = 174,
|
||||
p = 175,
|
||||
q = 176,
|
||||
r = 177,
|
||||
s = 178,
|
||||
t = 179,
|
||||
u = 180,
|
||||
v = 181,
|
||||
w = 182,
|
||||
x = 183,
|
||||
y = 184,
|
||||
z = 185,
|
||||
["¥"] = 240,
|
||||
["·"] = 116,
|
||||
["×"] = 241,
|
||||
["é"] = 186,
|
||||
["‘"] = 112,
|
||||
["’"] = 113,
|
||||
["“"] = 114,
|
||||
["”"] = 115,
|
||||
["…"] = 117,
|
||||
["′"] = 96,
|
||||
["″"] = 97,
|
||||
["№"] = 116,
|
||||
["⋯"] = 117,
|
||||
["─"] = 122,
|
||||
["│"] = 124,
|
||||
["┌"] = 121,
|
||||
["┐"] = 123,
|
||||
["└"] = 125,
|
||||
["┘"] = 126,
|
||||
["▲"] = 237,
|
||||
["▶"] = 237,
|
||||
["▷"] = 236,
|
||||
["▼"] = 238,
|
||||
["♀"] = 245,
|
||||
["♂"] = 239,
|
||||
[" "] = 127,
|
||||
["。"] = 232,
|
||||
["「"] = 112,
|
||||
["」"] = 113,
|
||||
["『"] = 114,
|
||||
["』"] = 115,
|
||||
["ぁ"] = 118,
|
||||
["あ"] = 177,
|
||||
["ぃ"] = 110,
|
||||
["い"] = 178,
|
||||
["ぅ"] = 111,
|
||||
["う"] = 179,
|
||||
["ぇ"] = 119,
|
||||
["え"] = 180,
|
||||
["ぉ"] = 120,
|
||||
["お"] = 181,
|
||||
["か"] = 182,
|
||||
["が"] = 38,
|
||||
["き"] = 183,
|
||||
["ぎ"] = 39,
|
||||
["く"] = 184,
|
||||
["ぐ"] = 40,
|
||||
["け"] = 185,
|
||||
["げ"] = 41,
|
||||
["こ"] = 186,
|
||||
["ご"] = 42,
|
||||
["さ"] = 187,
|
||||
["ざ"] = 43,
|
||||
["し"] = 188,
|
||||
["じ"] = 44,
|
||||
["す"] = 189,
|
||||
["ず"] = 45,
|
||||
["せ"] = 190,
|
||||
["ぜ"] = 46,
|
||||
["そ"] = 191,
|
||||
["ぞ"] = 47,
|
||||
["た"] = 192,
|
||||
["だ"] = 48,
|
||||
["ち"] = 193,
|
||||
["ぢ"] = 49,
|
||||
["っ"] = 223,
|
||||
["つ"] = 194,
|
||||
["づ"] = 50,
|
||||
["て"] = 195,
|
||||
["で"] = 51,
|
||||
["と"] = 196,
|
||||
["ど"] = 52,
|
||||
["な"] = 197,
|
||||
["に"] = 198,
|
||||
["ぬ"] = 199,
|
||||
["ね"] = 200,
|
||||
["の"] = 201,
|
||||
["は"] = 202,
|
||||
["ば"] = 58,
|
||||
["ぱ"] = 68,
|
||||
["ひ"] = 203,
|
||||
["び"] = 59,
|
||||
["ぴ"] = 69,
|
||||
["ふ"] = 204,
|
||||
["ぶ"] = 60,
|
||||
["ぷ"] = 70,
|
||||
["へ"] = 205,
|
||||
["べ"] = 61,
|
||||
["ぺ"] = 71,
|
||||
["ほ"] = 206,
|
||||
["ぼ"] = 62,
|
||||
["ぽ"] = 72,
|
||||
["ま"] = 207,
|
||||
["み"] = 208,
|
||||
["む"] = 209,
|
||||
["め"] = 210,
|
||||
["も"] = 211,
|
||||
["ゃ"] = 224,
|
||||
["や"] = 212,
|
||||
["ゅ"] = 225,
|
||||
["ゆ"] = 213,
|
||||
["ょ"] = 226,
|
||||
["よ"] = 214,
|
||||
["ら"] = 215,
|
||||
["り"] = 216,
|
||||
["る"] = 217,
|
||||
["れ"] = 218,
|
||||
["ろ"] = 219,
|
||||
["わ"] = 220,
|
||||
["を"] = 221,
|
||||
["ん"] = 222,
|
||||
["ァ"] = 233,
|
||||
["ア"] = 128,
|
||||
["ィ"] = 176,
|
||||
["イ"] = 129,
|
||||
["ゥ"] = 234,
|
||||
["ウ"] = 130,
|
||||
["ェ"] = 235,
|
||||
["エ"] = 131,
|
||||
["ォ"] = 244,
|
||||
["オ"] = 132,
|
||||
["カ"] = 133,
|
||||
["ガ"] = 5,
|
||||
["キ"] = 134,
|
||||
["ギ"] = 6,
|
||||
["ク"] = 135,
|
||||
["グ"] = 7,
|
||||
["ケ"] = 136,
|
||||
["ゲ"] = 8,
|
||||
["コ"] = 137,
|
||||
["ゴ"] = 9,
|
||||
["サ"] = 138,
|
||||
["ザ"] = 10,
|
||||
["シ"] = 139,
|
||||
["ジ"] = 11,
|
||||
["ス"] = 140,
|
||||
["ズ"] = 12,
|
||||
["セ"] = 141,
|
||||
["ゼ"] = 13,
|
||||
["ソ"] = 142,
|
||||
["ゾ"] = 14,
|
||||
["タ"] = 143,
|
||||
["ダ"] = 15,
|
||||
["チ"] = 144,
|
||||
["ヂ"] = 16,
|
||||
["ッ"] = 172,
|
||||
["ツ"] = 145,
|
||||
["ヅ"] = 17,
|
||||
["テ"] = 146,
|
||||
["デ"] = 18,
|
||||
["ト"] = 147,
|
||||
["ド"] = 19,
|
||||
["ナ"] = 148,
|
||||
["ニ"] = 149,
|
||||
["ヌ"] = 150,
|
||||
["ネ"] = 151,
|
||||
["ノ"] = 152,
|
||||
["ハ"] = 153,
|
||||
["バ"] = 25,
|
||||
["パ"] = 64,
|
||||
["ヒ"] = 154,
|
||||
["ビ"] = 26,
|
||||
["ピ"] = 65,
|
||||
["フ"] = 155,
|
||||
["ブ"] = 27,
|
||||
["プ"] = 66,
|
||||
["ホ"] = 156,
|
||||
["ボ"] = 28,
|
||||
["ポ"] = 67,
|
||||
["マ"] = 157,
|
||||
["ミ"] = 158,
|
||||
["ム"] = 159,
|
||||
["メ"] = 160,
|
||||
["モ"] = 161,
|
||||
["ャ"] = 173,
|
||||
["ヤ"] = 162,
|
||||
["ュ"] = 174,
|
||||
["ユ"] = 163,
|
||||
["ョ"] = 175,
|
||||
["ヨ"] = 164,
|
||||
["ラ"] = 165,
|
||||
["ル"] = 166,
|
||||
["レ"] = 167,
|
||||
["ロ"] = 168,
|
||||
["ワ"] = 169,
|
||||
["ヲ"] = 170,
|
||||
["ン"] = 171,
|
||||
["ー"] = 227,
|
||||
["円"] = 240,
|
||||
["!"] = 231,
|
||||
["."] = 242,
|
||||
["/"] = 243,
|
||||
["0"] = 246,
|
||||
["1"] = 247,
|
||||
["2"] = 248,
|
||||
["3"] = 249,
|
||||
["4"] = 250,
|
||||
["5"] = 251,
|
||||
["6"] = 252,
|
||||
["7"] = 253,
|
||||
["8"] = 254,
|
||||
["9"] = 255,
|
||||
["?"] = 230,
|
||||
["゙"] = 229,
|
||||
["゚"] = 228,
|
||||
},
|
||||
source = "pokered constants/charmap.asm",
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user