use crate::types::{ Item, ItemStat, AnimationSet, Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet, Quest, QuestPhase, QuestReward, Harvestable, HarvestableDrop, LootTable, LootDrop, Map, FastTravelLocation, FastTravelType, PlayerHouse, Trait, TraitTrainer, Shop, ShopItem, SkillType, Tool, }; use quick_xml::events::Event; use quick_xml::reader::Reader; use std::collections::HashMap; use std::fs::File; use std::io::BufReader; use std::path::Path; use thiserror::Error; #[derive(Debug, Error)] pub enum XmlParseError { #[error("XML parsing error: {0}")] XmlError(#[from] quick_xml::Error), #[error("IO error: {0}")] IoError(#[from] std::io::Error), #[error("Attribute error: {0}")] AttrError(#[from] quick_xml::events::attributes::AttrError), #[error("Missing required attribute: {0}")] MissingAttribute(String), #[error("Invalid attribute value: {0}")] InvalidAttribute(String), } pub fn parse_items_xml>(path: P) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut items = Vec::new(); let mut buf = Vec::new(); let mut current_item: Option = None; loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(e)) | Ok(Event::Empty(e)) => { match e.name().as_ref() { b"item" => { let attrs = parse_attributes(&e)?; // Get required attributes let id = attrs.get("id") .ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?; let name = attrs.get("name") .ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))? .clone(); let mut item = Item::new(id, name); // Note: This is a legacy simple parser. For full functionality, // use the item_loader module's load_items_from_directory function. // Parse optional attributes using defaults for new structure if let Some(v) = attrs.get("level") { item.level = v.parse().unwrap_or(1); } if let Some(v) = attrs.get("description") { item.description = v.clone(); } if let Some(v) = attrs.get("price") { item.price = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("maxstack") { item.max_stack = v.parse().unwrap_or(1); } if let Some(v) = attrs.get("abilityid") { item.ability_id = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("swap") { item.swap_item = v.parse().unwrap_or(0); } if attrs.get("twohanded").is_some() { item.two_handed = true; } if let Some(v) = attrs.get("foodamount") { item.food_amount = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("foodfrequency") { item.food_frequency = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("foodtime") { item.food_time = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("foodlevel") { item.food_level = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("handmodel") { item.handmodel = v.clone(); } if let Some(v) = attrs.get("groundmodel") { item.ground_model = v.parse().unwrap_or(item.type_id); } if let Some(v) = attrs.get("usingitemmodel") { item.using_item_model = v.clone(); } if let Some(v) = attrs.get("dropsfx") { item.drop_sfx = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("pickupsfx") { item.pickup_sfx = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("storagesize") { item.storage_size = v.parse().unwrap_or(0); } if attrs.get("hidemilestone").is_some() { item.hide_milestone = true; } if attrs.get("generateicon").is_some() { item.generate_icon = true; } if let Some(v) = attrs.get("comment") { item.comment = v.clone(); } current_item = Some(item); } b"stat" => { if let Some(ref mut item) = current_item { let attrs = parse_attributes(&e)?; let stat = parse_stat(&attrs); // Convert legacy ItemStat to new Stat vec let stats = stat.to_stats(); item.stats.extend(stats); } } b"crafting" => { // Crafting recipes are now handled by item_loader for full functionality // This is kept for backwards compatibility but doesn't fully populate the new structure } b"anim" => { if let Some(ref mut item) = current_item { let attrs = parse_attributes(&e)?; let anim = AnimationSet { idle: attrs.get("idle").and_then(|v| v.parse().ok()).unwrap_or(0), walk: attrs.get("walk").and_then(|v| v.parse().ok()).unwrap_or(0), run: attrs.get("run").and_then(|v| v.parse().ok()).unwrap_or(0), weapon_attack: attrs.get("weaponattack").and_then(|v| v.parse().ok()).unwrap_or(0), takehit: attrs.get("takehit").and_then(|v| v.parse().ok()).unwrap_or(0), use_anim: attrs.get("use").and_then(|v| v.parse().ok()).unwrap_or(0), }; item.animations = Some(anim); } } b"generate" => { // Generate rules are now handled by item_loader for full functionality // This is kept for backwards compatibility but doesn't process them } _ => {} } } Ok(Event::End(e)) => { match e.name().as_ref() { b"item" => { if let Some(item) = current_item.take() { items.push(item); } } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(items) } fn parse_attributes(element: &quick_xml::events::BytesStart) -> Result, XmlParseError> { let mut attrs = HashMap::new(); for attr in element.attributes() { let attr = attr?; let key = String::from_utf8_lossy(attr.key.as_ref()).to_string(); let value = attr.unescape_value()?.to_string(); attrs.insert(key, value); } Ok(attrs) } fn parse_stat(attrs: &HashMap) -> ItemStat { ItemStat { damagephysical: attrs.get("damagephysical").and_then(|v| v.parse().ok()), damagemagical: attrs.get("damagemagical").and_then(|v| v.parse().ok()), damageranged: attrs.get("damageranged").and_then(|v| v.parse().ok()), accuracyphysical: attrs.get("accuracyphysical").and_then(|v| v.parse().ok()), accuracymagical: attrs.get("accuracymagical").and_then(|v| v.parse().ok()), accuracyranged: attrs.get("accuracyranged").and_then(|v| v.parse().ok()), resistancephysical: attrs.get("resistancephysical").and_then(|v| v.parse().ok()), resistancemagical: attrs.get("resistancemagical").and_then(|v| v.parse().ok()), resistanceranged: attrs.get("resistanceranged").and_then(|v| v.parse().ok()), health: attrs.get("health").and_then(|v| v.parse().ok()), mana: attrs.get("mana").and_then(|v| v.parse().ok()), manaregen: attrs.get("manaregen").and_then(|v| v.parse().ok()), healing: attrs.get("healing").and_then(|v| v.parse().ok()), harvestingspeedwoodcutting: attrs.get("harvestingspeedwoodcutting").and_then(|v| v.parse().ok()), } } /// Parse health range string like "3-5" or "3" into (min, max) fn parse_health_range(health_str: &str) -> (i32, i32) { if let Some(dash_pos) = health_str.find('-') { let min_str = &health_str[..dash_pos]; let max_str = &health_str[dash_pos + 1..]; let min = min_str.trim().parse().unwrap_or(0); let max = max_str.trim().parse().unwrap_or(0); (min, max) } else { let val = health_str.trim().parse().unwrap_or(0); (val, val) } } // ============================================================================ // NPC Parser // ============================================================================ pub fn parse_npcs_xml>(path: P) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut npcs = Vec::new(); let mut buf = Vec::new(); let mut current_npc: Option = None; let mut current_bark_group: Option = None; let mut in_exitdialoguebarks = false; loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { match e.name().as_ref() { b"npc" => { let attrs = parse_attributes(e)?; let id = attrs.get("id") .ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?; let name = attrs.get("name").cloned().unwrap_or_default(); let mut npc = Npc::new(id, name); // Parse all optional attributes if let Some(v) = attrs.get("tags") { npc.tags = Some(v.clone()); } if let Some(v) = attrs.get("level") { npc.level = v.parse().ok(); } if let Some(v) = attrs.get("description") { npc.description = Some(v.clone()); } if let Some(v) = attrs.get("comment") { npc.comment = Some(v.clone()); } if let Some(v) = attrs.get("model") { npc.model = Some(v.clone()); } if let Some(v) = attrs.get("canfight") { npc.canfight = v.parse().ok(); } if let Some(v) = attrs.get("aggressive") { npc.aggressive = v.parse().ok(); } if let Some(v) = attrs.get("team") { npc.team = v.parse().ok(); } if let Some(v) = attrs.get("aggrodistance") { npc.aggrodistance = v.parse().ok(); } if let Some(v) = attrs.get("respawntime") { npc.respawntime = v.parse().ok(); } if let Some(v) = attrs.get("health") { npc.health = v.parse().ok(); } if let Some(v) = attrs.get("mana") { npc.mana = v.parse().ok(); } if let Some(v) = attrs.get("accuracy") { npc.accuracy = v.parse().ok(); } if let Some(v) = attrs.get("damagetype") { npc.damagetype = v.parse().ok(); } if let Some(v) = attrs.get("damageblock") { npc.damageblock = v.parse().ok(); } if let Some(v) = attrs.get("ability") { npc.ability = v.parse().ok(); } if let Some(v) = attrs.get("attackdistance") { npc.attackdistance = v.parse().ok(); } if let Some(v) = attrs.get("attackspeed") { npc.attackspeed = v.parse().ok(); } if let Some(v) = attrs.get("attackdelay") { npc.attackdelay = v.parse().ok(); } if let Some(v) = attrs.get("gfxattack") { npc.gfxattack = Some(v.clone()); } if let Some(v) = attrs.get("projectile") { npc.projectile = v.parse().ok(); } if let Some(v) = attrs.get("projectilerate") { npc.projectilerate = v.parse().ok(); } if let Some(v) = attrs.get("projectileendgfx") { npc.projectileendgfx = Some(v.clone()); } if let Some(v) = attrs.get("projectileattackdistance") { npc.projectileattackdistance = v.parse().ok(); } if let Some(v) = attrs.get("movementspeed") { npc.movementspeed = v.parse().ok(); } if let Some(v) = attrs.get("walkspeed") { npc.walkspeed = v.parse().ok(); } if let Some(v) = attrs.get("wandering") { npc.wandering = v.parse().ok(); } if let Some(v) = attrs.get("wanderingdistance") { npc.wanderingdistance = v.parse().ok(); } if let Some(v) = attrs.get("aibehaviour") { npc.aibehaviour = v.parse().ok(); } if let Some(v) = attrs.get("nobestiary") { npc.nobestiary = v.parse().ok(); } if let Some(v) = attrs.get("interactable") { npc.interactable = v.parse().ok(); } if let Some(v) = attrs.get("interactdistance") { npc.interactdistance = v.parse().ok(); } if let Some(v) = attrs.get("dontrotateoninteract") { npc.dontrotateoninteract = v.parse().ok(); } if let Some(v) = attrs.get("shop") { npc.shop = v.parse().ok(); } if let Some(v) = attrs.get("sfxattack") { npc.sfxattack = Some(v.clone()); } if let Some(v) = attrs.get("sfxdeath") { npc.sfxdeath = Some(v.clone()); } if let Some(v) = attrs.get("sfxtakehit") { npc.sfxtakehit = Some(v.clone()); } if let Some(v) = attrs.get("sfxidle") { npc.sfxidle = Some(v.clone()); } if let Some(v) = attrs.get("idlesoundtext") { npc.idlesoundtext = Some(v.clone()); } if let Some(v) = attrs.get("anim_attack") { npc.anim_attack = Some(v.clone()); } if let Some(v) = attrs.get("anim_death") { npc.anim_death = Some(v.clone()); } if let Some(v) = attrs.get("anim_idle") { npc.anim_idle = Some(v.clone()); } if let Some(v) = attrs.get("anim_run") { npc.anim_run = Some(v.clone()); } if let Some(v) = attrs.get("anim_walk") { npc.anim_walk = Some(v.clone()); } if let Some(v) = attrs.get("anim_takehit") { npc.anim_takehit = Some(v.clone()); } if let Some(v) = attrs.get("startanim") { npc.startanim = Some(v.clone()); } current_npc = Some(npc); } b"stat" if current_npc.is_some() => { if let Some(ref mut npc) = current_npc { let attrs = parse_attributes(e)?; let stat = parse_npc_stat(&attrs); npc.stats.push(stat); } } b"level" if current_npc.is_some() => { if let Some(ref mut npc) = current_npc { let attrs = parse_attributes(e)?; let level = parse_npc_level(&attrs); npc.levels.push(level); } } b"rightclick" if current_npc.is_some() => { if let Some(ref mut npc) = current_npc { let attrs = parse_attributes(e)?; if let Some(option) = attrs.get("option") { npc.rightclick = Some(RightClick { option: option.clone() }); } } } b"questmarker" if current_npc.is_some() => { if let Some(ref mut npc) = current_npc { let attrs = parse_attributes(e)?; if let (Some(id), Some(phase)) = (attrs.get("id"), attrs.get("phase")) { if let (Ok(id), Ok(phase)) = (id.parse::(), phase.parse::()) { npc.questmarkers.push(QuestMarker { id, phase, checks: attrs.get("checks").cloned(), }); } } } } b"barks" if current_npc.is_some() => { let attrs = parse_attributes(e)?; current_bark_group = Some(BarkGroup { cooldown: attrs.get("cooldown").and_then(|v| v.parse().ok()), rate: attrs.get("rate").and_then(|v| v.parse().ok()), range: attrs.get("range").and_then(|v| v.parse().ok()), checks: attrs.get("checks").cloned(), npcs: attrs.get("npcs").cloned(), barks: Vec::new(), }); } b"exitdialoguebarks" if current_npc.is_some() => { in_exitdialoguebarks = true; current_bark_group = Some(BarkGroup { cooldown: None, rate: None, range: None, checks: None, npcs: None, barks: Vec::new(), }); } b"anim" if current_npc.is_some() => { if let Some(ref mut npc) = current_npc { let attrs = parse_attributes(e)?; npc.animations = Some(NpcAnimationSet { idle: attrs.get("idle").cloned(), walk: attrs.get("walk").cloned(), run: attrs.get("run").cloned(), attack: attrs.get("attack").cloned(), death: attrs.get("death").cloned(), talk: attrs.get("talk").cloned(), }); } } _ => {} } } Ok(Event::Text(e)) => { // Handle bark text content if current_bark_group.is_some() { let text = e.unescape()?.to_string().trim().to_string(); if !text.is_empty() { // Text will be added to bark when we hit the end tag } } } Ok(Event::End(ref e)) => { match e.name().as_ref() { b"npc" => { if let Some(npc) = current_npc.take() { npcs.push(npc); } } b"barks" => { if let Some(bark_group) = current_bark_group.take() { if let Some(ref mut npc) = current_npc { if in_exitdialoguebarks { npc.exitdialoguebarks.push(bark_group); } else { npc.barks.push(bark_group); } } } } b"exitdialoguebarks" => { in_exitdialoguebarks = false; if let Some(bark_group) = current_bark_group.take() { if let Some(ref mut npc) = current_npc { npc.exitdialoguebarks.push(bark_group); } } } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(npcs) } fn parse_npc_stat(attrs: &HashMap) -> NpcStat { NpcStat { damagephysical: attrs.get("damagephysical").and_then(|v| v.parse().ok()), damagemagical: attrs.get("damagemagical").and_then(|v| v.parse().ok()), damageranged: attrs.get("damageranged").and_then(|v| v.parse().ok()), accuracyphysical: attrs.get("accuracyphysical").and_then(|v| v.parse().ok()), accuracymagical: attrs.get("accuracymagical").and_then(|v| v.parse().ok()), accuracyranged: attrs.get("accuracyranged").and_then(|v| v.parse().ok()), resistancephysical: attrs.get("resistancephysical").or_else(|| attrs.get("resistancePhysical")).and_then(|v| v.parse().ok()), resistancemagical: attrs.get("resistancemagical").or_else(|| attrs.get("resistanceMagical")).and_then(|v| v.parse().ok()), resistanceranged: attrs.get("resistanceranged").or_else(|| attrs.get("resistanceRanged")).and_then(|v| v.parse().ok()), health: attrs.get("health").and_then(|v| v.parse().ok()), mana: attrs.get("mana").and_then(|v| v.parse().ok()), manaregen: attrs.get("manaregen").and_then(|v| v.parse().ok()), healing: attrs.get("healing").and_then(|v| v.parse().ok()), } } fn parse_npc_level(attrs: &HashMap) -> NpcLevel { NpcLevel { swordsmanship: attrs.get("swordsmanship").and_then(|v| v.parse().ok()), archery: attrs.get("archery").and_then(|v| v.parse().ok()), magic: attrs.get("magic").and_then(|v| v.parse().ok()), defence: attrs.get("defence").and_then(|v| v.parse().ok()), mining: attrs.get("mining").and_then(|v| v.parse().ok()), woodcutting: attrs.get("woodcutting").and_then(|v| v.parse().ok()), fishing: attrs.get("fishing").and_then(|v| v.parse().ok()), cooking: attrs.get("cooking").and_then(|v| v.parse().ok()), carpentry: attrs.get("carpentry").and_then(|v| v.parse().ok()), blacksmithy: attrs.get("blacksmithy").and_then(|v| v.parse().ok()), tailoring: attrs.get("tailoring").and_then(|v| v.parse().ok()), alchemy: attrs.get("alchemy").and_then(|v| v.parse().ok()), } } // ============================================================================ // Quest Parser // ============================================================================ pub fn parse_quests_xml>(path: P) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut quests = Vec::new(); let mut buf = Vec::new(); let mut current_quest: Option = None; let mut in_rewards = false; loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { match e.name().as_ref() { b"quest" => { let attrs = parse_attributes(e)?; let id = attrs.get("id") .ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?; let name = attrs.get("name") .ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))? .clone(); let mut quest = Quest::new(id, name); // Parse optional attributes if let Some(v) = attrs.get("mainquest") { quest.mainquest = v.parse().ok(); } if let Some(v) = attrs.get("hidden") { quest.hidden = v.parse().ok(); } if let Some(v) = attrs.get("questdescription") { quest.questdescription = Some(v.clone()); } if let Some(v) = attrs.get("completiontext") { quest.completiontext = Some(v.clone()); } if let Some(v) = attrs.get("dontshowcompletionscreen") { quest.dontshowcompletionscreen = v.parse().ok(); } if let Some(v) = attrs.get("comment") { quest.comment = Some(v.clone()); } current_quest = Some(quest); } b"phase" if current_quest.is_some() => { if let Some(ref mut quest) = current_quest { let attrs = parse_attributes(e)?; if let Some(id) = attrs.get("id") { if let Ok(id) = id.parse::() { quest.phases.push(QuestPhase { id, trackerdescription: attrs.get("trackerdescription").cloned(), description: attrs.get("description").cloned(), helperarrownpc: attrs.get("helperarrownpc").cloned(), helperarrowpos: attrs.get("helperarrowpos").cloned(), checks: attrs.get("checks").cloned(), }); } } } } b"rewards" if current_quest.is_some() => { in_rewards = true; } b"reward" if current_quest.is_some() && in_rewards => { if let Some(ref mut quest) = current_quest { let attrs = parse_attributes(e)?; quest.rewards.push(QuestReward { item: attrs.get("item").and_then(|v| v.parse().ok()), skill: attrs.get("skill").cloned(), amount: attrs.get("amount").and_then(|v| v.parse().ok()), xp: attrs.get("xp").and_then(|v| v.parse().ok()), checks: attrs.get("checks").cloned(), comment: attrs.get("comment").cloned(), }); } } _ => {} } } Ok(Event::End(ref e)) => { match e.name().as_ref() { b"quest" => { if let Some(quest) = current_quest.take() { quests.push(quest); } } b"rewards" => { in_rewards = false; } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(quests) } // ============================================================================ // Harvestable Parser // ============================================================================ pub fn parse_harvestables_xml>(path: P) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut harvestables = Vec::new(); let mut buf = Vec::new(); let mut current_harvestable: Option = None; loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { match e.name().as_ref() { b"harvestable" => { let attrs = parse_attributes(e)?; let typeid = attrs.get("typeid") .ok_or_else(|| XmlParseError::MissingAttribute("typeid".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("typeid".to_string()))?; let name = attrs.get("name") .ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))? .clone(); let mut harvestable = Harvestable::new(typeid, name); // Parse optional attributes with defaults if let Some(v) = attrs.get("actionname") { harvestable.actionname = v.clone(); } if let Some(v) = attrs.get("desc") { harvestable.desc = v.clone(); } if let Some(v) = attrs.get("comment") { harvestable.comment = v.clone(); } if let Some(v) = attrs.get("level") { harvestable.level = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("skill") { harvestable.skill = v.parse().unwrap_or(SkillType::None); } if let Some(v) = attrs.get("tool") { harvestable.tool = v.parse().unwrap_or(Tool::None); } if let Some(v) = attrs.get("health") { let (min, max) = parse_health_range(v); harvestable.min_health = min; harvestable.max_health = max; } if let Some(v) = attrs.get("harvesttime") { harvestable.harvesttime = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("hittime") { harvestable.hittime = v.parse().unwrap_or(0); } if let Some(v) = attrs.get("respawntime") { harvestable.respawntime = v.parse().unwrap_or(0); } // Audio (handle both cases: harvestSfx and harvestsfx) if let Some(v) = attrs.get("harvestSfx").or_else(|| attrs.get("harvestsfx")) { harvestable.harvestsfx = v.clone(); } if let Some(v) = attrs.get("endSfx").or_else(|| attrs.get("endsfx")) { harvestable.endsfx = v.clone(); } if let Some(v) = attrs.get("receiveItemSfx").or_else(|| attrs.get("receiveitemsfx")) { harvestable.receiveitemsfx = v.clone(); } if let Some(v) = attrs.get("animation") { harvestable.animation = v.clone(); } if let Some(v) = attrs.get("takehitanimation") { harvestable.takehitanimation = v.clone(); } if let Some(v) = attrs.get("endgfx") { harvestable.endgfx = v.clone(); } if let Some(v) = attrs.get("tree") { harvestable.tree = v.parse().unwrap_or(0) == 1; } if let Some(v) = attrs.get("hidemilestone") { harvestable.hidemilestone = v.parse().unwrap_or(0) == 1; } if let Some(v) = attrs.get("nohighlight") { harvestable.nohighlight = v.parse().unwrap_or(0) == 1; } // Handle both cases: hideMinimap and hideminimap if let Some(v) = attrs.get("hideMinimap").or_else(|| attrs.get("hideminimap")) { harvestable.hideminimap = v.parse().unwrap_or(0) == 1; } if let Some(v) = attrs.get("noLeftClickInteract").or_else(|| attrs.get("noleftclickinteract")) { harvestable.noleftclickinteract = v.parse().unwrap_or(0) == 1; } if let Some(v) = attrs.get("interactDistance").or_else(|| attrs.get("interactdistance")) { harvestable.interactdistance = v.clone(); } current_harvestable = Some(harvestable); } b"item" if current_harvestable.is_some() => { if let Some(ref mut harvestable) = current_harvestable { let attrs = parse_attributes(e)?; if let Some(id_str) = attrs.get("id") { if let Ok(id) = id_str.parse::() { let drop = HarvestableDrop { id, minamount: attrs.get("minamount").and_then(|v| v.parse().ok()).unwrap_or(0), maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()).unwrap_or(0), droprate: attrs.get("droprate").and_then(|v| v.parse().ok()).unwrap_or(0), droprateboost: attrs.get("droprateboost").and_then(|v| v.parse().ok()).unwrap_or(0), amountboost: attrs.get("amountboost").and_then(|v| v.parse().ok()).unwrap_or(0), checks: attrs.get("checks").cloned().unwrap_or_default(), comment: attrs.get("comment").cloned().unwrap_or_default(), dontconsumehealth: attrs.get("dontconsumehealth").and_then(|v| v.parse().ok()).unwrap_or(0) == 1, }; harvestable.drops.push(drop); } } } } _ => {} } } Ok(Event::End(ref e)) => { match e.name().as_ref() { b"harvestable" => { if let Some(harvestable) = current_harvestable.take() { harvestables.push(harvestable); } } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(harvestables) } // ============================================================================ // Loot Parser // ============================================================================ pub fn parse_loot_xml>(path: P) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut loot_tables = Vec::new(); let mut buf = Vec::new(); let mut current_table: Option = None; loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { match e.name().as_ref() { b"table" => { let attrs = parse_attributes(e)?; // Parse npcid - can be comma-separated like "45,459" let npc_ids = if let Some(npcid_str) = attrs.get("npcid") { npcid_str .split(',') .filter_map(|s| s.trim().parse::().ok()) .collect::>() } else { Vec::new() }; let mut table = LootTable::new(npc_ids); // Parse optional name if let Some(v) = attrs.get("name") { table.name = Some(v.clone()); } current_table = Some(table); } b"drop" if current_table.is_some() => { if let Some(ref mut table) = current_table { let attrs = parse_attributes(e)?; // Parse item ID (required for a drop) if let Some(item_str) = attrs.get("item") { if let Ok(item) = item_str.parse::() { let drop = LootDrop { item, rate: attrs.get("rate").and_then(|v| v.parse().ok()), minamount: attrs.get("minamount").and_then(|v| v.parse().ok()), maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()), checks: attrs.get("checks").cloned(), comment: attrs.get("comment").cloned(), }; table.drops.push(drop); } } } } _ => {} } } Ok(Event::End(ref e)) => { match e.name().as_ref() { b"table" => { if let Some(table) = current_table.take() { loot_tables.push(table); } } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(loot_tables) } // ============================================================================ // Map Parser // ============================================================================ pub fn parse_maps_xml>(path: P) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut maps = Vec::new(); let mut buf = Vec::new(); loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { match e.name().as_ref() { b"map" => { let attrs = parse_attributes(e)?; // Get required attributes let scene_id = attrs.get("sceneid") .ok_or_else(|| XmlParseError::MissingAttribute("sceneid".to_string()))? .clone(); let music = attrs.get("music") .ok_or_else(|| XmlParseError::MissingAttribute("music".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("music".to_string()))?; let ambience = attrs.get("ambience") .ok_or_else(|| XmlParseError::MissingAttribute("ambience".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("ambience".to_string()))?; let mut map = Map::new(scene_id, music, ambience); // Parse optional attributes if let Some(v) = attrs.get("name") { map.name = v.clone(); } if let Some(v) = attrs.get("fogcolor") { map.fog_color = Some(v.clone()); } if let Some(v) = attrs.get("fogginess") { map.fogginess = v.parse().ok(); } if let Some(v) = attrs.get("viewdistance") { map.view_distance = v.parse().ok(); } if let Some(v) = attrs.get("npcviewdistance") { map.npc_view_distance = v.parse().ok(); } if let Some(v) = attrs.get("sunlight") { map.sunlight = v.parse().ok(); } if let Some(v) = attrs.get("suncolor") { map.sun_color = Some(v.clone()); } if let Some(v) = attrs.get("ambientcolor") { map.ambient_color = Some(v.clone()); } if let Some(v) = attrs.get("indoorsunlight") { map.indoor_sunlight = v.parse().ok(); } if let Some(v) = attrs.get("fogstart") { map.fog_start = v.parse().ok(); } if attrs.get("indoors").is_some() { map.indoors = true; } if attrs.get("noworldmap").is_some() { map.no_world_map = true; } if attrs.get("nominimap").is_some() { map.no_minimap = true; } if attrs.get("tpdisabled").is_some() { map.tp_disabled = true; } if attrs.get("dontloadnearbyscenes").is_some() { map.dont_load_nearby_scenes = true; } if attrs.get("noborder").is_some() { map.no_border = true; } if attrs.get("borderleft").is_some() { map.border_left = true; } if attrs.get("borderright").is_some() { map.border_right = true; } if attrs.get("borderup").is_some() { map.border_up = true; } if attrs.get("borderdown").is_some() { map.border_down = true; } if let Some(v) = attrs.get("respawnmap") { map.respawn_map = Some(v.clone()); } if let Some(v) = attrs.get("connectedmaps") { map.connected_maps = Some(v.clone()); } if let Some(v) = attrs.get("comment") { map.comment = Some(v.clone()); } maps.push(map); } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(maps) } // ============================================================================ // Fast Travel Parser // ============================================================================ /// Parse FastTravelLocations.xml (regular fast travel locations) pub fn parse_fast_travel_locations_xml>( path: P, ) -> Result, XmlParseError> { parse_fast_travel_xml_internal(path, FastTravelType::Location) } /// Parse FastTravelCanoe.xml (canoe fast travel locations) pub fn parse_fast_travel_canoe_xml>( path: P, ) -> Result, XmlParseError> { parse_fast_travel_xml_internal(path, FastTravelType::Canoe) } /// Parse FastTravelPortals.xml (portal fast travel locations) pub fn parse_fast_travel_portals_xml>( path: P, ) -> Result, XmlParseError> { parse_fast_travel_xml_internal(path, FastTravelType::Portal) } /// Internal function to parse any fast travel XML file fn parse_fast_travel_xml_internal>( path: P, travel_type: FastTravelType, ) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut locations = Vec::new(); let mut buf = Vec::new(); loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { match e.name().as_ref() { b"location" => { let attrs = parse_attributes(e)?; // Get required attributes let id = attrs .get("id") .ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?; let name = attrs .get("name") .ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))? .clone(); let position = attrs .get("pos") .ok_or_else(|| XmlParseError::MissingAttribute("pos".to_string()))? .clone(); let mut location = FastTravelLocation::new(id, name, position, travel_type); // Parse optional attributes based on type match travel_type { FastTravelType::Location => { // Regular locations have unlocked and connections if attrs.get("unlocked").is_some() { location.unlocked = true; } if let Some(v) = attrs.get("connections") { location.connections = Some(v.clone()); } } FastTravelType::Canoe => { // Canoe locations have checks if let Some(v) = attrs.get("checks") { location.checks = Some(v.clone()); } } FastTravelType::Portal => { // Portals have no additional fields } } locations.push(location); } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(locations) } // ============================================================================ // Player House Parser // ============================================================================ pub fn parse_player_houses_xml>(path: P) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut houses = Vec::new(); let mut buf = Vec::new(); loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { match e.name().as_ref() { b"playerhouse" => { let attrs = parse_attributes(e)?; // Get required attributes let id = attrs .get("id") .ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?; let name = attrs .get("name") .ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))? .clone(); let description = attrs .get("description") .unwrap_or(&String::new()) .clone(); let position = attrs .get("pos") .ok_or_else(|| XmlParseError::MissingAttribute("pos".to_string()))? .clone(); let price = attrs .get("price") .ok_or_else(|| XmlParseError::MissingAttribute("price".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("price".to_string()))?; let mut house = PlayerHouse::new(id, name, description, position, price); // Parse optional attributes if attrs.get("hidden").is_some() { house.hidden = true; } houses.push(house); } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(houses) } // ============================================================================ // Trait Parser // ============================================================================ pub fn parse_traits_xml>(path: P) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut traits = Vec::new(); let mut buf = Vec::new(); let mut current_trait: Option = None; loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { match e.name().as_ref() { b"trait" => { let attrs = parse_attributes(e)?; // Get required attributes let id = attrs .get("id") .ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?; let name = attrs .get("name") .unwrap_or(&String::new()) .clone(); let description = attrs .get("description") .unwrap_or(&String::new()) .clone(); let mut trait_obj = Trait::new(id, name, description); // Parse optional attributes if let Some(v) = attrs.get("learnability") { trait_obj.learnability = v.parse().ok(); } if let Some(v) = attrs.get("comment") { trait_obj.comment = Some(v.clone()); } current_trait = Some(trait_obj); } b"trainer" if current_trait.is_some() => { if let Some(ref mut trait_obj) = current_trait { let attrs = parse_attributes(e)?; // Parse trainer requirements if let (Some(skill), Some(level_str)) = (attrs.get("skill"), attrs.get("level")) { if let Ok(level) = level_str.parse::() { let mut trainer = TraitTrainer::new(skill.clone(), level); // Parse optional tier icon if let Some(v) = attrs.get("tiericon") { trainer.tier_icon = v.parse().ok(); } trait_obj.trainer = Some(trainer); } } } } _ => {} } } Ok(Event::End(ref e)) => { match e.name().as_ref() { b"trait" => { if let Some(trait_obj) = current_trait.take() { traits.push(trait_obj); } } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(traits) } // ============================================================================ // Shop Parser // ============================================================================ pub fn parse_shops_xml>(path: P) -> Result, XmlParseError> { let file = File::open(path)?; let buf_reader = BufReader::new(file); let mut reader = Reader::from_reader(buf_reader); reader.config_mut().trim_text(true); let mut shops = Vec::new(); let mut buf = Vec::new(); let mut current_shop: Option = None; loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { match e.name().as_ref() { b"shop" => { let attrs = parse_attributes(e)?; // Get required attributes let shop_id = attrs .get("shopid") .ok_or_else(|| XmlParseError::MissingAttribute("shopid".to_string()))? .parse::() .map_err(|_| XmlParseError::InvalidAttribute("shopid".to_string()))?; let name = attrs .get("name") .unwrap_or(&String::new()) .clone(); let mut shop = Shop::new(shop_id, name); // Parse optional attributes if attrs.get("isgeneralstore").is_some() { shop.is_general_store = true; } if let Some(v) = attrs.get("comment") { shop.comment = Some(v.clone()); } current_shop = Some(shop); } b"item" if current_shop.is_some() => { if let Some(ref mut shop) = current_shop { let attrs = parse_attributes(e)?; // Get item ID (can be numeric or string) if let Some(item_id) = attrs.get("id") { let mut item = ShopItem::new(item_id.clone()); // Parse optional attributes if let Some(v) = attrs.get("name") { item.name = Some(v.clone()); } if let Some(v) = attrs.get("price") { item.price = v.parse().ok(); } if let Some(v) = attrs.get("maxstock") { item.max_stock = v.parse().ok(); } if let Some(v) = attrs.get("restocktime") { item.restock_time = v.parse().ok(); } if let Some(v) = attrs.get("buyprice") { item.buy_price = v.parse().ok(); } if let Some(v) = attrs.get("comment") { item.comment = Some(v.clone()); } shop.add_item(item); } } } _ => {} } } Ok(Event::End(ref e)) => { match e.name().as_ref() { b"shop" => { if let Some(shop) = current_shop.take() { shops.push(shop); } } _ => {} } } Ok(Event::Eof) => break, Err(e) => return Err(XmlParseError::XmlError(e)), _ => {} } buf.clear(); } Ok(shops) }