use crate::assets::gumpart; use crate::assets::art::ArtImage; use crate::commands::asset_commands::ArtImageResult; use serde::{Deserialize, Serialize}; use std::path::Path; /// A single entry from gumps.xml (art gump with numeric ID). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GumpEntry { pub id: u32, pub name: String, pub tags: Vec, } /// A single entry from script_gumps.xml (C# Gump class from ServUO Scripts). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ScriptGumpEntry { pub class_name: String, pub file_path: String, pub tags: Vec, } /// Parse gumps.xml from the "UO Gumps" folder and return all entries. /// Supports both decimal IDs and 0x-prefixed hex IDs. #[tauri::command] pub fn list_gumps(app: tauri::AppHandle) -> Result, String> { let gump_dir = gumpart::find_gump_dir(&app) .ok_or_else(|| "UO Gumps folder not found".to_string())?; let xml_path = gump_dir.join("gumps.xml"); if !xml_path.exists() { return Err("gumps.xml not found in UO Gumps folder".to_string()); } let content = std::fs::read_to_string(&xml_path) .map_err(|e| format!("Failed to read gumps.xml: {}", e))?; let mut entries = Vec::new(); for line in content.lines() { let trimmed = line.trim(); if !trimmed.starts_with(" = if let Some(t) = tags { t.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect() } else { Vec::new() }; entries.push(GumpEntry { id, name, tags: tags_vec }); } } Ok(entries) } fn parse_attr(line: &str, attr: &str) -> Option { let needle = format!("{}=\"", attr); let start = line.find(&needle)? + needle.len(); let rest = &line[start..]; let end = rest.find('"')?; Some(rest[..end].to_string()) } fn parse_gump_id(s: &str) -> Option { if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) { u32::from_str_radix(hex, 16).ok() } else { s.parse::().ok() } } /// Get a gump image by gump ID. /// Priority: bundled BMP → gumpart.mul → gumpartLegacyMUL.uop #[tauri::command] pub fn get_gump_art( app: tauri::AppHandle, gump_id: u32, uo_root: String, ) -> Result { // 1. Bundled BMP if let Some(gump_dir) = gumpart::find_gump_dir(&app) { if let Ok(img) = gumpart::decode_gump_bmp(&gump_dir, gump_id as usize) { return Ok(art_image_to_result(img)); } } // 2. Fall back to MUL / UOP client files let root = Path::new(&uo_root); let mul_path = root.join("gumpart.mul"); let idx_path = root.join("gumpartidx.mul"); let uop_path = root.join("gumpartLegacyMUL.uop"); let img = if mul_path.exists() && idx_path.exists() { gumpart::decode_gump(&mul_path, &idx_path, gump_id as usize) .map_err(|e| e.to_string())? } else if uop_path.exists() { gumpart::decode_gump_uop(&uop_path, gump_id as usize) .map_err(|e| e.to_string())? } else { return Err("No gump art source found".to_string()); }; Ok(art_image_to_result(img)) } /// Parse script_gumps.xml from the "UO Gumps" folder and return all script gump entries. #[tauri::command] pub fn list_script_gumps(app: tauri::AppHandle) -> Result, String> { let gump_dir = gumpart::find_gump_dir_for_scripts(&app) .ok_or_else(|| "UO Gumps folder not found".to_string())?; let xml_path = gump_dir.join("script_gumps.xml"); if !xml_path.exists() { return Err(format!("script_gumps.xml not found — searched: {}", gump_dir.display())); } let content = std::fs::read_to_string(&xml_path) .map_err(|e| format!("Failed to read script_gumps.xml: {}", e))?; let mut entries = Vec::new(); for line in content.lines() { let trimmed = line.trim(); if !trimmed.starts_with(" = if let Some(t) = tags { t.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect() } else { Vec::new() }; entries.push(ScriptGumpEntry { class_name, file_path, tags: tags_vec }); } } Ok(entries) } fn art_image_to_result(img: ArtImage) -> ArtImageResult { ArtImageResult { width: img.width, height: img.height, pixels: img.pixels, } }