mod assets;
mod items;
mod moves;
pub mod pic;
mod species;
mod sprites;
pub mod text;
use std::borrow::Cow;
use std::sync::OnceLock;
pub(crate) use assets::sprite_path;
pub use items::Item;
pub use moves::Move;
pub(crate) use moves::percent;
pub use species::{Evolution, EvolutionMethod, Species, Stats};
pub(crate) use species::{bits, parse_learnset};
pub(crate) use sprites::in_front_box;
use text::Charmap;
pub use crate::games::pokemon::gen4::{GrowthRate, ItemId, LevelUpMove, MoveId, SpeciesId};
use crate::games::GameId;
use crate::io::{ByteSource, Reader};
use crate::media::Rgba;
use crate::platforms::gb::{self, BANK_SIZE, FarPointer};
use crate::{Error, Result};
const INDICES: usize = 190;
const MOVES: usize = 165;
const ITEMS: usize = 83;
const MACHINES: usize = 55;
const PALETTE_ENTRIES: usize = 152;
#[derive(Debug)]
pub struct Edition {
charmap: &'static Charmap,
name_len: usize,
base_stats: FarPointer,
species: usize,
mew: Option<FarPointer>,
pokedex: FarPointer,
names: FarPointer,
evolutions: FarPointer,
moves: FarPointer,
move_names: FarPointer,
machines: FarPointer,
item_names: FarPointer,
item_prices: FarPointer,
palette_ids: FarPointer,
palettes: FarPointer,
pic_banks: PicBanks,
}
#[derive(Debug)]
struct PicBanks {
ranges: &'static [(u8, u8)],
mew: Option<u8>,
}
const MEW: u8 = 0x15;
const RED_BLUE: Edition = Edition {
charmap: &text::ENGLISH,
name_len: 10,
base_stats: FarPointer::new(0x0E, 0x43DE),
species: 150,
mew: Some(FarPointer::new(0x01, 0x425B)),
pokedex: FarPointer::new(0x10, 0x5024),
names: FarPointer::new(0x07, 0x421E),
evolutions: FarPointer::new(0x0E, 0x705C),
moves: FarPointer::new(0x0E, 0x4000),
move_names: FarPointer::new(0x2C, 0x4000),
machines: FarPointer::new(0x04, 0x7773),
item_names: FarPointer::new(0x01, 0x472B),
item_prices: FarPointer::new(0x01, 0x4608),
palette_ids: FarPointer::new(0x1C, 0x65C8),
palettes: FarPointer::new(0x1C, 0x6660),
pic_banks: PicBanks {
ranges: &[
(0x1E, 0x09),
(0x49, 0x0A),
(0x73, 0x0B),
(0x98, 0x0C),
(0xFF, 0x0D),
],
mew: Some(0x01),
},
};
const YELLOW: Edition = Edition {
species: 151,
mew: None,
pokedex: FarPointer::new(0x10, 0x50B1),
names: FarPointer::new(0x3A, 0x4000),
evolutions: FarPointer::new(0x0E, 0x71E5),
move_names: FarPointer::new(0x2F, 0x4000),
machines: FarPointer::new(0x04, 0x632D),
item_names: FarPointer::new(0x01, 0x45B7),
item_prices: FarPointer::new(0x01, 0x4494),
palette_ids: FarPointer::new(0x1C, 0x6921),
palettes: FarPointer::new(0x1C, 0x69B9),
pic_banks: PicBanks {
mew: None,
..RED_BLUE.pic_banks
},
..RED_BLUE
};
const GREEN: Edition = Edition {
charmap: &text::JAPANESE,
name_len: 5,
base_stats: FarPointer::new(0x0E, 0x4000),
mew: Some(FarPointer::new(0x01, 0x4200)),
pokedex: FarPointer::new(0x10, 0x679A),
names: FarPointer::new(0x0E, 0x5068),
evolutions: FarPointer::new(0x0E, 0x7427),
moves: FarPointer::new(0x0E, 0x5658),
move_names: FarPointer::new(0x04, 0x4000),
machines: FarPointer::new(0x04, 0x6276),
item_names: FarPointer::new(0x01, 0x433F),
item_prices: FarPointer::new(0x01, 0x421C),
palette_ids: FarPointer::new(0x1C, 0x6A1E),
palettes: FarPointer::new(0x1C, 0x6AB6),
pic_banks: PicBanks {
ranges: &[
(0x1E, 0x09),
(0x49, 0x0A),
(0x74, 0x0B),
(0x99, 0x0C),
(0xFF, 0x0D),
],
mew: Some(0x01),
},
..RED_BLUE
};
const RELEASES: &[(GameId, u16, &Edition)] = &[
(GameId::PokemonRed, 0x91E6, &RED_BLUE),
(GameId::PokemonBlue, 0x9D0A, &RED_BLUE),
(GameId::PokemonYellow, 0x047C, &YELLOW),
(GameId::PokemonGreen, 0xDDD5, &GREEN),
];
impl Edition {
pub fn covers(game: GameId) -> bool {
RELEASES.iter().any(|&(known, _, _)| known == game)
}
pub fn release(game: GameId, checksum: u16) -> Option<&'static Self> {
RELEASES
.iter()
.find(|&&(known, sum, _)| known == game && sum == checksum)
.map(|&(_, _, edition)| edition)
}
}
pub struct Gen1<S> {
game: GameId,
edition: &'static Edition,
source: S,
species: OnceLock<Vec<Species>>,
moves: OnceLock<Vec<Move>>,
items: OnceLock<Vec<Item>>,
palettes: OnceLock<Vec<[Rgba; 4]>>,
}
impl<S: ByteSource> Gen1<S> {
pub fn open(source: S) -> Result<Self> {
let (game, edition) = open_release(&source, Edition::release, "Gen 1")?;
Ok(Self {
game,
edition,
source,
species: OnceLock::new(),
moves: OnceLock::new(),
items: OnceLock::new(),
palettes: OnceLock::new(),
})
}
pub fn game(&self) -> GameId {
self.game
}
pub fn species(&self) -> Result<&[Species]> {
cached(&self.species, || {
let edition = self.edition;
let mut records = read(&self.source, edition.base_stats, edition.species * 28)?;
if let Some(mew) = edition.mew {
records
.to_mut()
.extend_from_slice(&read(&self.source, mew, 28)?);
}
let pokedex = read(&self.source, edition.pokedex, INDICES)?;
let names = read(&self.source, edition.names, INDICES * edition.name_len)?;
let names = edition.charmap.fixed(&names, edition.name_len);
let evolutions = Bank::read(&self.source, edition.evolutions.bank)?;
species::parse(
&records,
&pokedex,
&names,
&evolutions,
edition.evolutions.address,
)
})
.map(Vec::as_slice)
}
pub fn moves(&self) -> Result<&[Move]> {
cached(&self.moves, || {
let edition = self.edition;
let records = read(&self.source, edition.moves, MOVES * 6)?;
let names = Bank::read(&self.source, edition.move_names.bank)?;
let names = edition
.charmap
.list(names.from(edition.move_names.address)?, MOVES);
let machines = read(&self.source, edition.machines, MACHINES)?;
moves::parse(&records, &names, &machines)
})
.map(Vec::as_slice)
}
pub fn items(&self) -> Result<&[Item]> {
cached(&self.items, || {
let edition = self.edition;
let names = Bank::read(&self.source, edition.item_names.bank)?;
let names = edition
.charmap
.list(names.from(edition.item_names.address)?, ITEMS);
let prices = read(&self.source, edition.item_prices, ITEMS * 3)?;
items::parse(&names, &prices)
})
.map(Vec::as_slice)
}
}
pub(crate) fn open_release<E>(
source: &impl ByteSource,
release: impl Fn(GameId, u16) -> Option<&'static E>,
generation: &str,
) -> Result<(GameId, &'static E)> {
let head = source.read_up_to(0, 0x150)?;
let identity = crate::identify(source)?;
let checksum = gb::global_checksum(&head)?;
let game = GameId::identify(&identity).ok_or_else(|| {
Error::Unsupported(format!("{identity} is not a {generation} Pokémon game"))
})?;
let edition = release(game, checksum).ok_or_else(|| {
Error::Unsupported(format!(
"{identity} (checksum {checksum:04x}) is a release of {} romkit has no tables for",
game.name()
))
})?;
Ok((game, edition))
}
pub(crate) fn read(source: &impl ByteSource, at: FarPointer, len: usize) -> Result<Cow<'_, [u8]>> {
let offset = at.offset().ok_or_else(|| {
Error::malformed("Game Boy ROM", at.address.into(), "pointer outside ROM")
})?;
source.read(offset, len as u64)
}
pub(crate) struct Bank<'a> {
pub(crate) number: u8,
pub(crate) bytes: Cow<'a, [u8]>,
}
impl<'a> Bank<'a> {
pub(crate) fn read(source: &'a impl ByteSource, number: u8) -> Result<Self> {
let offset = u64::from(number) * BANK_SIZE;
let bytes = source
.read(offset, BANK_SIZE)
.map_err(|_| Error::malformed("Game Boy ROM", offset, "bank past the end"))?;
Ok(Self { number, bytes })
}
pub(crate) fn from(&self, address: u16) -> Result<&[u8]> {
let base = match self.number {
0 => 0,
_ => BANK_SIZE as u16,
};
address
.checked_sub(base)
.and_then(|start| self.bytes.get(start.into()..))
.ok_or_else(|| {
Error::malformed("Game Boy ROM", address.into(), "pointer outside its bank")
})
}
pub(crate) fn reader(&self, format: &'static str, address: u16) -> Result<Reader<'_>> {
Ok(Reader::new(format, self.from(address)?))
}
}
pub(crate) fn cached<T>(cell: &OnceLock<T>, init: impl FnOnce() -> Result<T>) -> Result<&T> {
if let Some(value) = cell.get() {
return Ok(value);
}
let value = init()?;
Ok(cell.get_or_init(|| value))
}
raw_enum! {
pub enum Type: u8 {
Normal = 0x00,
Fighting = 0x01,
Flying = 0x02,
Poison = 0x03,
Ground = 0x04,
Rock = 0x05,
Bird = 0x06,
Bug = 0x07,
Ghost = 0x08,
Steel = 0x09,
Mystery = 0x13,
Fire = 0x14,
Water = 0x15,
Grass = 0x16,
Electric = 0x17,
Psychic = 0x18,
Ice = 0x19,
Dragon = 0x1A,
Dark = 0x1B,
}
}