mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-26 13:33:27 -04:00
Extract FRLG gEggMoves so Gen 3 mods see egg moves
The Gen 3 extractor emitted eggCycles/eggGroups but never egg moves, so the
gen3_dexnav hidden-mon roll was inert on FireRed: a hidden mon kept its
level-up moves and could never gain an egg move.
Decode the ROM's own gEggMoves table (pokefirered/src/data/pokemon/
egg_moves.h) into pokemon/egg_moves.lua and expose it the same way as
learnsets/tmhm:
- Versions: EGG_MOVES = 0x25EF0C plus the 20000 species offset, the 0xFFFF
run terminator and a defensive per-species cap. The table is one flat u16
stream of `{ species + 20000, move..., 0xFFFF }` runs with no final
terminator, so the first word that is neither a header nor a plausible move
id ends the scan.
- PokemonExtract: extract_egg_moves + write_egg_moves_lua, written to the
cache by run(), required by ready() so a stale cache re-extracts, and
returned as pack.eggMoves. FORMAT_VERSION 3 -> 4.
- Pokemon.eggMoves(species): runtime accessor with the same species coercion
as Pokemon.learnset; nil for a species with no egg move.
- Schemas: eggMoves on monTables/monRecord/writeMon and the Gen 3 species
field list, so a mod reads record.eggMoves as move ids and writes names back.
- Versions.CACHE_VERSION 98 -> 99 to force re-extraction.
The table is sparse: species without egg moves are absent rather than an
empty list, all the way from the ROM to the mod record.
Validated against a supported FireRed USA 1.0 dump: 165 species, 973 moves,
<=8 per species, Bulbasaur {113,130,219,204,80,345,320,174}, Mankey
{157,193,96,68,179,251,279,265} (no Toxic), Mew nil.
Tests: new tests/engine/game3_egg_moves.lua (31 checks ROM-free, 39 with a
ROM), gate_gen3_mod_api 734/734, engine tier 587/587, modkit tier 37/37,
lint --gate clean.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -22,6 +22,7 @@ Pokemon._abilityNames = nil
|
||||
Pokemon._speciesMeta = nil
|
||||
Pokemon._moveNames = nil
|
||||
Pokemon._learnsets = nil
|
||||
Pokemon._eggMoves = nil
|
||||
Pokemon._evolutions = nil
|
||||
Pokemon._tmhm = nil
|
||||
Pokemon._dex = nil
|
||||
@@ -124,6 +125,7 @@ function Pokemon.install(cache)
|
||||
Pokemon._speciesMeta = nil
|
||||
Pokemon._moveNames = nil
|
||||
Pokemon._learnsets = nil
|
||||
Pokemon._eggMoves = nil
|
||||
Pokemon._evolutions = nil
|
||||
Pokemon._tmhm = nil
|
||||
Pokemon._dex = nil
|
||||
@@ -144,6 +146,7 @@ function Pokemon.install(cache)
|
||||
Pokemon._speciesMeta = load_lua(c, root .. "/meta.lua")
|
||||
Pokemon._moveNames = load_lua(c, root .. "/move_names.lua")
|
||||
Pokemon._learnsets = load_lua(c, root .. "/learnsets.lua")
|
||||
Pokemon._eggMoves = load_lua(c, root .. "/egg_moves.lua")
|
||||
Pokemon._evolutions = load_lua(c, root .. "/evolutions.lua")
|
||||
Pokemon._tmhm = load_lua(c, root .. "/tmhm.lua")
|
||||
Pokemon._dex = load_lua(c, root .. "/dex.lua")
|
||||
@@ -203,6 +206,7 @@ function Pokemon.invalidate()
|
||||
Pokemon._speciesMeta = nil
|
||||
Pokemon._moveNames = nil
|
||||
Pokemon._learnsets = nil
|
||||
Pokemon._eggMoves = nil
|
||||
Pokemon._evolutions = nil
|
||||
Pokemon._tmhm = nil
|
||||
Pokemon._dex = nil
|
||||
@@ -498,6 +502,19 @@ function Pokemon.learnset(species)
|
||||
return (Pokemon._learnsets and Pokemon._learnsets[species]) or {}
|
||||
end
|
||||
|
||||
--- Egg move ids for a species (FRLG gEggMoves), or nil when it has none.
|
||||
--- Mirrors Pokemon.learnset's species coercion so mods can pass either form.
|
||||
function Pokemon.eggMoves(species)
|
||||
if type(species) == "table" then species = Pokemon.speciesOf(species) end
|
||||
if type(species) == "string" then species = Pokemon.speciesFromName(species) or tonumber(species) end
|
||||
species = tonumber(species)
|
||||
if not species then return nil end
|
||||
if not Pokemon._eggMoves then Pokemon.install(Pokemon._cache) end
|
||||
local list = Pokemon._eggMoves and Pokemon._eggMoves[species]
|
||||
if type(list) ~= "table" or #list == 0 then return nil end
|
||||
return list
|
||||
end
|
||||
|
||||
function Pokemon.evolutions(species)
|
||||
if type(species) == "table" then species = Pokemon.speciesOf(species) end
|
||||
if type(species) == "string" then species = Pokemon.speciesFromName(species) or tonumber(species) end
|
||||
|
||||
@@ -9,7 +9,7 @@ local Lz77 = require("src.import.gba.lz77")
|
||||
local PokemonExtract = {}
|
||||
|
||||
PokemonExtract.MAGIC = "SVPK"
|
||||
PokemonExtract.FORMAT_VERSION = 3
|
||||
PokemonExtract.FORMAT_VERSION = 4
|
||||
PokemonExtract.CACHE_SUB = "pokemon"
|
||||
|
||||
local function default_cache_root()
|
||||
@@ -390,6 +390,28 @@ local function write_learnsets_lua(learnsets)
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
local function write_egg_moves_lua(eggMoves)
|
||||
local lines = {
|
||||
"-- Auto-generated FRLG gEggMoves: [species] = { move ids }.",
|
||||
"return {",
|
||||
}
|
||||
local ids = {}
|
||||
for id in pairs(eggMoves) do
|
||||
if type(id) == "number" then ids[#ids + 1] = id end
|
||||
end
|
||||
table.sort(ids)
|
||||
for _, id in ipairs(ids) do
|
||||
local list = eggMoves[id] or {}
|
||||
if #list > 0 then
|
||||
lines[#lines + 1] = string.format(" [%d] = { %s },",
|
||||
id, table.concat(list, ", "))
|
||||
end
|
||||
end
|
||||
lines[#lines + 1] = "}"
|
||||
lines[#lines + 1] = ""
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
local function write_evolutions_lua(evos)
|
||||
local lines = {
|
||||
"-- Auto-generated FRLG gEvolutionTable (method, param, targetSpecies).",
|
||||
@@ -464,6 +486,48 @@ local function write_dex_lua(dex)
|
||||
return table.concat(lines, "\n")
|
||||
end
|
||||
|
||||
--- Decode gEggMoves into `{ [species] = { moveId, … } }`.
|
||||
---
|
||||
--- Not a pointer table: one flat u16 stream of runs, each opened by
|
||||
--- `species + EGG_MOVES_SPECIES_OFFSET` (a move id is always < 355, so the
|
||||
--- offset is what tells a header from a move) and closed by 0xFFFF. The table
|
||||
--- stops after its last run, so the first word that is neither a header nor a
|
||||
--- plausible move id ends the scan. Species without egg moves are absent.
|
||||
local function extract_egg_moves(rom, num)
|
||||
local eggMoves = {}
|
||||
local base = Versions.EGG_MOVES
|
||||
if not base then return eggMoves end
|
||||
local offset = Versions.EGG_MOVES_SPECIES_OFFSET or 20000
|
||||
local terminator = Versions.EGG_MOVES_TERMINATOR or 0xFFFF
|
||||
local maxMoves = Versions.EGG_MOVES_MAX or 16
|
||||
local moveCount = Versions.MOVES_COUNT or 355
|
||||
local limit = math.min(rom.size or Versions.ROM_SIZE or base, base + 0x10000)
|
||||
local species
|
||||
local o = base
|
||||
while o + 1 < limit do
|
||||
local word = rom:u16(o)
|
||||
o = o + 2
|
||||
if word == terminator then
|
||||
if not species then break end -- the table's own terminator
|
||||
species = nil
|
||||
elseif word >= offset then
|
||||
local id = word - offset
|
||||
species = (id < num) and id or nil
|
||||
if species then eggMoves[species] = eggMoves[species] or {} end
|
||||
elseif species and word > 0 and word < moveCount then
|
||||
local list = eggMoves[species]
|
||||
if #list < maxMoves then list[#list + 1] = word end
|
||||
else
|
||||
break -- not a gEggMoves stream: stop rather than invent data
|
||||
end
|
||||
end
|
||||
return eggMoves
|
||||
end
|
||||
|
||||
-- Exposed for tests (tests/engine/game3_egg_moves.lua).
|
||||
PokemonExtract.eggMovesFromRom = extract_egg_moves
|
||||
PokemonExtract.writeEggMovesLua = write_egg_moves_lua
|
||||
|
||||
--- Extract full pack into cache under {cacheRoot}/pokemon/.
|
||||
function PokemonExtract.run(rom, cache, opts)
|
||||
opts = opts or {}
|
||||
@@ -674,6 +738,10 @@ function PokemonExtract.run(rom, cache, opts)
|
||||
}
|
||||
end
|
||||
|
||||
-- Egg moves (gEggMoves): sparse, so a species with no egg move is absent
|
||||
-- rather than an empty list.
|
||||
local eggMoves = extract_egg_moves(rom, num)
|
||||
|
||||
-- National dex entries (category / height / weight)
|
||||
local dex = {}
|
||||
local dexBase = Versions.POKEDEX_ENTRIES or 0x44E850
|
||||
@@ -751,6 +819,7 @@ function PokemonExtract.run(rom, cache, opts)
|
||||
cache:write(root .. "/learnsets.lua", write_learnsets_lua(learnsets))
|
||||
cache:write(root .. "/evolutions.lua", write_evolutions_lua(evolutions))
|
||||
cache:write(root .. "/tmhm.lua", write_tmhm_lua(tmhm, tmMoves))
|
||||
cache:write(root .. "/egg_moves.lua", write_egg_moves_lua(eggMoves))
|
||||
cache:write(root .. "/dex.lua", write_dex_lua(dex))
|
||||
cache:write(root .. "/manifest.lua", write_manifest(num, Versions.POKEMON_VERSION))
|
||||
|
||||
@@ -859,6 +928,7 @@ function PokemonExtract.run(rom, cache, opts)
|
||||
abilityNames = abilityNames,
|
||||
moveNames = moveNames,
|
||||
learnsets = learnsets,
|
||||
eggMoves = eggMoves,
|
||||
evolutions = evolutions,
|
||||
battleMoves = battle and battle.pack,
|
||||
partyChrome = chrome,
|
||||
@@ -906,6 +976,7 @@ function PokemonExtract.ready(cache, cacheRoot)
|
||||
and valid_file(root .. "/names.lua", 20)
|
||||
and valid_file(root .. "/stats.lua", 20)
|
||||
and valid_file(root .. "/learnsets.lua", 20)
|
||||
and valid_file(root .. "/egg_moves.lua", 20)
|
||||
and valid_file(root .. "/move_names.lua", 20)
|
||||
and valid_file(root .. "/party/slot_main.rgba", 80 * 56 * 4)
|
||||
and valid_file(root .. "/summary/page_info.rgba", 240 * 160 * 4)
|
||||
|
||||
@@ -24,7 +24,8 @@ Versions.ROM_SIZE = 16777216
|
||||
-- v90: fanfare audio cues, emote cues (0x62-0x66), pause menu YES/NO exit & main menu launcher exit
|
||||
-- v91: ROM-native Help topics, context lists, text and chrome.
|
||||
-- v93: original furniture/sign scripts and metatile interaction behaviors.
|
||||
Versions.CACHE_VERSION = 98
|
||||
-- v99: gEggMoves → pokemon/egg_moves.lua (hidden-mon egg moves were inert).
|
||||
Versions.CACHE_VERSION = 99
|
||||
Versions.NATIVE_VERSION = 5
|
||||
Versions.OW_VERSION = 1
|
||||
Versions.ANIM_VERSION = 1
|
||||
@@ -97,6 +98,15 @@ Versions.MOVE_NAMES = 0x247094 -- gMoveNames
|
||||
Versions.MOVE_NAME_LENGTH = 12 -- +1 EOS → 13-byte stride
|
||||
Versions.MOVE_DESCRIPTIONS = 0x4886E8 -- gMoveDescriptionPointers (354 pointers)
|
||||
Versions.LEVEL_UP_LEARNSETS = 0x25D7B4 -- gLevelUpLearnsets pointer table
|
||||
-- gEggMoves (pokefirered/src/data/pokemon/egg_moves.h). Not a pointer table:
|
||||
-- one flat u16 stream of `{ species + EGG_MOVES_SPECIES_OFFSET, move…, 0xFFFF }`
|
||||
-- runs, each run ended by EGG_MOVES_TERMINATOR; the table simply stops after the
|
||||
-- last run, so the following symbol's data ends the scan. Only species that
|
||||
-- actually have an egg move appear, so it is sparse.
|
||||
Versions.EGG_MOVES = 0x25EF0C -- gEggMoves (FireRed USA 1.0)
|
||||
Versions.EGG_MOVES_SPECIES_OFFSET = 20000
|
||||
Versions.EGG_MOVES_TERMINATOR = 0xFFFF
|
||||
Versions.EGG_MOVES_MAX = 16 -- per species; the ROM's real max is 8
|
||||
Versions.EVOLUTION_TABLE = 0x259754 -- gEvolutionTable
|
||||
Versions.EVOS_PER_MON = 5
|
||||
Versions.EVOLUTION_ENTRY_SIZE = 8 -- method,u16 param,u16 target,u16 pad
|
||||
|
||||
@@ -968,6 +968,7 @@ local function monTables(base)
|
||||
abilityNames = tableAt(base, "_abilityNames", "abilityNames"),
|
||||
meta = tableAt(base, "_speciesMeta", "speciesMeta", "meta"),
|
||||
learnsets = tableAt(base, "_learnsets", "learnsets"),
|
||||
eggMoves = tableAt(base, "_eggMoves", "eggMoves"),
|
||||
evolutions = tableAt(base, "_evolutions", "evolutions"),
|
||||
tmhm = tableAt(base, "_tmhm", "tmhm"),
|
||||
dex = tableAt(base, "_dex", "dex"),
|
||||
@@ -1132,6 +1133,13 @@ local function monRecord(base, id)
|
||||
end
|
||||
end
|
||||
record.learnset = learnset
|
||||
-- gEggMoves is sparse: a species with no egg move has no key at all.
|
||||
local eggMoves = {}
|
||||
for _, move in ipairs(t.eggMoves and t.eggMoves[num] or {}) do
|
||||
local id = toId(moveIndex, move)
|
||||
if id then eggMoves[#eggMoves + 1] = id end
|
||||
end
|
||||
if #eggMoves > 0 then record.eggMoves = eggMoves end
|
||||
local evolutions = {}
|
||||
for _, row in ipairs(t.evolutions and t.evolutions[num] or {}) do
|
||||
local method = row.method or row[1]
|
||||
@@ -1252,6 +1260,14 @@ local function writeMon(target, t, num, value)
|
||||
end
|
||||
t.learnsets[num] = rows
|
||||
end
|
||||
if type(value.eggMoves) == "table" and t.eggMoves then
|
||||
local list = {}
|
||||
for _, move in ipairs(value.eggMoves) do
|
||||
local moveNum = toNum(moveIndex, move)
|
||||
if moveNum then list[#list + 1] = moveNum end
|
||||
end
|
||||
t.eggMoves[num] = list
|
||||
end
|
||||
if type(value.evolutions) == "table" and t.evolutions then
|
||||
local rows = {}
|
||||
for _, row in ipairs(value.evolutions) do
|
||||
@@ -1772,6 +1788,7 @@ R.pokemon = {
|
||||
abilities = f.opt(f.list(f.union{ f.str, f.int(0, 255) })),
|
||||
learnset = f.list(f.rec{ level = f.int(1, 100), move = f.id("moves") }),
|
||||
tmhm = f.opt(f.list(f.id("moves"))),
|
||||
eggMoves = f.opt(f.list(f.id("moves"))),
|
||||
evolutions = f.list(f.rec{ method = f.id("evolution_methods"),
|
||||
species = f.id("pokemon"),
|
||||
level = f.opt(f.int(0, 100)),
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
-- FRLG egg moves (gEggMoves), end to end: the ROM stream the extractor
|
||||
-- decodes, the cache file it writes and the accessor a mod reads.
|
||||
--
|
||||
-- Why this exists: the Gen 3 pack used to carry no egg moves at all, so a
|
||||
-- mod asking for a species' egg-move list got nothing back and a hidden-mon
|
||||
-- generator fell through to whatever else it had -- the TM/HM pool. The
|
||||
-- extractor now reads gEggMoves and nothing else, and these checks pin that.
|
||||
--
|
||||
-- luajit tests/engine/game3_egg_moves.lua [path/to/firered.gba]
|
||||
--
|
||||
-- The real-ROM block needs the supported USA 1.0 dump; without it the
|
||||
-- synthetic checks below still cover the decode, the writer and the accessor.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
local Versions = require("src.import.gba.versions")
|
||||
local PokemonExtract = require("src.import.gba.pokemon_extract")
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
|
||||
local BASE = Versions.EGG_MOVES
|
||||
local OFF = Versions.EGG_MOVES_SPECIES_OFFSET
|
||||
local TERM = Versions.EGG_MOVES_TERMINATOR
|
||||
local NUM = Versions.NUM_SPECIES
|
||||
|
||||
eq(BASE, 0x25EF0C, "gEggMoves is pinned to the FireRed USA 1.0 offset")
|
||||
eq(OFF, 20000, "species headers carry EGG_MOVES_SPECIES_OFFSET")
|
||||
eq(TERM, 0xFFFF, "EGG_MOVES_TERMINATOR is 0xFFFF")
|
||||
check(Versions.CACHE_VERSION >= 99,
|
||||
"the cache version is bumped past the packs that had no egg_moves.lua")
|
||||
eq(PokemonExtract.FORMAT_VERSION, 4, "the pokemon pack format version is bumped")
|
||||
|
||||
-- ------------------------------------------------------------------ decode
|
||||
-- A ROM whose gEggMoves stream is `words` and which reads 0 everywhere else:
|
||||
-- a decode that wandered past the table would pick up those zeros instead of
|
||||
-- inventing moves out of neighbouring data.
|
||||
local function romOf(words)
|
||||
local at = {}
|
||||
for i, word in ipairs(words) do
|
||||
local off = BASE + (i - 1) * 2
|
||||
at[off] = word % 256
|
||||
at[off + 1] = math.floor(word / 256)
|
||||
end
|
||||
return {
|
||||
size = Versions.ROM_SIZE,
|
||||
get = function(_, off) return at[off] or 0 end,
|
||||
u16 = function(self, off) return self:get(off) + self:get(off + 1) * 256 end,
|
||||
}
|
||||
end
|
||||
|
||||
local function decode(words)
|
||||
return PokemonExtract.eggMovesFromRom(romOf(words), NUM)
|
||||
end
|
||||
|
||||
local function count(t)
|
||||
local n = 0
|
||||
for _ in pairs(t) do n = n + 1 end
|
||||
return n
|
||||
end
|
||||
|
||||
do -- two runs, each closed by its own terminator
|
||||
local eggMoves = decode({ OFF + 4, 57, 10, TERM, OFF + 16, 33, TERM })
|
||||
eq(count(eggMoves), 2, "two species runs decode to two species")
|
||||
eq(table.concat(eggMoves[4], ","), "57,10", "a run keeps its moves in ROM order")
|
||||
eq(table.concat(eggMoves[16], ","), "33", "and the second run keeps its own")
|
||||
end
|
||||
|
||||
do -- a run with no moves is an empty list, not a missing key
|
||||
local eggMoves = decode({ OFF + 5, TERM, OFF + 6, 12, TERM })
|
||||
eq(count(eggMoves), 2, "a moveless run still opens a species")
|
||||
eq(#eggMoves[5], 0, "but carries no move")
|
||||
eq(table.concat(eggMoves[6], ","), "12", "and the next run is unaffected")
|
||||
end
|
||||
|
||||
do -- a species that appears twice keeps both runs' moves
|
||||
local eggMoves = decode({ OFF + 4, 57, TERM, OFF + 4, 10, TERM })
|
||||
eq(count(eggMoves), 1, "a repeated header is one species")
|
||||
eq(table.concat(eggMoves[4], ","), "57,10", "with both runs' moves")
|
||||
end
|
||||
|
||||
do -- a terminator with no run open is the table's end
|
||||
eq(count(decode({ TERM, OFF + 4, 57, TERM })), 0,
|
||||
"a leading terminator ends the scan before any species")
|
||||
end
|
||||
|
||||
do -- and a word that is neither a header nor a move ends it too
|
||||
local eggMoves = decode({ OFF + 4, 57, 0, OFF + 16, 33, TERM })
|
||||
eq(table.concat(eggMoves[4] or {}, ","), "57", "moves before the stray word decode")
|
||||
eq(eggMoves[16], nil, "and nothing after it is invented")
|
||||
end
|
||||
|
||||
do -- a header past the species count is not a species
|
||||
eq(count(decode({ OFF + NUM, 57, TERM })), 0,
|
||||
"an out-of-range species header decodes to nothing")
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------ writer
|
||||
do
|
||||
local src = PokemonExtract.writeEggMovesLua({
|
||||
[16] = { 10, 33 }, [4] = { 57 }, [5] = {},
|
||||
})
|
||||
local chunk = load(src, "@egg_moves.lua", "t", {})
|
||||
check(chunk ~= nil, "the writer emits loadable lua")
|
||||
local written = chunk and chunk() or {}
|
||||
eq(table.concat(written[16], ","), "10,33", "the writer keeps a list intact")
|
||||
eq(table.concat(written[4], ","), "57", "and a one-move list")
|
||||
eq(written[5], nil, "an empty list is left out (gEggMoves is sparse)")
|
||||
check(src:find("[4] = { 57 },", 1, true) ~= nil, "species keys are emitted")
|
||||
local at4 = src:find("[4] = { 57 },", 1, true)
|
||||
local at16 = src:find("[16] = { 10, 33 },", 1, true)
|
||||
check(at4 and at16 and at4 < at16, "species keys come out in ascending order")
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------- accessor
|
||||
do
|
||||
-- the cache the runtime installs from, with just the files this touches
|
||||
local files = {
|
||||
["data/generated/gba/pokemon/names.lua"] = 'return {\n' ..
|
||||
' [4] = "CHARMANDER",\n [16] = "PIDGEY",\n}\n',
|
||||
["data/generated/gba/pokemon/egg_moves.lua"] =
|
||||
PokemonExtract.writeEggMovesLua({ [4] = { 57 }, [16] = { 10, 33 } }),
|
||||
}
|
||||
local cache = { read = function(_, rel) return files[rel] end }
|
||||
|
||||
Pokemon.install(cache)
|
||||
eq(Pokemon._eggMoves and Pokemon._eggMoves[4][1], 57,
|
||||
"install() loads pokemon/egg_moves.lua into _eggMoves")
|
||||
eq(table.concat(Pokemon.eggMoves(16), ","), "10,33",
|
||||
"eggMoves(species) returns the list")
|
||||
eq(Pokemon.eggMoves("PIDGEY") and #Pokemon.eggMoves("PIDGEY"), 2,
|
||||
"eggMoves accepts a species name")
|
||||
eq(Pokemon.eggMoves({ species = 4 }) and Pokemon.eggMoves({ species = 4 })[1], 57,
|
||||
"eggMoves accepts a mon table")
|
||||
eq(Pokemon.eggMoves(151), nil,
|
||||
"a species with no egg moves returns nil, not an empty list")
|
||||
eq(Pokemon.eggMoves("NOT A SPECIES"), nil, "an unknown name returns nil")
|
||||
eq(Pokemon.eggMoves(nil), nil, "and so does no argument at all")
|
||||
|
||||
Pokemon.invalidate()
|
||||
eq(Pokemon._eggMoves, nil, "invalidate() drops the loaded list")
|
||||
end
|
||||
|
||||
-- ----------------------------------------------------------------- real ROM
|
||||
-- Pinned against the supported FireRed USA 1.0 dump; skipped otherwise.
|
||||
local path = arg and arg[1]
|
||||
if not path then
|
||||
print("SKIP real-ROM egg-move block: pass a FireRed .gba path explicitly")
|
||||
else
|
||||
local FileIO = require("src.import.gba.file_io")
|
||||
local imports = FileIO.makeImports(path, "test")
|
||||
local rom = assert(require("src.import.gba.rom").open(imports, "firered"))
|
||||
local eggMoves = PokemonExtract.eggMovesFromRom(rom, NUM)
|
||||
|
||||
eq(count(eggMoves), 165, "the ROM's gEggMoves names 165 species")
|
||||
local moves = 0
|
||||
local widest = 0
|
||||
for _, list in pairs(eggMoves) do
|
||||
moves = moves + #list
|
||||
if #list > widest then widest = #list end
|
||||
end
|
||||
eq(moves, 973, "holding 973 egg moves in total")
|
||||
check(widest <= 8, "no species has more than the ROM's 8 egg moves")
|
||||
eq(table.concat(eggMoves[1], ","), "113,130,219,204,80,345,320,174",
|
||||
"Bulbasaur's egg moves match the ROM")
|
||||
eq(table.concat(eggMoves[411], ","), "50,174,95,138",
|
||||
"so do Deoxys'")
|
||||
local mankey = eggMoves[56] or {}
|
||||
check(#mankey == 8, "Mankey has 8 egg moves")
|
||||
local toxic = false
|
||||
for _, move in ipairs(mankey) do
|
||||
if move == 92 then toxic = true end
|
||||
end
|
||||
check(not toxic, "and Toxic (a TM) is not one of them")
|
||||
|
||||
-- the whole path a mod depends on: ROM → run() → pokemon/egg_moves.lua →
|
||||
-- the runtime accessor. cacheRoot is the runtime's own root so that
|
||||
-- Pokemon.install reads exactly the file the extractor just wrote.
|
||||
local root = require("src.import.gba.extract_island1").CACHE_ROOT
|
||||
local files = {}
|
||||
local cache = {
|
||||
write = function(_, rel, bytes) files[rel] = bytes; return true end,
|
||||
exists = function(_, rel) return files[rel] ~= nil end,
|
||||
read = function(_, rel) return files[rel] end,
|
||||
}
|
||||
local pack = PokemonExtract.run(rom, cache, { cacheRoot = root })
|
||||
check(cache:exists(root .. "/pokemon/egg_moves.lua"),
|
||||
"run() writes pokemon/egg_moves.lua into the cache")
|
||||
eq(count(pack.eggMoves or {}), 165, "and returns the same 165 species")
|
||||
|
||||
Pokemon.install(cache)
|
||||
eq(table.concat(Pokemon.eggMoves(1), ","), "113,130,219,204,80,345,320,174",
|
||||
"the written cache loads back into the runtime")
|
||||
eq(table.concat(Pokemon.eggMoves(56), ","), "157,193,96,68,179,251,279,265",
|
||||
"so a mod reading Mankey's egg moves gets the ROM's list")
|
||||
eq(Pokemon.eggMoves(151), nil, "and a species without egg moves gets nil")
|
||||
|
||||
imports:_close()
|
||||
end
|
||||
|
||||
T.finish("game3_egg_moves")
|
||||
@@ -92,6 +92,8 @@ do
|
||||
"resolving a derived spec again is a no-op")
|
||||
T.check(gen3.fields.learnset ~= nil and gen3.fields.baseStats.fields.specialAttack ~= nil,
|
||||
"the Gen 3 species shape is folded onto `fields`")
|
||||
T.check(gen3.fields.eggMoves ~= nil,
|
||||
"the Gen 3 species shape carries the gEggMoves field")
|
||||
T.eq(gen3.gen3Fields, nil, "the gen3* keys are gone from the derived spec")
|
||||
T.eq(gen3.gen2Fields, nil, "and so are the gen2* keys")
|
||||
T.eq(gen3.target, "gen3Pokemon", "the derived spec carries the routed path")
|
||||
@@ -277,6 +279,9 @@ local GEN3_READY = fixture("fix_gen3_ready", { "gen1", "gen3" }, [[
|
||||
local mew = mod.content.pokemon:get("MEW")
|
||||
mod.exports.mewIndex = mew and mew.index
|
||||
mod.exports.mewStats = mew and mew.baseStats.specialAttack
|
||||
local pidgey = mod.content.pokemon:get("PIDGEY")
|
||||
mod.exports.pidgeyEggMoves = pidgey and pidgey.eggMoves
|
||||
mod.exports.mewEggMoves = mew and mew.eggMoves
|
||||
local copy = {}
|
||||
for key, value in pairs(mew) do copy[key] = value end
|
||||
copy.spriteFront = mod.path .. "/front.png"
|
||||
@@ -284,6 +289,7 @@ local GEN3_READY = fixture("fix_gen3_ready", { "gen1", "gen3" }, [[
|
||||
mod.content.pokemon:patch("CHARMANDER", {
|
||||
catchRate = 3,
|
||||
learnset = { { level = 1, move = "SURF" } },
|
||||
eggMoves = { "EMBER" },
|
||||
evolutions = { { method = "EVO_ITEM", item = "THUNDERSTONE",
|
||||
species = "CHARMELEON" } },
|
||||
})
|
||||
@@ -318,6 +324,10 @@ do
|
||||
local exports = run.loader.exports.fix_gen3_ready or {}
|
||||
T.eq(exports.mewIndex, 151, "Gen 3: MEW resolves by name to species 151")
|
||||
T.eq(exports.mewStats, 100, "Gen 3: with the split special stats")
|
||||
T.eq(table.concat(exports.pidgeyEggMoves or {}, ","), "SCRATCH,TACKLE",
|
||||
"Gen 3: the species record carries its egg moves as move ids")
|
||||
T.eq(exports.mewEggMoves, nil,
|
||||
"Gen 3: a species with no gEggMoves entry carries no eggMoves field")
|
||||
local seen = {}
|
||||
for _, id in ipairs(exports.species or {}) do seen[id] = true end
|
||||
T.check(seen.NIDORAN_F and seen.CHARMANDER and not seen["?"],
|
||||
@@ -327,6 +337,7 @@ do
|
||||
T.eq(P._speciesMeta[4].catchRate, 3, "Gen 3: a patch lands in the numeric meta table")
|
||||
T.eq(P._speciesMeta[151].catchRate, 45, "Gen 3: the skipped mod's patch left no trace")
|
||||
T.eq(P._learnsets[4][1][2], 57, "Gen 3: a learnset move name writes back as its number")
|
||||
T.eq(P._eggMoves[4][1], 52, "Gen 3: an egg move name writes back as its number")
|
||||
T.eq(P._evolutions[4][1].method, 7, "Gen 3: an evolution method writes back as EVO_ITEM")
|
||||
T.eq(P._evolutions[4][1].param, 96, "Gen 3: and its item as the item number")
|
||||
T.eq(P._evolutions[4][1].target, 5, "Gen 3: and its species as the species number")
|
||||
|
||||
@@ -199,6 +199,9 @@ function Sdk.gen3Data()
|
||||
_learnsets = { [4] = { { 1, 10 }, { 7, 52 } }, [5] = { { 1, 10 } },
|
||||
[16] = { { 1, 33 } }, [29] = { { 1, 33 } },
|
||||
[151] = { { 1, 1 } } },
|
||||
-- sparse, exactly like the extractor: a species with no egg move has
|
||||
-- no key rather than an empty list
|
||||
_eggMoves = { [4] = { 57 }, [16] = { 10, 33 } },
|
||||
_evolutions = { [4] = { { method = 4, param = 16, target = 5 } } },
|
||||
_dex = { [4] = { category = "LIZARD", height = 6, weight = 85 },
|
||||
[151] = { category = "NEW SPECIES", height = 4, weight = 40 } },
|
||||
|
||||
Reference in New Issue
Block a user