use crate::media::Media;
use crate::rng::StreamRng;
use lactea_render::CompatRules;
#[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, rules: &CompatRules) -> Option<usize> {
lactea_hlsl::rand_slot(root, rules.max_rand_slot, !rules.rand_filter_raw_name)
}
pub(crate) fn pick_for_sampler(
seed: u64,
rules: &CompatRules,
raw: &str,
root: &str,
pool: &[String],
) -> Option<String> {
pick_random(
seed,
if rules.rand_filter_raw_name {
raw
} else {
root
},
pool,
)
}
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::*;
use lactea_render::Compat;
#[test]
fn slot_range_and_spelling_follow_the_compat_rules() {
let (pm, md2) = (Compat::ProjectM.rules(), Compat::MilkDrop2.rules());
assert_eq!(rand_slot("rand15_smalltiled", &pm), Some(15));
assert_eq!(rand_slot("rand16", &pm), None);
assert_eq!(rand_slot("rand16", &md2), Some(16));
assert_eq!(rand_slot("rand99", &md2), Some(99));
assert_eq!(rand_slot("rand03x", &pm), Some(3));
assert_eq!(rand_slot("rand03x", &md2), None);
assert_eq!(rand_slot("randx1", &pm), None);
assert_eq!(rand_slot("clouds", &md2), None);
}
#[test]
fn pool_filter_survives_a_quality_prefix_only_in_md2() {
let pool: Vec<String> = ["clouds", "smalltiled_a", "smalltiled_b", "zzz"]
.iter()
.map(|s| s.to_string())
.collect();
let (pm, md2) = (Compat::ProjectM.rules(), Compat::MilkDrop2.rules());
let (raw, root) = ("fw_rand03_smalltiled", "rand03_smalltiled");
for seed in 0..20 {
let p = pick_for_sampler(seed, &md2, raw, root, &pool).unwrap();
assert!(p.starts_with("smalltiled"));
}
let mut seen = std::collections::HashSet::new();
for seed in 0..64 {
seen.insert(pick_for_sampler(seed, &pm, raw, root, &pool).unwrap());
}
assert!(seen.len() > 2);
for seed in 0..20 {
let p = pick_for_sampler(seed, &pm, root, root, &pool).unwrap();
assert!(p.starts_with("smalltiled"));
}
assert!(pick_for_sampler(1, &pm, "rand01", "rand01", &[]).is_none());
assert!(pick_for_sampler(1, &pm, "rand01_nomatch", "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());
}
}