use std::cmp::Reverse;
use std::sync::OnceLock;
use super::{Gen4, cached};
use crate::formats::narc::Narc;
use crate::formats::nitro3d::{Nsbmd, Textures};
use crate::games::GameId;
use crate::io::{ByteSource, Reader};
use crate::media::Mesh;
use crate::{Error, Result};
struct Archives {
maps: &'static str,
map_sounds: bool,
buildings: &'static str,
furniture: Option<&'static str>,
map_textures: &'static str,
building_textures: &'static str,
overworld: &'static str,
}
const DIAMOND_PEARL: Archives = Archives {
maps: "fielddata/land_data/land_data_release.narc",
map_sounds: false,
buildings: "fielddata/build_model/build_model.narc",
furniture: None,
map_textures: "fielddata/areadata/area_map_tex/map_tex_set.narc",
building_textures: "fielddata/areadata/area_build_model/areabm_texset.narc",
overworld: "data/mmodel/mmodel.narc",
};
const PLATINUM: Archives = Archives {
maps: "fielddata/land_data/land_data.narc",
..DIAMOND_PEARL
};
const HEARTGOLD_SOULSILVER: Archives = Archives {
maps: "a/0/6/5",
map_sounds: true,
buildings: "fielddata/build_model/bm_field.narc",
furniture: Some("fielddata/build_model/bm_room.narc"),
map_textures: "a/0/4/4",
building_textures: "a/0/7/0",
overworld: "a/0/8/1",
};
impl Archives {
fn of(game: GameId) -> &'static Self {
match game {
GameId::PokemonPlatinum => &PLATINUM,
GameId::PokemonHeartGold | GameId::PokemonSoulSilver => &HEARTGOLD_SOULSILVER,
_ => &DIAMOND_PEARL,
}
}
fn models(&self, set: ModelSet) -> Option<&'static str> {
match set {
ModelSet::Maps => Some(self.maps),
ModelSet::Buildings => Some(self.buildings),
ModelSet::Furniture => self.furniture,
}
}
fn textures(&self, set: TextureSet) -> &'static str {
match set {
TextureSet::Maps => self.map_textures,
TextureSet::Buildings => self.building_textures,
TextureSet::Overworld => self.overworld,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelSet {
Maps,
Buildings,
Furniture,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TextureSet {
Maps,
Buildings,
Overworld,
}
const MODEL_SETS: [(ModelSet, &str); 3] = [
(ModelSet::Maps, "maps"),
(ModelSet::Buildings, "buildings"),
(ModelSet::Furniture, "furniture"),
];
const TEXTURE_SETS: [(TextureSet, &str); 3] = [
(TextureSet::Maps, "maps"),
(TextureSet::Buildings, "buildings"),
(TextureSet::Overworld, "overworld"),
];
#[derive(Default)]
pub(super) struct Cache {
names: [OnceLock<Vec<Vec<String>>>; 3],
}
impl<S: ByteSource> Gen4<S> {
pub fn model_count(&self, set: ModelSet) -> Result<usize> {
match Archives::of(self.game).models(set) {
Some(archive) => Ok(Narc::parse(&self.rom.file(archive)?)?.len()),
None => Ok(0),
}
}
pub fn model(&self, set: ModelSet, index: usize) -> Result<Mesh> {
let archives = Archives::of(self.game);
let archive = archives
.models(set)
.ok_or_else(|| Error::NotFound(format!("{set:?} models")))?;
let file = self.rom.file(archive)?;
let member = Narc::parse(&file)?.member(&file, index)?;
let bytes = match set {
ModelSet::Maps => terrain(&member, archives.map_sounds)?,
_ => &member,
};
let nsbmd = Nsbmd::parse(bytes)?;
let model = nsbmd
.models
.first()
.ok_or_else(|| Error::malformed("NSBMD", 0, "holds no model"))?;
let own: Vec<&Textures> = nsbmd.textures.iter().collect();
let missing: Vec<&str> = model
.materials
.iter()
.map(|material| material.texture_name())
.filter(|name| !own.iter().any(|textures| textures.texture(name).is_some()))
.collect();
let texture_set = match set {
ModelSet::Maps => TextureSet::Maps,
_ => TextureSet::Buildings,
};
let shared = self.shared_textures(texture_set, missing)?;
model.mesh(&own.into_iter().chain(&shared).collect::<Vec<_>>())
}
pub fn texture_names(&self, set: TextureSet) -> Result<&[Vec<String>]> {
let index = TEXTURE_SETS
.iter()
.position(|&(s, _)| s == set)
.expect("listed");
cached(&self.field.names[index], || {
let file = self.rom.file(Archives::of(self.game).textures(set))?;
let narc = Narc::parse(&file)?;
(0..narc.len())
.map(|member| {
let member = narc.member_source(&file, member)?;
match &*member.read_up_to(0, 4)? {
b"BTX0" => Textures::names(&member),
_ => Ok(Vec::new()),
}
})
.collect()
})
.map(Vec::as_slice)
}
pub fn textures(&self, set: TextureSet, member: usize) -> Result<Textures> {
let file = self.rom.file(Archives::of(self.game).textures(set))?;
Textures::parse(&Narc::parse(&file)?.member(&file, member)?)
}
fn shared_textures(&self, set: TextureSet, mut wanted: Vec<&str>) -> Result<Vec<Textures>> {
let names = self.texture_names(set)?;
let mut sets = Vec::new();
while !wanted.is_empty() {
let held = |names: &[String]| {
wanted
.iter()
.filter(|w| names.iter().any(|n| n == *w))
.count()
};
let best = names
.iter()
.enumerate()
.map(|(member, names)| (held(names), Reverse(member)))
.max()
.filter(|&(held, _)| held > 0);
let Some((_, Reverse(member))) = best else {
break;
};
wanted.retain(|w| !names[member].iter().any(|n| n == w));
sets.push(self.textures(set, member)?);
}
Ok(sets)
}
pub(super) fn field_assets(&self) -> Result<Vec<String>> {
let mut assets = Vec::new();
for (set, directory) in MODEL_SETS {
let count = self.model_count(set)?;
assets.extend((0..count).map(|index| format!("models/{directory}/{index:04}.glb")));
}
for (set, directory) in TEXTURE_SETS {
for (member, names) in self.texture_names(set)?.iter().enumerate() {
let mut files: Vec<String> = Vec::new();
for file in names.iter().map(|name| file_name(name)) {
if !files.contains(&file) {
files.push(file);
}
}
assets.extend(
files
.into_iter()
.map(|file| format!("textures/{directory}/{member:04}/{file}.png")),
);
}
}
Ok(assets)
}
pub(super) fn extract_field(&self, path: &str) -> Option<Result<Vec<u8>>> {
if let Some(path) = path.strip_prefix("models/") {
let (directory, file) = path.split_once('/')?;
let (set, _) = MODEL_SETS.into_iter().find(|&(_, d)| d == directory)?;
let index = file.strip_suffix(".glb")?.parse().ok()?;
return Some(self.model(set, index).and_then(|mesh| mesh.to_glb()));
}
let (directory, path) = path.strip_prefix("textures/")?.split_once('/')?;
let (set, _) = TEXTURE_SETS.into_iter().find(|&(_, d)| d == directory)?;
let (member, file) = path.split_once('/')?;
let (member, file) = (member.parse().ok()?, file.strip_suffix(".png")?);
let png = || {
let textures = self.textures(set, member)?;
let texture = textures
.textures
.iter()
.find(|texture| file_name(&texture.name) == file)
.ok_or_else(|| {
Error::NotFound(format!("texture {file} in {directory} {member}"))
})?;
textures
.image(texture, textures.palette_for(texture))?
.to_png()
};
Some(png())
}
}
fn terrain(block: &[u8], sounds: bool) -> Result<&[u8]> {
let block = Reader::new("map block", block);
let header = match sounds {
true => 20 + block.u16_at(18)? as usize,
false => 16,
};
let start = header
.saturating_add(block.u32_at(0)? as usize)
.saturating_add(block.u32_at(4)? as usize);
block.slice_at(start, block.u32_at(8)? as usize)
}
fn file_name(name: &str) -> String {
name.chars()
.map(|c| match c.is_ascii_alphanumeric() || "._-".contains(c) {
true => c,
false => '_',
})
.collect()
}