Add Pokemon Crystal as a sixth supported version

Crystal boots from a user-supplied ROM, imports a full cache and is playable:
copyright, the Crystal intro movie, the animated title, gender select, Oak,
and out into Johto. 122 of the cart's 169 script specials are implemented.

Import and data
- tools/make_crystal_manifest.py derives the manifest by importing
  make_gold_manifest as a library, with three additive keyword seams. Gold and
  Silver still regenerate byte-identical, which is the standing requirement for
  touching that generator.
- crystal_symbol_deltas.py and crystal_movie_symbols.py carry the symbol delta:
  Crystal renames the credits mons, splits the trainer card, Pokegear and
  pack-pal blocks by gender, and replaces the intro and title outright.
- Crystal-only manifest keys: engineFlagOrder (162 flags to Gold's 93, so the
  badge block sits one higher) and unownCharmap (the main charmap parser stops
  at the first newcharmap so the two cannot contaminate each other).

Extractor
- RomExtractorGen2 becomes three-edition. Crystal corrections: PAL_MAP_BANK
  0x13, a flat PICS_FIX pic bank, audio bank 0x5e, the mapSongs id-100 hole,
  seven NPC trades, a TradeTexts stride of 8, the five Crystal tileset anim
  steps with per-row degrade, and the column-major trainer card portraits.
- New: animated front sprites (frames, bitmasks, play and idle scripts), the
  Battle Tower roster, Kris assets, Mobile System GB art, and the Crystal
  intro and title via src/import/CrystalMovie.lua.

Engine
- GameVersion gains engine(id) and fixes(id). Gold and Silver keep their
  original bugs where the bug is not hardware dependent; Crystal gets the fixes
  Crystal shipped: Lucky Number boxes 10-14, surfing onto an NPC, and the
  Reflect and Light Screen defence overflow.
- Crystal story: Suicune and Eusine, Celebi behind the GS Ball flag, the Ruins
  of Alph chambers, Buena, the Move Tutor, the Poke Seer, and the Battle Tower
  including the wInBattleTowerBattle badge-boost guard.
- Kris and the gender flag, animated fronts in battle and the summary screen,
  and mon caught data.

