The new experience (#201)

* new launcher and save converts and pipeline

* fixing bugs
This commit is contained in:
bryanthaboi
2026-07-25 12:36:53 -04:00
committed by GitHub
parent 6625391f76
commit 3069b2e2a9
135 changed files with 16596 additions and 1508 deletions
+813
View File
@@ -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
+189
View File
@@ -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
+579
View File
@@ -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,
[""] = 246,
[""] = 247,
[""] = 248,
[""] = 249,
[""] = 250,
[""] = 251,
[""] = 252,
[""] = 253,
[""] = 254,
[""] = 255,
[""] = 230,
[""] = 229,
[""] = 228,
},
source = "pokered constants/charmap.asm",
}
File diff suppressed because it is too large Load Diff