736 lines
38 KiB
Rust
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)
|
|
}
|