mod assets;
mod moves;
mod species;
pub mod text;
use std::sync::OnceLock;
pub use moves::{Ailment, Move, MoveCategory, MoveClass, MoveFlags, MoveTarget, StatChange, ZMove};
pub use species::{Evolution, EvolutionMethod, Form, HeldItems, Species, ZCrystal};
use text::Text;
pub use super::gen4::{Ability, AbilityId, ItemId, MoveId, SpeciesId};
use crate::formats::garc::Garc;
use crate::games::GameId;
use crate::io::{ByteSource, Window};
use crate::platforms::n3ds::N3dsRom;
use crate::{Error, Result};
pub const LANGUAGES: [&str; 10] = [
"ja-kana", "ja-kanji", "en", "fr", "it", "de", "es", "ko", "zh-hans", "zh-hant",
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextSet {
Main,
Story,
}
#[derive(Debug)]
pub struct Edition {
generation: u8,
personal: &'static str,
personal_len: usize,
evolutions: &'static str,
learnsets: &'static str,
moves: &'static str,
main_text: &'static str,
story_text: &'static str,
languages: usize,
species_names: usize,
move_names: usize,
move_descriptions: usize,
ability_names: usize,
ability_descriptions: usize,
}
const X_Y: Edition = Edition {
generation: 6,
personal: "a/2/1/8",
personal_len: 0x40,
evolutions: "a/2/1/5",
learnsets: "a/2/1/4",
moves: "a/2/1/2",
main_text: "a/0/7/2",
story_text: "a/0/8/0",
languages: 8,
species_names: 80,
move_names: 13,
move_descriptions: 15,
ability_names: 34,
ability_descriptions: 33,
};
const OMEGA_RUBY_ALPHA_SAPPHIRE: Edition = Edition {
generation: 6,
personal: "a/1/9/5",
personal_len: 0x50,
evolutions: "a/1/9/2",
learnsets: "a/1/9/1",
moves: "a/1/8/9",
main_text: "a/0/7/1",
story_text: "a/0/7/9",
languages: 8,
species_names: 98,
move_names: 14,
move_descriptions: 16,
ability_names: 37,
ability_descriptions: 36,
};
const SUN_MOON: Edition = Edition {
generation: 7,
personal: "a/0/1/7",
personal_len: 0x54,
evolutions: "a/0/1/4",
learnsets: "a/0/1/3",
moves: "a/0/1/1",
main_text: "a/0/3/0",
story_text: "a/0/4/0",
languages: 10,
species_names: 55,
move_names: 113,
move_descriptions: 112,
ability_names: 96,
ability_descriptions: 97,
};
const ULTRA_SUN_ULTRA_MOON: Edition = Edition {
species_names: 60,
move_names: 118,
move_descriptions: 117,
ability_names: 101,
ability_descriptions: 102,
..SUN_MOON
};
impl Edition {
pub fn of(game: GameId) -> Option<&'static Self> {
match game {
GameId::PokemonX | GameId::PokemonY => Some(&X_Y),
GameId::PokemonOmegaRuby | GameId::PokemonAlphaSapphire => {
Some(&OMEGA_RUBY_ALPHA_SAPPHIRE)
}
GameId::PokemonSun | GameId::PokemonMoon => Some(&SUN_MOON),
GameId::PokemonUltraSun | GameId::PokemonUltraMoon => Some(&ULTRA_SUN_ULTRA_MOON),
_ => None,
}
}
fn bank_name(&self, set: TextSet, bank: usize) -> Option<&'static str> {
let named = [
(self.species_names, "species_names"),
(self.move_names, "move_names"),
(self.move_descriptions, "move_descriptions"),
(self.ability_names, "ability_names"),
(self.ability_descriptions, "ability_descriptions"),
];
let (_, name) = named.into_iter().find(|&(known, _)| known == bank)?;
(set == TextSet::Main).then_some(name)
}
}
pub struct N3ds<S> {
game: GameId,
edition: &'static Edition,
rom: N3dsRom<S>,
texts: [OnceLock<Garc>; 2 * LANGUAGES.len()],
species: OnceLock<Vec<Species>>,
moves: OnceLock<Vec<Move>>,
abilities: OnceLock<Vec<Ability>>,
}
impl<S: ByteSource> N3ds<S> {
pub fn open(source: S) -> Result<Self> {
let rom = N3dsRom::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 3DS Pokémon game")))?;
Ok(Self {
game,
edition: Edition::of(game).expect("filtered above"),
rom,
texts: Default::default(),
species: OnceLock::new(),
moves: OnceLock::new(),
abilities: OnceLock::new(),
})
}
pub fn game(&self) -> GameId {
self.game
}
pub fn rom(&self) -> &N3dsRom<S> {
&self.rom
}
pub fn languages(&self) -> &'static [&'static str] {
&LANGUAGES[..self.edition.languages]
}
pub fn text(&self, set: TextSet, language: &str) -> Result<Text<'_, Window<&S>>> {
let index = self
.languages()
.iter()
.position(|&known| known == language)
.ok_or_else(|| Error::NotFound(format!("{language} text")))?;
let (first, slot) = match set {
TextSet::Main => (self.edition.main_text, index),
TextSet::Story => (self.edition.story_text, LANGUAGES.len() + index),
};
let archive = self.rom.file(&archive_path(first, index))?;
let garc = cached(&self.texts[slot], || Garc::parse(&archive))?;
Ok(Text::new(archive, garc))
}
pub fn species(&self) -> Result<&[Species]> {
cached(&self.species, || {
let edition = self.edition;
let names = self
.text(TextSet::Main, "en")?
.strings(edition.species_names)?;
species::parse(
edition,
&self.rom.read(edition.personal)?,
&self.rom.read(edition.evolutions)?,
&self.rom.read(edition.learnsets)?,
&names,
)
})
.map(Vec::as_slice)
}
pub fn moves(&self) -> Result<&[Move]> {
cached(&self.moves, || {
let text = self.text(TextSet::Main, "en")?;
let names = text.strings(self.edition.move_names)?;
let descriptions = text.strings(self.edition.move_descriptions)?;
moves::parse(
self.edition.generation,
&self.rom.read(self.edition.moves)?,
&names,
&descriptions,
)
})
.map(Vec::as_slice)
}
pub fn abilities(&self) -> Result<&[Ability]> {
cached(&self.abilities, || {
let text = self.text(TextSet::Main, "en")?;
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)
}
}
fn archive_path(first: &str, offset: usize) -> String {
let number = first
.bytes()
.filter(u8::is_ascii_digit)
.fold(0, |number, digit| number * 10 + (digit - b'0') as usize)
+ offset;
format!("a/{}/{}/{}", number / 100, number / 10 % 10, number % 10)
}
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,
Fairy = 17,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn archive_paths_count_in_decimal() {
assert_eq!(archive_path("a/0/7/9", 1), "a/0/8/0");
assert_eq!(archive_path("a/0/3/0", 9), "a/0/3/9");
assert_eq!(archive_path("a/2/1/8", 0), "a/2/1/8");
}
#[test]
fn every_3ds_pokemon_game_has_an_edition() {
use GameId::*;
let games = [
PokemonX,
PokemonY,
PokemonOmegaRuby,
PokemonAlphaSapphire,
PokemonSun,
PokemonMoon,
PokemonUltraSun,
PokemonUltraMoon,
];
assert!(games.into_iter().all(|game| Edition::of(game).is_some()));
assert!(Edition::of(PokemonPlatinum).is_none());
assert_eq!(X_Y.bank_name(TextSet::Main, 80), Some("species_names"));
assert_eq!(X_Y.bank_name(TextSet::Story, 80), None);
}
}