Files
cursebreaker-parser-rust/cursebreaker-parser/src/xml_parser.rs
2026-01-12 07:19:38 +00:00

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)
}