use crate::import::{
bsi::{self, BsiFile},
palette::Palette,
};
use log::warn;
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
use walkdir::WalkDir;
pub struct AllFramesInfo {
pub width: u16,
pub height: u16,
pub frame_count: u16,
pub frames: Vec<Option<Vec<u8>>>,
}
pub struct TextureCache {
palette: Option<Palette>,
bsi_files: HashMap<u16, BsiFile>,
texbsi_paths: HashMap<u16, PathBuf>,
missing_texture_requests: HashSet<(u16, u8)>,
remapped_texture_requests: HashSet<(u16, u16)>,
}
impl TextureCache {
#[must_use]
pub fn new(asset_dir: PathBuf, palette: Option<Palette>) -> Self {
let mut texbsi_paths = HashMap::new();
for entry in WalkDir::new(asset_dir)
.into_iter()
.filter_map(std::result::Result::ok)
{
if !entry.file_type().is_file() {
continue;
}
let Some(file_name) = entry.path().file_name().and_then(|n| n.to_str()) else {
continue;
};
let upper = file_name.to_ascii_uppercase();
if !upper.starts_with("TEXBSI.") {
continue;
}
let Some(id_part) = upper.strip_prefix("TEXBSI.") else {
continue;
};
if let Ok(texture_id) = id_part.parse::<u16>() {
texbsi_paths
.entry(texture_id)
.or_insert_with(|| entry.path().to_path_buf());
}
}
Self {
palette,
bsi_files: HashMap::new(),
texbsi_paths,
missing_texture_requests: HashSet::new(),
remapped_texture_requests: HashSet::new(),
}
}
#[must_use]
pub fn from_data(texbsi_data: HashMap<u16, Vec<u8>>, palette: Option<Palette>) -> Self {
let mut bsi_files = HashMap::new();
let mut texbsi_paths = HashMap::new();
for (id, data) in texbsi_data {
match bsi::parse_bsi_file(&data) {
Ok(parsed) => {
bsi_files.insert(id, parsed);
texbsi_paths.insert(id, PathBuf::new());
}
Err(e) => {
warn!("Failed to parse pre-loaded TEXBSI.{id:03}: {e}");
}
}
}
Self {
palette,
bsi_files,
texbsi_paths,
missing_texture_requests: HashSet::new(),
remapped_texture_requests: HashSet::new(),
}
}
#[must_use]
pub const fn palette(&self) -> Option<&Palette> {
self.palette.as_ref()
}
fn warn_missing_once(&mut self, texture_id: u16, image_id: u8, reason: &str) {
if self.missing_texture_requests.insert((texture_id, image_id)) {
warn!(
"Texture lookup failed for TEXBSI.{texture_id:03} image {image_id}: {reason}; using white fallback material"
);
}
}
fn resolve_texbsi_id(&self, texture_id: u16) -> u16 {
if self.texbsi_paths.contains_key(&texture_id) {
return texture_id;
}
let folded_9bit = texture_id & 0x01FF;
if self.texbsi_paths.contains_key(&folded_9bit) {
return folded_9bit;
}
if texture_id > 500 {
let wrapped = texture_id % 500;
if self.texbsi_paths.contains_key(&wrapped) {
return wrapped;
}
}
texture_id
}
fn find_texbsi_path(&self, texture_id: u16) -> Option<(u16, PathBuf)> {
let resolved_id = self.resolve_texbsi_id(texture_id);
self.texbsi_paths
.get(&resolved_id)
.cloned()
.map(|path| (resolved_id, path))
}
fn warn_remapped_once(&mut self, requested_texture_id: u16, resolved_texture_id: u16) {
if requested_texture_id == resolved_texture_id {
return;
}
if self
.remapped_texture_requests
.insert((requested_texture_id, resolved_texture_id))
{
warn!(
"Remapped TEXBSI id {requested_texture_id} -> {resolved_texture_id} for texture lookup"
);
}
}
fn ensure_bsi_loaded(&mut self, texture_id: u16, image_id: u8) -> bool {
if self.bsi_files.contains_key(&texture_id) {
return true;
}
let resolved_id = self.resolve_texbsi_id(texture_id);
if resolved_id != texture_id
&& let Some(bsi) = self.bsi_files.get(&resolved_id).cloned()
{
self.warn_remapped_once(texture_id, resolved_id);
self.bsi_files.insert(texture_id, bsi);
return true;
}
let Some((resolved_texture_id, path)) = self.find_texbsi_path(texture_id) else {
self.warn_missing_once(texture_id, image_id, "file not found or unreadable");
return false;
};
self.warn_remapped_once(texture_id, resolved_texture_id);
let Ok(data) = std::fs::read(&path) else {
self.warn_missing_once(texture_id, image_id, "file not found or unreadable");
return false;
};
let Ok(bsi) = bsi::parse_bsi_file(&data) else {
self.warn_missing_once(texture_id, image_id, "failed to parse TEXBSI file");
return false;
};
self.bsi_files.insert(texture_id, bsi.clone());
self.bsi_files.entry(resolved_texture_id).or_insert(bsi);
true
}
pub fn ensure_bsi_available(&mut self, texture_id: u16) {
self.ensure_bsi_loaded(texture_id, 0);
}
#[must_use]
pub fn image_count(&self, texture_id: u16) -> Option<usize> {
self.bsi_files.get(&texture_id).map(|bsi| bsi.images.len())
}
pub fn get_image_rgba(&mut self, texture_id: u16, image_id: u8) -> Option<(Vec<u8>, u16, u16)> {
if !self.ensure_bsi_loaded(texture_id, image_id) {
return None;
}
let bsi = self.bsi_files.get(&texture_id)?;
let Some(image) = bsi
.images
.iter()
.find(|entry| entry.image_index == u16::from(image_id))
else {
self.warn_missing_once(texture_id, image_id, "image id not present in TEXBSI file");
return None;
};
let rgba = flip_rows_vertical(image.decode_rgba(self.palette.as_ref()), image.width);
Some((rgba, image.width, image.height))
}
pub fn get_image_rgba_with_frame_count(
&mut self,
texture_id: u16,
image_id: u8,
) -> Option<(Vec<u8>, u16, u16, u16)> {
if !self.ensure_bsi_loaded(texture_id, image_id) {
return None;
}
let bsi = self.bsi_files.get(&texture_id)?;
let Some(image) = bsi
.images
.iter()
.find(|entry| entry.image_index == u16::from(image_id))
else {
self.warn_missing_once(texture_id, image_id, "image id not present in TEXBSI file");
return None;
};
let rgba = flip_rows_vertical(image.decode_rgba(self.palette.as_ref()), image.width);
Some((rgba, image.width, image.height, image.frame_count))
}
pub fn get_all_frames_by_image_id(
&mut self,
texture_id: u16,
image_id: u8,
) -> Option<AllFramesInfo> {
if !self.ensure_bsi_loaded(texture_id, image_id) {
return None;
}
let bsi = self.bsi_files.get(&texture_id)?;
let Some(image) = bsi
.images
.iter()
.find(|entry| entry.image_index == u16::from(image_id))
else {
self.warn_missing_once(texture_id, image_id, "image id not present in TEXBSI file");
return None;
};
let mut frames = Vec::with_capacity(usize::from(image.frame_count));
for frame_idx in 0..usize::from(image.frame_count) {
frames.push(
image
.decode_frame_rgba(frame_idx, self.palette.as_ref())
.map(|rgba| flip_rows_vertical(rgba, image.width)),
);
}
Some(AllFramesInfo {
width: image.width,
height: image.height,
frame_count: image.frame_count,
frames,
})
}
pub fn get_image_dimensions(&mut self, texture_id: u16, image_id: u8) -> Option<(u16, u16)> {
if !self.ensure_bsi_loaded(texture_id, image_id) {
return None;
}
let bsi = self.bsi_files.get(&texture_id)?;
let Some(image) = bsi
.images
.iter()
.find(|entry| entry.image_index == u16::from(image_id))
else {
self.warn_missing_once(texture_id, image_id, "image id not present in TEXBSI file");
return None;
};
Some((image.width, image.height))
}
pub fn get_image_tex_scale(&mut self, texture_id: u16, image_id: u8) -> Option<f32> {
if !self.ensure_bsi_loaded(texture_id, image_id) {
return None;
}
let bsi = self.bsi_files.get(&texture_id)?;
let image = bsi
.images
.iter()
.find(|entry| entry.image_index == u16::from(image_id))?;
let raw = if image.tex_scale == 0 {
0x0100
} else {
image.tex_scale
};
Some(f32::from(raw) / 256.0)
}
pub fn get_image_png(
&mut self,
texture_id: u16,
image_id: u8,
compress: bool,
) -> Option<(Vec<u8>, u16, u16, bool)> {
let (rgba, width, height) = self.get_image_rgba(texture_id, image_id)?;
let has_alpha = rgba.as_chunks::<4>().0.iter().any(|pixel| pixel[3] == 0);
let Some(png_bytes) = encode_rgba_png(u32::from(width), u32::from(height), &rgba, compress)
else {
self.warn_missing_once(
texture_id,
image_id,
"failed to encode PNG (invalid image dimensions or pixel data)",
);
return None;
};
Some((png_bytes, width, height, has_alpha))
}
}
fn encode_rgba_png(width: u32, height: u32, rgba: &[u8], compress: bool) -> Option<Vec<u8>> {
use image::ImageEncoder;
use image::codecs::png::{CompressionType, FilterType, PngEncoder};
if width == 0 || height == 0 {
return None;
}
let expected = width as usize * height as usize * 4;
if rgba.len() != expected {
return None;
}
let mut buf = Vec::new();
let (compression, filter) = if compress {
(CompressionType::Default, FilterType::Adaptive)
} else {
(CompressionType::Fast, FilterType::NoFilter)
};
let encoder = PngEncoder::new_with_quality(&mut buf, compression, filter);
encoder
.write_image(rgba, width, height, image::ExtendedColorType::Rgba8)
.ok()?;
Some(buf)
}
fn flip_rows_vertical(rgba: Vec<u8>, width: u16) -> Vec<u8> {
let stride = usize::from(width) * 4;
if stride == 0 {
return rgba;
}
let mut flipped = Vec::with_capacity(rgba.len());
for row in rgba.chunks_exact(stride).rev() {
flipped.extend_from_slice(row);
}
flipped
}
pub(super) fn create_palette_color_png(rgb: [u8; 3], compress: bool) -> Option<Vec<u8>> {
let width = 8u32;
let height = 8u32;
let mut rgba = Vec::with_capacity((width * height * 4) as usize);
for _ in 0..(width * height) {
rgba.extend_from_slice(&[rgb[0], rgb[1], rgb[2], 255]);
}
encode_rgba_png(width, height, &rgba, compress)
}