use quick_xml::de::from_str as xml_from_str;
use std::path::Path;
use crate::formats::lsf::LsfDocument;
use super::types::{ModConfig, VTexConfigXml, VirtualTexturesJson};
#[must_use]
pub fn find_mod_root(gtp_path: &Path) -> Option<(std::path::PathBuf, String)> {
let mut current = gtp_path.parent()?;
loop {
if let Some(name) = current.file_name().and_then(|n| n.to_str()) {
if name == "Public" {
let mod_root = current.parent()?.to_path_buf();
let remaining = gtp_path.strip_prefix(current).ok()?;
let mod_name = remaining.iter().next()?.to_str()?.to_string();
return Some((mod_root, mod_name));
}
}
current = current.parent()?;
}
}
pub fn find_mod_name_from_mods_dir(mod_root: &Path) -> Option<String> {
let mods_dir = mod_root.join("Mods");
if !mods_dir.is_dir() {
return None;
}
for entry in std::fs::read_dir(&mods_dir).ok()?.flatten() {
let path = entry.path();
if path.is_dir() {
let has_vtex_config = path.join("VTexConfig.xml").exists();
let has_script_extender = path.join("ScriptExtender").is_dir();
if has_vtex_config || has_script_extender {
return path.file_name()?.to_str().map(String::from);
}
}
}
None
}
#[must_use]
pub fn load_virtual_textures_json(mod_root: &Path, mod_name: &str) -> Option<VirtualTexturesJson> {
let json_path = mod_root
.join("Mods")
.join(mod_name)
.join("ScriptExtender")
.join("VirtualTextures.json");
if !json_path.exists() {
return None;
}
let content = std::fs::read_to_string(&json_path).ok()?;
serde_json::from_str(&content).ok()
}
#[must_use]
pub fn load_vtex_config_xml(mod_root: &Path, mod_name: &str) -> Option<VTexConfigXml> {
let xml_path = mod_root.join("Mods").join(mod_name).join("VTexConfig.xml");
if !xml_path.exists() {
return None;
}
let content = std::fs::read_to_string(&xml_path).ok()?;
xml_from_str(&content).ok()
}
#[derive(Debug)]
pub struct LsfVTexConfig {
pub tileset_name: String,
pub virtual_textures_path: Option<String>,
pub texture_names: Vec<String>,
}
#[must_use]
pub fn parse_vtex_config_from_lsf(doc: &LsfDocument) -> Option<LsfVTexConfig> {
let root_nodes = doc.root_nodes();
let tileset_idx = root_nodes
.iter()
.find(|&&idx| doc.node_name(idx) == Some("TileSet"))?;
let tileset_name = doc.get_fixed_string_attr(*tileset_idx, "Name")?;
let paths_nodes = doc.find_children_by_name(*tileset_idx, "Paths");
let virtual_textures_path = if let Some(&paths_idx) = paths_nodes.first() {
let vt_nodes = doc.find_children_by_name(paths_idx, "VirtualTextures");
vt_nodes.first().and_then(|&vt_idx| {
get_text_content(doc, vt_idx)
})
} else {
None
};
let textures_nodes = doc.find_children_by_name(*tileset_idx, "Textures");
let texture_names = if let Some(&textures_idx) = textures_nodes.first() {
doc.find_children_by_name(textures_idx, "Texture")
.iter()
.filter_map(|&tex_idx| doc.get_fixed_string_attr(tex_idx, "Name"))
.collect()
} else {
Vec::new()
};
Some(LsfVTexConfig {
tileset_name,
virtual_textures_path,
texture_names,
})
}
fn get_text_content(doc: &LsfDocument, node_idx: usize) -> Option<String> {
for (_, name, type_id, offset, length) in doc.attributes_of(node_idx) {
if (name.is_empty() || name == "value") && (type_id == 22 || type_id == 19) {
if offset + length > doc.values.len() {
continue;
}
let bytes = &doc.values[offset..offset + length];
if type_id == 22 {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
return String::from_utf8(bytes[..end].to_vec()).ok();
} else if type_id == 19 && length >= 4 {
let str_len = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize;
if str_len > 0 && 4 + str_len <= bytes.len() {
let str_bytes = &bytes[4..4 + str_len - 1]; return String::from_utf8(str_bytes.to_vec()).ok();
}
}
}
}
None
}
#[must_use]
pub fn load_mod_config(gtp_path: &Path) -> Option<ModConfig> {
let (mod_root, mod_name) = find_mod_root(gtp_path)?;
let vtex_json = load_virtual_textures_json(&mod_root, &mod_name);
let vtex_xml = load_vtex_config_xml(&mod_root, &mod_name);
let (tileset_name, gtex_hashes) = if let Some(ref xml) = vtex_xml {
let hashes = xml
.textures
.as_ref()
.map(|t| t.textures.iter().map(|tex| tex.name.clone()).collect())
.unwrap_or_default();
(Some(xml.name.clone()), hashes)
} else {
(None, Vec::new())
};
Some(ModConfig {
mod_root,
mod_name,
tileset_name,
gtex_hashes,
mappings: vtex_json.map(|v| v.mappings).unwrap_or_default(),
})
}