mod assets;
mod items;
mod moves;
mod sound;
mod species;
mod sprites;
pub mod text;
use std::sync::OnceLock;
pub use items::{BattlePockets, Item, NaturalGift, Pocket};
pub use moves::{
Ailment, BattleStat, Move, MoveClass, MoveFlags, MoveKind, MoveTarget, Span, StatChange,
};
pub use species::{Ability, Evolution, EvolutionMethod, Form, HeldItems, Species};
use text::Text;
pub use crate::games::pokemon::gen4::{
AbilityId, Color, EggGroup, GenderRatio, GrowthRate, ItemId, LevelUpMove, MoveId, SpeciesId,
Stats, Variant,
};
use sprites::Archives;
use crate::formats::narc::Narc;
use crate::formats::sdat::Sdat;
use crate::games::GameId;
use crate::io::{ByteSource, Window};
use crate::platforms::nds::NdsRom;
use crate::{Error, Result};
const PERSONAL: &str = "a/0/1/6";
const LEARNSETS: &str = "a/0/1/8";
const EVOLUTIONS: &str = "a/0/1/9";
const MOVES: &str = "a/0/2/1";
const ITEMS: &str = "a/0/2/4";
const TEXT: &str = "a/0/0/2";
const STORY_TEXT: &str = "a/0/0/3";
#[derive(Debug)]
pub struct Edition {
species_names: usize,
move_names: usize,
move_descriptions: usize,
ability_names: usize,
ability_descriptions: usize,
item_names: usize,
item_descriptions: usize,
sprites: Archives,
sound: &'static str,
}
const BLACK_WHITE: Edition = Edition {
species_names: 70,
move_names: 203,
move_descriptions: 202,
ability_names: 182,
ability_descriptions: 183,
item_names: 54,
item_descriptions: 53,
sprites: Archives {
trainers: "a/0/7/2",
forms: 652,
first_icon: 7,
},
sound: "wb_sound_data.sdat",
};
const BLACK2_WHITE2: Edition = Edition {
species_names: 90,
move_names: 403,
move_descriptions: 402,
ability_names: 374,
ability_descriptions: 375,
item_names: 64,
item_descriptions: 63,
sprites: Archives {
trainers: "a/0/7/1",
forms: 685,
first_icon: 8,
},
sound: "swan_sound_data.sdat",
};
impl Edition {
pub fn of(game: GameId) -> Option<&'static Self> {
match game {
GameId::PokemonBlack | GameId::PokemonWhite => Some(&BLACK_WHITE),
GameId::PokemonBlack2 | GameId::PokemonWhite2 => Some(&BLACK2_WHITE2),
_ => None,
}
}
}
pub struct Gen5<S> {
game: GameId,
edition: &'static Edition,
rom: NdsRom<S>,
text: OnceLock<Narc>,
story_text: OnceLock<Narc>,
species: OnceLock<Vec<Species>>,
moves: OnceLock<Vec<Move>>,
abilities: OnceLock<Vec<Ability>>,
items: OnceLock<Vec<Item>>,
sprites: OnceLock<sprites::Index>,
sound: OnceLock<Sdat>,
}
impl<S: ByteSource> Gen5<S> {
pub fn open(source: S) -> Result<Self> {
let rom = NdsRom::open(source)?;
let identity = rom.header().identity();
let game = GameId::identify(&identity)
.filter(|&game| Edition::of(game).is_some())
.ok_or_else(|| Error::Unsupported(format!("{identity} is not a Gen 5 Pokémon game")))?;
Ok(Self {
game,
edition: Edition::of(game).expect("filtered above"),
rom,
text: OnceLock::new(),
story_text: OnceLock::new(),
species: OnceLock::new(),
moves: OnceLock::new(),
abilities: OnceLock::new(),
items: OnceLock::new(),
sprites: OnceLock::new(),
sound: OnceLock::new(),
})
}
pub fn game(&self) -> GameId {
self.game
}
pub fn rom(&self) -> &NdsRom<S> {
&self.rom
}
pub fn text(&self) -> Result<Text<'_, Window<&S>>> {
self.archive(TEXT, &self.text)
}
pub fn story_text(&self) -> Result<Text<'_, Window<&S>>> {
self.archive(STORY_TEXT, &self.story_text)
}
fn archive<'a>(
&'a self,
path: &str,
narc: &'a OnceLock<Narc>,
) -> Result<Text<'a, Window<&'a S>>> {
let archive = self.rom.file(path)?;
let narc = cached(narc, || Narc::parse(&archive))?;
Ok(Text::new(archive, narc))
}
pub fn species(&self) -> Result<&[Species]> {
cached(&self.species, || {
let personal = self.rom.read(PERSONAL)?;
let evolutions = self.rom.read(EVOLUTIONS)?;
let learnsets = self.rom.read(LEARNSETS)?;
let names = self.text()?.strings(self.edition.species_names)?;
species::parse(&personal, &evolutions, &learnsets, &names)
})
.map(Vec::as_slice)
}
pub fn moves(&self) -> Result<&[Move]> {
cached(&self.moves, || {
let text = self.text()?;
let names = text.strings(self.edition.move_names)?;
let descriptions = text.strings(self.edition.move_descriptions)?;
moves::parse(&self.rom.read(MOVES)?, &names, &descriptions)
})
.map(Vec::as_slice)
}
pub fn abilities(&self) -> Result<&[Ability]> {
cached(&self.abilities, || {
let text = self.text()?;
let names = text.strings(self.edition.ability_names)?;
let descriptions = text.strings(self.edition.ability_descriptions)?;
Ok(species::abilities(self.species()?, &names, &descriptions))
})
.map(Vec::as_slice)
}
pub fn items(&self) -> Result<&[Item]> {
cached(&self.items, || {
let text = self.text()?;
let names = text.strings(self.edition.item_names)?;
let descriptions = text.strings(self.edition.item_descriptions)?;
items::parse(&self.rom.read(ITEMS)?, &names, &descriptions)
})
.map(Vec::as_slice)
}
}
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 = 0,
Fighting = 1,
Flying = 2,
Poison = 3,
Ground = 4,
Rock = 5,
Bug = 6,
Ghost = 7,
Steel = 8,
Fire = 9,
Water = 10,
Grass = 11,
Electric = 12,
Psychic = 13,
Ice = 14,
Dragon = 15,
Dark = 16,
}
}