Verification
- Every extracted asset is pixel-compared against pret's own source PNGs.
- Gold caches are byte-identical before and after, file for file.
- New Crystal suites plus a T2 Gen 2 tier; the full suite passes.
This commit is contained in:
bryanthaboi
2026-08-23 12:10:28 -04:00
parent e88f2ef060
commit ca4d3d283c
141 changed files with 43306 additions and 583 deletions
+396
View File
@@ -0,0 +1,396 @@
-- Crystal's intro movie and title screen extraction (pokecrystal
-- engine/movie/intro.asm:1678-1777, engine/movie/title.asm:364-374).
-- Called from RomExtractorGen2's extractIntro/extractTitle stages when
-- edition == "crystal"; reads through the passed-in extractor and never
-- mutates it.
local ImageWriter = require("src.import.ImageWriter")
local CrystalMovie = {}
local TILE_BYTES = 16
local SHEET_TILES = 16
-- vBGMap tilemaps and attrmaps are one full 32x32 map
-- (engine/movie/intro.asm:1611-1628 decompresses $40 tiles = 1024 bytes).
local MAP_BYTES = 1024
local function padTiles(raw, count)
local length = count * TILE_BYTES
while #raw > length do table.remove(raw) end
while #raw < length do raw[#raw + 1] = 0 end
return raw
end
-- Lay `count` tiles of `source` (from tile `from`, 0-based) over `target`
-- starting at tile `at` (0-based).
local function overlayTiles(target, source, at, from, count)
for offset = 1, count * TILE_BYTES do
target[at * TILE_BYTES + offset] = source[from * TILE_BYTES + offset] or 0
end
return target
end
local function blankTiles(count)
local out = {}
for index = 1, count * TILE_BYTES do out[index] = 0 end
return out
end
-- Sheets are 16 tiles per row so tile id N resolves to (N % 16, N / 16),
-- the same layout data/sprite_anims/oam.asm:120-136 assumes.
local function writeSheet(self, raw, count, relative)
padTiles(raw, count)
local rows = math.ceil(count / SHEET_TILES)
padTiles(raw, rows * SHEET_TILES)
self:write2bpp(raw, SHEET_TILES * 8, rows * 8, relative, true)
return "assets/generated/" .. relative
end
local function readMap(self, label)
local bytes = self:decompressLz3Symbol(label)
while #bytes > MAP_BYTES do table.remove(bytes) end
while #bytes < MAP_BYTES do bytes[#bytes + 1] = 0 end
return bytes
end
-- 16 palettes: 8 BG then 8 OBJ (engine/movie/intro.asm:123-130 copies
-- `16 palettes` over wBGPals1, which wOBPals1 follows).
local function readPalettes(self, label)
local symbol = self:symbol(label)
local flat = self:colors(symbol.bank, symbol.address, 64)
local bg, obj = {}, {}
for pal = 0, 7 do
local a, b = {}, {}
for slot = 1, 4 do
a[slot] = flat[pal * 4 + slot]
b[slot] = flat[(pal + 8) * 4 + slot]
end
bg[pal + 1], obj[pal + 1] = a, b
end
return { bg = bg, obj = obj }
end
local function readColors(self, label, count)
local symbol = self:symbol(label)
return self:colors(symbol.bank, symbol.address, count)
end
function CrystalMovie.extractIntro(self)
self:beginStage("Intro movie")
local out = {
generation = 2,
layout = "crystal",
source = "ROM:CrystalIntro (engine/movie/intro.asm)",
}
local unowns = padTiles(self:decompressLz3Symbol("IntroUnownsGFX"), 128)
local grassSym = self:symbol("IntroGrass4GFX")
local grass4 = self.rom:bytes(grassSym.bank, grassSym.address, TILE_BYTES)
local unownsPath = writeSheet(self, unowns, 128, "intro/unowns_tiles.png")
local pulsePath = writeSheet(self,
self:decompressLz3Symbol("IntroPulseGFX"), 16, "intro/pulse_sprites.png")
local backgroundPath = writeSheet(self,
self:decompressLz3Symbol("IntroBackgroundGFX"), 128,
"intro/background_tiles.png")
local runPath = writeSheet(self,
self:decompressLz3Symbol("IntroSuicuneRunGFX"), 192,
"intro/suicune_run_sprites.png")
local pichuPath = writeSheet(self,
self:decompressLz3Symbol("IntroPichuWooperGFX"), 128,
"intro/pichu_wooper_sprites.png")
self:tick("Intro movie", 1, 4)
-- IntroScene15: jump BG at vTiles2 plus IntroGrass4GFX at vTiles1 tile 0,
-- i.e. BG id $80 (engine/movie/intro.asm:744-753).
local jump = blankTiles(256)
overlayTiles(jump, self:decompressLz3Symbol("IntroSuicuneJumpGFX"), 0, 0, 128)
overlayTiles(jump, grass4, 0x80, 0, 1)
local jumpPath = writeSheet(self, jump, 256, "intro/suicune_jump_tiles.png")
-- The same grass tile is the whole of the SUICUNE_AWAY object's sheet
-- (data/sprite_anims/oam.asm:136 base $80; engine/movie/intro.asm:750-753).
local unownBack = blankTiles(144)
overlayTiles(unownBack, self:decompressLz3Symbol("IntroUnownBackGFX"), 0, 0, 48)
overlayTiles(unownBack, grass4, 0x80, 0, 1)
local unownBackPath = writeSheet(self, unownBack, 144,
"intro/unown_back_sprites.png")
-- IntroScene17 loads 255 tiles from vTiles1, so BG id $80 is close tile 0
-- and ids wrap through $00-$7e (engine/movie/intro.asm:825-828).
local closeRaw = padTiles(self:decompressLz3Symbol("IntroSuicuneCloseGFX"), 255)
local close = blankTiles(256)
for id = 0, 255 do
local from = (id + 128) % 256
if from < 255 then overlayTiles(close, closeRaw, id, from, 1) end
end
local closePath = writeSheet(self, close, 256, "intro/suicune_close_tiles.png")
-- IntroScene19: suicune_back at vTiles2, the Unown ring at vTiles1, and
-- grass over vTiles1 tile $7f = BG id $ff (engine/movie/intro.asm:890-901).
local back = blankTiles(256)
overlayTiles(back, self:decompressLz3Symbol("IntroSuicuneBackGFX"), 0, 0, 128)
overlayTiles(back, unowns, 128, 0, 128)
overlayTiles(back, grass4, 0xff, 0, 1)
local backPath = writeSheet(self, back, 256, "intro/suicune_back_tiles.png")
local crystalUnownsPath = writeSheet(self,
self:decompressLz3Symbol("IntroCrystalUnownsGFX"), 32,
"intro/crystal_unowns_tiles.png")
-- Intro_RustleGrass swaps 4 tiles at vTiles2 tile $09 through grass1/2/3
-- (engine/movie/intro.asm:1521-1547); one 16-tile row, frame f at f*4.
local grassStrip = blankTiles(16)
for frame, label in ipairs({ "IntroGrass1GFX", "IntroGrass2GFX",
"IntroGrass3GFX" }) do
local sym = self:symbol(label)
overlayTiles(grassStrip,
self.rom:bytes(sym.bank, sym.address, 4 * TILE_BYTES), (frame - 1) * 4,
0, 4)
end
out.grassFrames = writeSheet(self, grassStrip, 16, "intro/grass_anim.png")
self:tick("Intro movie", 2, 4)
local unownPals = readPalettes(self, "IntroUnownsPalette")
local backgroundPals = readPalettes(self, "IntroBackgroundPalette")
local suicunePals = readPalettes(self, "IntroSuicunePalette")
local closePals = readPalettes(self, "IntroSuicuneClosePalette")
local crystalUnownsPals = readPalettes(self, "IntroCrystalUnownsPalette")
local function act(tiles, sprites, tilemapLabel, attrmapLabel, palettes)
return {
tiles = tiles,
sprites = sprites,
tilemap = readMap(self, tilemapLabel),
attrmap = readMap(self, attrmapLabel),
palettes = palettes,
}
end
out.acts = {
unownA = act(unownsPath, pulsePath,
"IntroUnownATilemap", "IntroUnownAAttrmap", unownPals),
unownHI = act(unownsPath, pulsePath,
"IntroUnownHITilemap", "IntroUnownHIAttrmap", unownPals),
unowns = act(unownsPath, pulsePath,
"IntroUnownsTilemap", "IntroUnownsAttrmap", unownPals),
background = act(backgroundPath, runPath,
"IntroBackgroundTilemap", "IntroBackgroundAttrmap", backgroundPals),
suicuneJump = act(jumpPath, unownBackPath,
"IntroSuicuneJumpTilemap", "IntroSuicuneJumpAttrmap", suicunePals),
suicuneClose = act(closePath, nil,
"IntroSuicuneCloseTilemap", "IntroSuicuneCloseAttrmap", closePals),
suicuneBack = act(backPath, unownBackPath,
"IntroSuicuneBackTilemap", "IntroSuicuneBackAttrmap", suicunePals),
crystalUnowns = act(crystalUnownsPath, nil,
"IntroCrystalUnownsTilemap", "IntroCrystalUnownsAttrmap",
crystalUnownsPals),
}
-- IntroScene7/10 load pichu_wooper into VRAM bank 1
-- (engine/movie/intro.asm:340-348); OAM_BANK1 picks this sheet.
out.acts.background.sprites1 = pichuPath
self:tick("Intro movie", 3, 4)
out.fades = {
toWhite = {},
unownAppear = readColors(self, "Intro_Scene20_AppearUnown.pal1", 4),
unownAppear2 = readColors(self, "Intro_Scene20_AppearUnown.pal2", 4),
wordFast = readColors(self, "Intro_FadeUnownWordPals.FastFadePalettes", 16),
wordSlow = readColors(self, "Intro_FadeUnownWordPals.SlowFadePalettes", 16),
}
-- Intro_Scene24_ApplyPaletteFade steps 8 rows of one palette each
-- (engine/movie/intro.asm:1155-1189).
local fade = readColors(self, "Intro_Scene24_ApplyPaletteFade.FadePals", 32)
for row = 0, 7 do
local pal = {}
for slot = 1, 4 do pal[slot] = fade[row * 4 + slot] end
out.fades.toWhite[row + 1] = pal
end
self:write("intro", out)
self:tick("Intro movie", 4, 4)
return out
end
--------------------------------------------------------------------------
local function shadeOf(r)
if r > 0.9 then return 0 end
if r > 0.5 then return 1 end
if r > 0.2 then return 2 end
return 3
end
local function tilesFrom2bpp(raw)
local tiles = {}
for offset = 1, #raw - (#raw % TILE_BYTES), TILE_BYTES do
local one = {}
for index = offset, offset + TILE_BYTES - 1 do one[#one + 1] = raw[index] end
tiles[#tiles + 1] = ImageWriter.decode2bpp(one, 8, 8, true)
end
return tiles
end
local function blitTile(target, tile, tx, ty)
if not tile then return end
for y = 0, 7 do
for x = 0, 7 do
local r, g, b, a = tile:getPixel(x, y)
if a ~= 0 then target:setPixel(tx + x, ty + y, r, g, b, a) end
end
end
end
local function colorize(image, palFor)
local w, h = image:getDimensions()
local out = ImageWriter.blank(w, h, 0, 0, 0, 0)
for y = 0, h - 1 do
for x = 0, w - 1 do
local r, _, _, a = image:getPixel(x, y)
if a ~= 0 then
local pal = palFor(x, y)
local c = pal[shadeOf(r) + 1] or pal[4] or { 0, 0, 0 }
out:setPixel(x, y, c[1] / 255, c[2] / 255, c[3] / 255, 1)
end
end
end
return out
end
function CrystalMovie.extractTitle(self)
self:beginStage("Title screen")
-- TitleLogoGFX decompresses to vTiles1, so BG id $80 is logo tile 0 and
-- ids $00-$1b wrap on past it (engine/movie/title.asm:91-94).
local logoTiles = tilesFrom2bpp(self:decompressLz3Symbol("TitleLogoGFX"))
-- TitleSuicuneGFX fills vTiles4-vTiles5: bank-1 ids $80-$ff then $00-$7f
-- (engine/movie/title.asm:24-27).
local suicuneTiles = tilesFrom2bpp(self:decompressLz3Symbol("TitleSuicuneGFX"))
local gemTiles = tilesFrom2bpp(self:decompressLz3Symbol("TitleCrystalGFX"))
local pals = readPalettes(self, "TitleScreenPalettes")
self:tick("Title screen", 1, 5)
-- Attribute regions from _TitleScreen's ByteFills
-- (engine/movie/title.asm:39-85): logo gradient rows, the version strip,
-- pal 7 on the window copyright line, pal 0 everywhere else.
local function bgPalAt(col, row)
if row == 9 and col >= 5 and col <= 15 then return 1 end
if row >= 3 and row <= 4 then return 2 end
if row == 5 then return 3 end
if row == 6 then return 4 end
if row == 7 then return 5 end
if row >= 8 and row <= 9 then return 6 end
if row == 17 then return 7 end
return 0
end
local shade = ImageWriter.blank(160, 144, 0, 0, 0, 0)
-- DrawTitleGraphic lays 7 rows of 20 running tiles from d=$80
-- (engine/movie/title.asm:107-112,275-302).
for row = 0, 6 do
for col = 0, 19 do
blitTile(shade, logoTiles[row * 20 + col + 1], col * 8, (row + 3) * 8)
end
end
-- Copyright line: 13 tiles from d=$c on the window's row 0, shown at
-- hWY = $88 (engine/movie/title.asm:114-119; engine/menus/intro_menu.asm:1121-1122).
for index = 0, 12 do
blitTile(shade, logoTiles[128 + 12 + index + 1], (3 + index) * 8, 17 * 8)
end
local colored = colorize(shade, function(x, y)
return pals.bg[bgPalAt(math.floor(x / 8), math.floor(y / 8)) + 1]
end)
self:save(colored, "title/crystal_screen.png")
self:save(shade, "title/crystal_screen_gray.png")
local logo = ImageWriter.blank(160, 56, 0, 0, 0, 0)
ImageWriter.blit(logo, colored, 0, 0, 0, 24, 160, 56)
self:save(logo, "title/crystal_logo.png")
local wordmark = ImageWriter.blank(88, 8, 0, 0, 0, 0)
ImageWriter.blit(wordmark, colored, 0, 0, 40, 72, 88, 8)
self:save(wordmark, "title/crystal_wordmark.png")
self:tick("Title screen", 2, 5)
-- LoadSuicuneFrame: 6 rows of 8 tiles at hlcoord 6,12, row stride 16;
-- frame bases $80/$88/$00/$08 (engine/movie/title.asm:245-273).
local suicunePaths, suicuneGrayPaths = {}, {}
for index, base in ipairs({ 0x00, 0x08, 0x80, 0x88 }) do
local frame = ImageWriter.blank(64, 48, 0, 0, 0, 0)
for row = 0, 5 do
for col = 0, 7 do
blitTile(frame, suicuneTiles[base + row * 16 + col + 1],
col * 8, row * 8)
end
end
local tinted = colorize(frame, function() return pals.bg[1] end)
local rel = ("title/crystal_suicune_%d.png"):format(index)
self:save(tinted, rel)
suicunePaths[index] = "assets/generated/" .. rel
local grayRel = ("title/crystal_suicune_%d_gray.png"):format(index)
self:save(frame, grayRel)
suicuneGrayPaths[index] = "assets/generated/" .. grayRel
if index == 1 then self:save(tinted, "title/crystal_suicune.png") end
end
self:tick("Title screen", 3, 5)
-- InitializeBackground: five 48x16 strips of six 8x16 OBJs, consecutive
-- tile pairs, OBJ pal 0, OAM_PRIO (engine/movie/title.asm:304-338).
local gem = ImageWriter.blank(48, 80, 0, 0, 0, 0)
for strip = 0, 4 do
for slot = 0, 5 do
local tile = strip * 12 + slot * 2
blitTile(gem, gemTiles[tile + 1], slot * 8, strip * 16)
blitTile(gem, gemTiles[tile + 2], slot * 8, strip * 16 + 8)
end
end
local gemTinted = colorize(gem, function() return pals.obj[1] end)
self:save(gemTinted, "title/crystal_gem.png")
self:save(gem, "title/crystal_gem_gray.png")
self:tick("Title screen", 4, 5)
local function unit(color) return { color[1] / 255, color[2] / 255, color[3] / 255 } end
local data = {
generation = 2,
layout = "crystal_title",
source = "ROM:TitleSuicuneGFX + TitleLogoGFX + TitleCrystalGFX"
.. " + TitleScreenPalettes",
screen = "assets/generated/title/crystal_screen.png",
screenGray = "assets/generated/title/crystal_screen_gray.png",
image = "assets/generated/title/crystal_logo.png",
wordmark = "assets/generated/title/crystal_wordmark.png",
suicune = "assets/generated/title/crystal_suicune.png",
suicuneFrames = suicunePaths,
suicuneFramesGray = suicuneGrayPaths,
-- hlcoord 6, 12 (engine/movie/title.asm:252) in screen pixels.
suicuneX = 48,
suicuneY = 96,
-- SuicuneFrameIterator advances every 8 frames (engine/movie/title.asm:217-243).
suicuneEvery = 8,
gem = "assets/generated/title/crystal_gem.png",
gemGray = "assets/generated/title/crystal_gem_gray.png",
-- Strip 0 starts at OAM (64, -$22) and stops at OAM y 22, i.e. screen
-- (56, 6) (engine/movie/title.asm:306-311,340-362).
gemX = 56,
gemY = 6,
gemFromY = -50,
gemStep = 2,
-- TitleScreenEntrance: hSCX from +112 to 0 by 4 with alternating-line
-- signage over the logo's 80 lines; the copyright window sits at
-- hWY = $88 only after it lands (engine/menus/intro_menu.asm:1078-1111).
entrance = { scx = 112, step = 4, lines = 80, hideBelow = 136 },
entranceSfx = "Sfx_TitleScreenEntrance",
-- TitleScreenTimer (engine/menus/intro_menu.asm:1125-1136).
timeoutFrames = 73 * 60 + 36,
sky = unit(pals.bg[1][1]),
below = unit(pals.bg[1][1]),
palettes = { bg = pals.bg, obj = pals.obj },
}
self:write("title", data)
self:tick("Title screen", 5, 5)
return data
end
return CrystalMovie
+26 -10
View File
@@ -405,6 +405,7 @@ end
local CART_COLOR = {
red = PAL.railRed, blue = PAL.railBlue, yellow = PAL.railGold,
gold = PAL.railAmber, silver = PAL.railSilver,
crystal = PAL.railCrystal,
}
local function cartColor(version)
return CART_COLOR[version] or PAL.green
@@ -1062,6 +1063,8 @@ local GAME_TABS = {
label = "Gold" },
{ id = "silver", key = "tab-silver", letter = "S", color = PAL.railSilver,
label = "Silver" },
{ id = "crystal", key = "tab-crystal", letter = "C",
color = PAL.railCrystal, label = "Crystal" },
}
local HEADER_TABS = {
@@ -3555,24 +3558,37 @@ end
-- also what a controller reaches after the tab row.
local function buildGameModal(imp, m)
local pad = math.floor(18 * m.s)
local gap = math.floor(8 * m.s)
local w = math.floor(360 * m.s)
local h = pad + Kit.textHeight("button") + math.floor(12 * m.s)
+ #GAME_TABS * (m.btnH + gap) + m.btnH + pad
local px, py, pw = modalPanel(m, w, h)
local headH = Kit.textHeight("button") + math.floor(12 * m.s)
local avail = m.H - 2 * m.pad
local cols, gap, btnH = 1, math.floor(8 * m.s), m.btnH
local function rows() return math.ceil(#GAME_TABS / cols) + 1 end
local function total() return 2 * pad + headH + rows() * btnH
+ (rows() - 1) * gap end
if total() > avail then cols = 2 end
if total() > avail then gap = math.max(2, math.floor(3 * m.s)) end
if total() > avail then
btnH = math.max(Kit.tapMin(),
btnH - math.ceil((total() - avail) / rows()))
end
local nrows = rows() - 1
local w = math.floor((cols > 1 and 440 or 360) * m.s)
local px, py, pw = modalPanel(m, w, total())
local cy = py + pad
Kit.text("button", Strings("Choose game"), px + pad, cy, PAL.heading)
cy = cy + Kit.textHeight("button") + math.floor(12 * m.s)
cy = cy + headH
local chrome = headerChrome(imp)
for _, g in ipairs(GAME_TABS) do
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "gamepop-" .. g.id,
local colW = math.floor((pw - 2 * pad - (cols - 1) * gap) / cols)
for i, g in ipairs(GAME_TABS) do
local bx = px + pad + ((i - 1) % cols) * (colW + gap)
local by = cy + math.floor((i - 1) / cols) * (btnH + gap)
btn(imp, bx, by, colW, btnH, "gamepop-" .. g.id,
Strings(g.label), {
face = "tab", font = "small", letter = g.letter, color = g.color,
active = imp.tab == g.id,
action = chrome.tab[g.id] })
cy = cy + m.btnH + gap
end
btn(imp, px + pad, cy, pw - 2 * pad, m.btnH, "gamepop-close",
cy = cy + nrows * (btnH + gap)
btn(imp, px + pad, cy, pw - 2 * pad, btnH, "gamepop-close",
Strings("Close"), { font = "small",
action = function() imp._gamePopup = nil end })
end
File diff suppressed because it is too large Load Diff
+77 -7
View File
@@ -155,6 +155,53 @@ local VERSION_REQUIRED_FILES_OVERRIDE = {
-- PCMailGFX (engine/pokemon/bills_pc.asm:2170-2173)
"assets/generated/pc/mail_item.png",
},
crystal = {
"data/generated/constants.lua",
"data/generated/maps.lua",
"data/generated/roofs.lua",
"data/generated/sprites.lua",
"data/generated/scripts.lua",
"data/generated/text.lua",
"data/generated/rom_text.lua",
"data/generated/pokemon.lua",
"data/generated/encounters.lua",
"data/generated/tilesets.lua",
"data/generated/landmarks.lua",
"data/generated/audio.lua",
"data/generated/marts.lua",
"data/generated/oak_speech.lua",
"data/generated/title.lua",
"data/generated/intro.lua",
"assets/generated/fonts/font.png",
"assets/generated/fonts/frames.png",
"assets/generated/title/crystal_logo.png",
"assets/generated/title/crystal_wordmark.png",
"assets/generated/title/crystal_suicune.png",
"assets/generated/splash/ditto.png",
"assets/generated/intro/chris.png",
"assets/generated/intro/kris.png",
"assets/generated/intro/suicune_run_sprites.png",
"assets/generated/intro/unowns_tiles.png",
"assets/generated/intro/oak.png",
"assets/generated/tilesets/johto.png",
"assets/generated/tilesets/roofs/new_bark.png",
"assets/generated/sprites/chris.png",
"assets/generated/sprites/kris.png",
"assets/generated/battle/front/chikorita.png",
"assets/generated/battle/front/wooper.png",
"assets/generated/battle/front/pikachu.png",
"assets/generated/battle/trainers/falkner.png",
"assets/generated/battle/hud/balls.png",
"assets/generated/audio/programs.bin",
"assets/generated/slots/gold_slots_1.png",
"assets/generated/card_flip/card_flip_1.png",
"assets/generated/pc/mail_item.png",
"assets/generated/trainer_card/card_f.png",
"data/generated/mobile_gfx.lua",
"assets/generated/battle/player_back_female.png",
"assets/generated/battle/trainers/kris.png",
"assets/generated/battle/trainers/chris.png",
},
}
-- Same Gen 2 extract, so a Silver cache is complete when the same files exist.
VERSION_REQUIRED_FILES_OVERRIDE.silver = VERSION_REQUIRED_FILES_OVERRIDE.gold
@@ -605,6 +652,27 @@ local function isAcceptedRomSize(n)
return n == ROM_BYTES_GEN1 or n == ROM_BYTES_GEN2
end
local function cartLabels(generation)
local out = {}
for _, id in ipairs(GameVersion.ORDER) do
if generation == nil or GameVersion.generation(id) == generation then
out[#out + 1] = GameVersion.info(id).label
end
end
return out
end
local function cartsSlashed(generation)
return table.concat(cartLabels(generation), "/")
end
local function cartsProse()
local names = cartLabels(nil)
local last = table.remove(names)
if #names == 0 then return last end
return table.concat(names, ", ") .. ", or " .. last
end
local function savesInboxDir(version)
return SAVES_INBOX_DIR .. "/" .. tostring(version)
end
@@ -1849,18 +1917,18 @@ function RomImporter:startData(data, displayName)
return
end
if not isAcceptedRomSize(#data) then
self:setError(("Expected a 1 MiB Game Boy ROM (Red/Blue/Yellow) or a "
.. "2 MiB Game Boy Color ROM (Gold/Silver); this file is %.2f MiB.")
:format(#data / 1024 / 1024))
self:setError(("Expected a 1 MiB Game Boy ROM (%s) or a "
.. "2 MiB Game Boy Color ROM (%s); this file is %.2f MiB.")
:format(cartsSlashed(1), cartsSlashed(2), #data / 1024 / 1024))
return
end
local actualHash = sha1(data)
local version = GameVersion.forSha1(actualHash)
if not version then
self:setError(("Unsupported ROM (SHA-1 %s). This needs a clean US Pokemon "
.. "Red, Blue, Yellow, Gold, or Silver dump; patched, trimmed or "
.. "%s dump; patched, trimmed or "
.. "\"fixed\" dumps "
.. "(tagged [b] or [BF]) never verify."):format(actualHash))
.. "(tagged [b] or [BF]) never verify."):format(actualHash, cartsProse()))
return
end
local info = GameVersion.info(version)
@@ -2965,8 +3033,10 @@ function RomImporter:resumeAfterOverlay()
end
function RomImporter:_cycleTab(delta)
local order = { "red", "blue", "yellow", "gold", "silver",
"mods", "find", "skins", "bug" }
local order = { "mods", "find", "skins", "bug" }
for i = #GameVersion.ORDER, 1, -1 do
table.insert(order, 1, GameVersion.ORDER[i])
end
local idx = 1
for i, id in ipairs(order) do
if id == self.tab then idx = i; break end