use std::collections::HashMap;
use std::collections::HashSet;
use std::io::Read;
use vfs::VfsPath;
pub struct ColorScheme {
pub name: String,
pub colors: [[f32; 4]; 4],
}
#[derive(Clone, Debug)]
pub struct UvTransform {
pub scale: [f32; 2],
pub offset: [f32; 2],
}
impl Default for UvTransform {
fn default() -> Self {
Self { scale: [1.0, 1.0], offset: [0.0, 0.0] }
}
}
pub struct CamouflageEntry {
pub name: String,
pub tiled: bool,
pub textures: HashMap<String, String>,
pub color_scheme: Option<String>,
pub uv_transforms: HashMap<String, UvTransform>,
pub ship_groups: Vec<String>,
}
pub fn classify_part_category(mfm_stem: &str) -> &'static str {
let lower = mfm_stem.to_lowercase();
if lower.ends_with("_hull") || lower.ends_with("_hull_wire") {
return "tile"; }
if lower.ends_with("_deckhouse") {
return "deckhouse";
}
if lower.contains("_bulge") {
return "bulge";
}
let bytes = mfm_stem.as_bytes();
if bytes.len() >= 4 && bytes[0].is_ascii_uppercase() {
let cat = &mfm_stem[1..3];
match cat {
"GM" | "GS" | "GA" => return "gun",
"D0" | "D1" => return "director",
"F0" | "F1" => return "director",
"RS" => return "misc",
_ => {}
}
}
if lower.contains("_hull") {
return "tile";
}
"tile"
}
pub struct CamouflageDb {
entries: HashMap<String, Vec<CamouflageEntry>>,
color_schemes: HashMap<String, ColorScheme>,
ship_groups: HashMap<String, HashSet<String>>,
}
impl CamouflageDb {
pub fn load(vfs: &VfsPath) -> Option<Self> {
let mut xml_bytes = Vec::new();
vfs.join("camouflages.xml").ok()?.open_file().ok()?.read_to_end(&mut xml_bytes).ok()?;
let xml_str = String::from_utf8_lossy(&xml_bytes);
Self::parse(&xml_str)
}
fn parse(xml: &str) -> Option<Self> {
let doc = roxmltree::Document::parse(xml).ok()?;
let mut ship_groups: HashMap<String, HashSet<String>> = HashMap::new();
if let Some(sg_node) = doc
.root()
.children()
.find(|n| n.is_element())
.and_then(|data| data.children().find(|n| n.has_tag_name("shipgroups.xml")))
{
for group_node in sg_node.children().filter(|n| n.is_element()) {
let group_name = group_node.tag_name().name().to_string();
if let Some(ships_node) = group_node.children().find(|n| n.has_tag_name("ships"))
&& let Some(text) = ships_node.text()
{
let indices: HashSet<String> = text.split_whitespace().map(|s| s.to_string()).collect();
ship_groups.insert(group_name, indices);
}
}
}
let mut color_schemes = HashMap::new();
for cs_node in doc.descendants().filter(|n| n.has_tag_name("colorScheme")) {
let Some(name) = child_text(&cs_node, "name").map(|s| s.trim()) else {
continue;
};
if name.is_empty() {
continue;
}
let mut colors = [[0.0f32; 4]; 4];
for (i, color) in colors.iter_mut().enumerate() {
let tag = format!("color{i}");
if let Some(text) = child_text(&cs_node, &tag) {
let parts: Vec<f32> = text.split_whitespace().filter_map(|s| s.parse().ok()).collect();
if parts.len() >= 4 {
*color = [parts[0], parts[1], parts[2], parts[3]];
}
}
}
color_schemes.insert(name.to_string(), ColorScheme { name: name.to_string(), colors });
}
let mut entries: HashMap<String, Vec<CamouflageEntry>> = HashMap::new();
for camo_node in doc.descendants().filter(|n| n.has_tag_name("camouflage")) {
let Some(name) = child_text(&camo_node, "name").map(|s| s.trim()) else {
continue;
};
if name.is_empty() {
continue;
}
let tiled = child_text(&camo_node, "tiled").map(|s| s.trim() == "true").unwrap_or(false);
let mut textures = HashMap::new();
if let Some(tex_node) = camo_node.children().find(|n| n.has_tag_name("Textures")) {
for child in tex_node.children().filter(|n| n.is_element()) {
let tag = child.tag_name().name();
if tag.ends_with("_mgn") || tag.ends_with("_animmap") {
continue;
}
if let Some(path) = child.text().map(|t| t.trim().to_string())
&& !path.is_empty()
{
textures.insert(tag.to_lowercase(), path);
}
}
}
let color_scheme = child_text(&camo_node, "colorSchemes")
.map(|s| s.trim())
.and_then(|s| s.split_whitespace().next())
.map(|s| s.to_string())
.filter(|s| !s.is_empty());
let camo_ship_groups: Vec<String> = child_text(&camo_node, "shipGroups")
.map(|s| s.split_whitespace().map(|g| g.to_string()).collect())
.unwrap_or_default();
let mut uv_transforms = HashMap::new();
if let Some(uv_node) = camo_node.children().find(|n| n.has_tag_name("UV")) {
for child in uv_node.children().filter(|n| n.is_element()) {
let tag = child.tag_name().name().to_lowercase();
let scale = child_text(&child, "scale")
.map(|s| {
let parts: Vec<f32> = s.split_whitespace().filter_map(|v| v.parse().ok()).collect();
if parts.len() >= 2 { [parts[0], parts[1]] } else { [1.0, 1.0] }
})
.unwrap_or([1.0, 1.0]);
let offset = child_text(&child, "offset")
.map(|s| {
let parts: Vec<f32> = s.split_whitespace().filter_map(|v| v.parse().ok()).collect();
if parts.len() >= 2 { [parts[0], parts[1]] } else { [0.0, 0.0] }
})
.unwrap_or([0.0, 0.0]);
uv_transforms.insert(tag, UvTransform { scale, offset });
}
}
entries.entry(name.to_string()).or_default().push(CamouflageEntry {
name: name.to_string(),
tiled,
textures,
color_scheme,
uv_transforms,
ship_groups: camo_ship_groups,
});
}
Some(Self { entries, color_schemes, ship_groups })
}
pub fn get(&self, name: &str, ship_index: Option<&str>) -> Option<&CamouflageEntry> {
let variants = self.entries.get(name)?;
if variants.len() == 1 {
return variants.first();
}
if let Some(idx) = ship_index {
for entry in variants {
if entry.ship_groups.is_empty() {
continue;
}
for group_name in &entry.ship_groups {
if let Some(members) = self.ship_groups.get(group_name)
&& members.contains(idx)
{
return Some(entry);
}
}
}
}
variants.iter().find(|e| e.ship_groups.is_empty()).or(variants.first())
}
pub fn color_scheme(&self, name: &str) -> Option<&ColorScheme> {
self.color_schemes.get(name)
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn total_entries(&self) -> usize {
self.entries.values().map(|v| v.len()).sum()
}
}
fn child_text<'a>(node: &'a roxmltree::Node, tag: &str) -> Option<&'a str> {
node.children().find(|n| n.has_tag_name(tag))?.text()
}