use crate::media::Media;
use crate::rng::StreamRng;
#[derive(Clone, Debug)]
pub struct Image {
pub width: u32,
pub height: u32,
pub rgba8: Vec<u8>,
}
pub trait TextureProvider {
fn load(&mut self, name: &str) -> Option<Image>;
fn random_pool(&self) -> Vec<String>;
fn load_md3_texture(&mut self, name: &str) -> Option<Media> {
self.load(name).map(Media::Still)
}
fn load_sprite(&mut self, _path: &str) -> Option<Media> {
None
}
}
pub(crate) fn split_qualified(full: &str) -> (&str, bool, bool) {
let b = full.as_bytes();
if b.len() <= 3 || b[2] != b'_' {
return (full, true, true);
}
let rest = &full[3..]; let (linear, wrap) = match full.get(..3).map(str::to_ascii_lowercase).as_deref() {
Some("fc_" | "cf_") => (true, false),
Some("pw_" | "wp_") => (false, true),
Some("pc_" | "cp_") => (false, false),
_ => (true, true),
};
(rest, linear, wrap)
}
pub(crate) fn shape_image(full: &str) -> Option<lactea_render::ShapeImage> {
if full.is_empty() {
return None;
}
let (name, linear, wrap) = split_qualified(full);
Some(lactea_render::ShapeImage {
name: name.to_ascii_lowercase(),
linear,
wrap,
})
}
pub(crate) fn rand_slot(root: &str) -> Option<usize> {
let b = root.as_bytes();
if b.len() >= 6 && &b[..4] == b"rand" && b[4].is_ascii_digit() && b[5].is_ascii_digit() {
let slot = usize::from(b[4] - b'0') * 10 + usize::from(b[5] - b'0');
if slot <= 15 {
return Some(slot);
}
}
None
}
pub(crate) fn pick_random(seed: u64, name: &str, pool: &[String]) -> Option<String> {
if pool.is_empty() {
return None;
}
let lower = name.to_ascii_lowercase();
let mut rng = StreamRng::new(seed, &format!("randtex_{lower}"));
let prefix = if lower.len() > 7 && lower.as_bytes()[6] == b'_' {
&lower[7..]
} else {
""
};
if prefix.is_empty() {
let i = (rng.next64() % pool.len() as u64) as usize;
return Some(pool[i].clone());
}
let filtered: Vec<&String> = pool
.iter()
.filter(|n| n.to_ascii_lowercase().starts_with(prefix))
.collect();
if filtered.is_empty() {
return None;
}
let i = (rng.next64() % filtered.len() as u64) as usize;
Some(filtered[i].clone())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn slots() {
assert_eq!(rand_slot("rand03"), Some(3));
assert_eq!(rand_slot("rand15_smalltiled"), Some(15));
assert_eq!(rand_slot("rand16"), None);
assert_eq!(rand_slot("randx1"), None);
assert_eq!(rand_slot("clouds"), None);
}
#[test]
fn pick_filters_by_prefix_only_without_quality_prefix() {
let pool: Vec<String> = ["clouds", "smalltiled_a", "smalltiled_b", "zzz"]
.iter()
.map(|s| s.to_string())
.collect();
for seed in 0..20 {
let p = pick_random(seed, "rand03_smalltiled", &pool).unwrap();
assert!(p.starts_with("smalltiled"));
}
let mut seen = std::collections::HashSet::new();
for seed in 0..64 {
seen.insert(pick_random(seed, "fw_rand03_smalltiled", &pool).unwrap());
}
assert!(seen.len() > 2);
assert!(pick_random(1, "rand01", &[]).is_none());
assert!(pick_random(1, "rand01_nomatch", &pool).is_none());
}
#[test]
fn quality_prefixes_pick_filter_and_wrap() {
assert_eq!(split_qualified("clouds"), ("clouds", true, true));
assert_eq!(split_qualified("fw_clouds"), ("clouds", true, true));
assert_eq!(split_qualified("WF_Clouds"), ("Clouds", true, true));
assert_eq!(split_qualified("fc_clouds"), ("clouds", true, false));
assert_eq!(split_qualified("cf_clouds"), ("clouds", true, false));
assert_eq!(split_qualified("pw_clouds"), ("clouds", false, true));
assert_eq!(split_qualified("PC_clouds"), ("clouds", false, false));
assert_eq!(split_qualified("xx_clouds"), ("clouds", true, true)); assert_eq!(split_qualified("a_b"), ("a_b", true, true));
assert_eq!(split_qualified("ab_"), ("ab_", true, true));
assert_eq!(split_qualified("abc_d"), ("abc_d", true, true));
assert_eq!(split_qualified("éé_x"), ("éé_x", true, true)); assert_eq!(split_qualified("é_é_x"), ("é_x", true, true)); let im = shape_image("PC_Idle").unwrap();
assert_eq!(
(im.name.as_str(), im.linear, im.wrap),
("idle", false, false)
);
assert!(shape_image("").is_none());
}
}