1228 lines
57 KiB
Rust
1228 lines
57 KiB
Rust
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<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);
|
|
|
|
// 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<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()),
|
|
}
|
|
}
|
|
|
|
/// 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<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 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::<i32>() {
|
|
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<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)
|
|
}
|
|
|
|
// ============================================================================
|
|
// Map Parser
|
|
// ============================================================================
|
|
|
|
pub fn parse_maps_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Map>, 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::<i32>()
|
|
.map_err(|_| XmlParseError::InvalidAttribute("music".to_string()))?;
|
|
|
|
let ambience = attrs.get("ambience")
|
|
.ok_or_else(|| XmlParseError::MissingAttribute("ambience".to_string()))?
|
|
.parse::<i32>()
|
|
.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<P: AsRef<Path>>(
|
|
path: P,
|
|
) -> Result<Vec<FastTravelLocation>, XmlParseError> {
|
|
parse_fast_travel_xml_internal(path, FastTravelType::Location)
|
|
}
|
|
|
|
/// Parse FastTravelCanoe.xml (canoe fast travel locations)
|
|
pub fn parse_fast_travel_canoe_xml<P: AsRef<Path>>(
|
|
path: P,
|
|
) -> Result<Vec<FastTravelLocation>, XmlParseError> {
|
|
parse_fast_travel_xml_internal(path, FastTravelType::Canoe)
|
|
}
|
|
|
|
/// Parse FastTravelPortals.xml (portal fast travel locations)
|
|
pub fn parse_fast_travel_portals_xml<P: AsRef<Path>>(
|
|
path: P,
|
|
) -> Result<Vec<FastTravelLocation>, XmlParseError> {
|
|
parse_fast_travel_xml_internal(path, FastTravelType::Portal)
|
|
}
|
|
|
|
/// Internal function to parse any fast travel XML file
|
|
fn parse_fast_travel_xml_internal<P: AsRef<Path>>(
|
|
path: P,
|
|
travel_type: FastTravelType,
|
|
) -> Result<Vec<FastTravelLocation>, 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::<i32>()
|
|
.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<P: AsRef<Path>>(path: P) -> Result<Vec<PlayerHouse>, 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::<i32>()
|
|
.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::<i32>()
|
|
.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<P: AsRef<Path>>(path: P) -> Result<Vec<Trait>, 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<Trait> = 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::<i32>()
|
|
.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::<i32>() {
|
|
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<P: AsRef<Path>>(path: P) -> Result<Vec<Shop>, 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<Shop> = 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::<i32>()
|
|
.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)
|
|
}
|