Files
cursebreaker-parser-rust/cursebreaker-parser/src/xml_parser.rs
2026-01-07 10:40:29 +00:00

736 lines
38 KiB
Rust

use crate::types::{
Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule,
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet,
Quest, QuestPhase, QuestReward,
Harvestable, HarvestableDrop,
LootTable, LootDrop,
};
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<P: AsRef<Path>>(path: P) -> Result<Vec<Item>, 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<Item> = 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::<i32>()
.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);
// Parse optional attributes
if let Some(v) = attrs.get("level") { item.level = v.parse().ok(); }
if let Some(v) = attrs.get("description") { item.description = Some(v.clone()); }
if let Some(v) = attrs.get("price") { item.price = v.parse().ok(); }
if let Some(v) = attrs.get("slot") { item.slot = Some(v.clone()); }
if let Some(v) = attrs.get("category") { item.category = Some(v.clone()); }
if let Some(v) = attrs.get("skill") { item.skill = Some(v.clone()); }
if let Some(v) = attrs.get("tool") { item.tool = Some(v.clone()); }
if let Some(v) = attrs.get("stackable") { item.stackable = v.parse().ok(); }
if let Some(v) = attrs.get("maxstack") { item.maxstack = v.parse().ok(); }
if let Some(v) = attrs.get("abilityid") { item.abilityid = v.parse().ok(); }
if let Some(v) = attrs.get("swap") { item.swap = v.parse().ok(); }
if let Some(v) = attrs.get("twohanded") { item.twohanded = v.parse().ok(); }
if let Some(v) = attrs.get("foodamount") { item.foodamount = v.parse().ok(); }
if let Some(v) = attrs.get("foodfrequency") { item.foodfrequency = v.parse().ok(); }
if let Some(v) = attrs.get("foodtime") { item.foodtime = v.parse().ok(); }
if let Some(v) = attrs.get("foodlevel") { item.foodlevel = v.parse().ok(); }
if let Some(v) = attrs.get("craftingskill") { item.craftingskill = Some(v.clone()); }
if let Some(v) = attrs.get("workbench") { item.workbench = v.parse().ok(); }
if let Some(v) = attrs.get("craftingitems") { item.craftingitems = Some(v.clone()); }
if let Some(v) = attrs.get("handmodel") { item.handmodel = Some(v.clone()); }
if let Some(v) = attrs.get("groundmodel") { item.groundmodel = Some(v.clone()); }
if let Some(v) = attrs.get("usingitemmodel") { item.usingitemmodel = Some(v.clone()); }
if let Some(v) = attrs.get("dropsfx") { item.dropsfx = Some(v.clone()); }
if let Some(v) = attrs.get("pickupsfx") { item.pickupsfx = Some(v.clone()); }
if let Some(v) = attrs.get("hitgfx") { item.hitgfx = Some(v.clone()); }
if let Some(v) = attrs.get("attackanimations") { item.attackanimations = Some(v.clone()); }
if let Some(v) = attrs.get("attackanimationspeed") { item.attackanimationspeed = Some(v.clone()); }
if let Some(v) = attrs.get("attackhitsounds") { item.attackhitsounds = Some(v.clone()); }
if let Some(v) = attrs.get("storageitem") { item.storageitem = Some(v.clone()); }
if let Some(v) = attrs.get("storagesize") { item.storagesize = v.parse().ok(); }
if let Some(v) = attrs.get("hidemilestone") { item.hidemilestone = v.parse().ok(); }
if let Some(v) = attrs.get("generateicon") { item.generateicon = v.parse().ok(); }
if let Some(v) = attrs.get("comment") { item.comment = Some(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);
item.stats.push(stat);
}
}
b"crafting" => {
if let Some(ref mut item) = current_item {
let attrs = parse_attributes(&e)?;
let recipe = CraftingRecipe {
workbench: attrs.get("workbench").and_then(|v| v.parse().ok()),
craftingitems: attrs.get("craftingitems").cloned(),
craftingskill: attrs.get("craftingskill").cloned(),
checks: attrs.get("checks").cloned(),
};
item.crafting_recipes.push(recipe);
}
}
b"anim" => {
if let Some(ref mut item) = current_item {
let attrs = parse_attributes(&e)?;
let anim = AnimationSet {
idle: attrs.get("idle").cloned(),
walk: attrs.get("walk").cloned(),
run: attrs.get("run").cloned(),
weaponattack: attrs.get("weaponattack").cloned(),
takehit: attrs.get("takehit").cloned(),
};
item.animations = Some(anim);
}
}
b"generate" => {
if let Some(ref mut item) = current_item {
let attrs = parse_attributes(&e)?;
let rule = GenerateRule {
generatestats: attrs.get("generatestats").cloned(),
generatecrafting: attrs.get("generatecrafting").and_then(|v| v.parse().ok()),
generateicon: attrs.get("generateicon").and_then(|v| v.parse().ok()),
};
item.generate_rules.push(rule);
}
}
_ => {}
}
}
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<HashMap<String, String>, 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<String, String>) -> 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()),
}
}
// ============================================================================
// NPC Parser
// ============================================================================
pub fn parse_npcs_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Npc>, 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<Npc> = None;
let mut current_bark_group: Option<BarkGroup> = 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::<i32>()
.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::<i32>(), phase.parse::<i32>()) {
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<String, String>) -> 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<String, String>) -> 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<P: AsRef<Path>>(path: P) -> Result<Vec<Quest>, 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<Quest> = 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::<i32>()
.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::<i32>() {
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<P: AsRef<Path>>(path: P) -> Result<Vec<Harvestable>, 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<Harvestable> = 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::<i32>()
.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
if let Some(v) = attrs.get("actionname") { harvestable.actionname = Some(v.clone()); }
if let Some(v) = attrs.get("desc") { harvestable.desc = Some(v.clone()); }
if let Some(v) = attrs.get("comment") { harvestable.comment = Some(v.clone()); }
if let Some(v) = attrs.get("level") { harvestable.level = v.parse().ok(); }
if let Some(v) = attrs.get("skill") { harvestable.skill = Some(v.clone()); }
if let Some(v) = attrs.get("tool") { harvestable.tool = Some(v.clone()); }
if let Some(v) = attrs.get("health") { harvestable.health = Some(v.clone()); }
if let Some(v) = attrs.get("harvesttime") { harvestable.harvesttime = v.parse().ok(); }
if let Some(v) = attrs.get("hittime") { harvestable.hittime = v.parse().ok(); }
if let Some(v) = attrs.get("respawntime") { harvestable.respawntime = v.parse().ok(); }
// Audio (handle both cases: harvestSfx and harvestsfx)
if let Some(v) = attrs.get("harvestSfx").or_else(|| attrs.get("harvestsfx")) {
harvestable.harvestsfx = Some(v.clone());
}
if let Some(v) = attrs.get("endSfx").or_else(|| attrs.get("endsfx")) {
harvestable.endsfx = Some(v.clone());
}
if let Some(v) = attrs.get("receiveItemSfx").or_else(|| attrs.get("receiveitemsfx")) {
harvestable.receiveitemsfx = Some(v.clone());
}
if let Some(v) = attrs.get("animation") { harvestable.animation = Some(v.clone()); }
if let Some(v) = attrs.get("takehitanimation") { harvestable.takehitanimation = Some(v.clone()); }
if let Some(v) = attrs.get("endgfx") { harvestable.endgfx = Some(v.clone()); }
if let Some(v) = attrs.get("tree") { harvestable.tree = v.parse().ok(); }
if let Some(v) = attrs.get("hidemilestone") { harvestable.hidemilestone = v.parse().ok(); }
if let Some(v) = attrs.get("nohighlight") { harvestable.nohighlight = v.parse().ok(); }
// Handle both cases: hideMinimap and hideminimap
if let Some(v) = attrs.get("hideMinimap").or_else(|| attrs.get("hideminimap")) {
harvestable.hideminimap = v.parse().ok();
}
if let Some(v) = attrs.get("noLeftClickInteract").or_else(|| attrs.get("noleftclickinteract")) {
harvestable.noleftclickinteract = v.parse().ok();
}
if let Some(v) = attrs.get("interactDistance").or_else(|| attrs.get("interactdistance")) {
harvestable.interactdistance = Some(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::<i32>() {
let drop = HarvestableDrop {
id,
minamount: attrs.get("minamount").and_then(|v| v.parse().ok()),
maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()),
droprate: attrs.get("droprate").and_then(|v| v.parse().ok()),
droprateboost: attrs.get("droprateboost").and_then(|v| v.parse().ok()),
amountboost: attrs.get("amountboost").and_then(|v| v.parse().ok()),
checks: attrs.get("checks").cloned(),
comment: attrs.get("comment").cloned(),
dontconsumehealth: attrs.get("dontconsumehealth").and_then(|v| v.parse().ok()),
};
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<P: AsRef<Path>>(path: P) -> Result<Vec<LootTable>, 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<LootTable> = 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::<i32>().ok())
.collect::<Vec<i32>>()
} 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::<i32>() {
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)
}