From 185d324efe0a959f8d7cc5161c7ae7fea1185cb1 Mon Sep 17 00:00:00 2001 From: Connor Date: Wed, 7 Jan 2026 10:17:30 +0000 Subject: [PATCH] harvestables --- cursebreaker-parser/XML_PARSING.md | 67 ++++++++- .../examples/game_data_demo.rs | 72 ++++++++- .../src/harvestable_database.rs | 142 ++++++++++++++++++ cursebreaker-parser/src/lib.rs | 3 + cursebreaker-parser/src/main.rs | 12 +- cursebreaker-parser/src/types/harvestable.rs | 112 ++++++++++++++ cursebreaker-parser/src/types/mod.rs | 2 + cursebreaker-parser/src/xml_parser.rs | 119 +++++++++++++++ 8 files changed, 521 insertions(+), 8 deletions(-) create mode 100644 cursebreaker-parser/src/harvestable_database.rs create mode 100644 cursebreaker-parser/src/types/harvestable.rs diff --git a/cursebreaker-parser/XML_PARSING.md b/cursebreaker-parser/XML_PARSING.md index 46981b9..666e4bb 100644 --- a/cursebreaker-parser/XML_PARSING.md +++ b/cursebreaker-parser/XML_PARSING.md @@ -8,7 +8,7 @@ The parser now supports loading game data from Cursebreaker's XML files and stor ## Features -- ✅ Parse Items, NPCs, and Quests XML files with full attribute and nested element support +- ✅ Parse Items, NPCs, Quests, and Harvestables XML files with full attribute and nested element support - ✅ In-memory databases with fast lookups by ID, name, and various filters - ✅ JSON serialization for SQL database storage - ✅ Type-safe data structures with serde support @@ -131,7 +131,7 @@ Run the demos to see all features in action: # Items only cargo run --example item_database_demo -# All game data (Items, NPCs, Quests) +# All game data (Items, NPCs, Quests, Harvestables) cargo run --example game_data_demo ``` @@ -193,6 +193,40 @@ let side_quests = quest_db.get_side_quests(); let hidden = quest_db.get_hidden_quests(); ``` +## Loading Harvestables + +```rust +use cursebreaker_parser::HarvestableDatabase; + +let harvestable_db = HarvestableDatabase::load_from_xml("Data/XMLs/Harvestables/HarvestableInfo.xml")?; +println!("Loaded {} harvestables", harvestable_db.len()); +``` + +### Querying Harvestables + +```rust +// Get by type ID +if let Some(harvestable) = harvestable_db.get_by_typeid(1) { + println!("Found: {}", harvestable.name); +} + +// Get by skill +let woodcutting = harvestable_db.get_by_skill("Woodcutting"); +let mining = harvestable_db.get_by_skill("mining"); +let fishing = harvestable_db.get_by_skill("Fishing"); + +// Get trees (harvestables with tree=1) +let trees = harvestable_db.get_trees(); + +// Get by tool requirement +let hatchet_nodes = harvestable_db.get_by_tool("hatchet"); +let pickaxe_nodes = harvestable_db.get_by_tool("pickaxe"); + +// Get by level range +let beginner = harvestable_db.get_by_level_range(1, 10); +let advanced = harvestable_db.get_by_level_range(50, 100); +``` + ## Cross-referencing Data ```rust @@ -213,6 +247,16 @@ for npc in npc_db.all_npcs() { println!("NPC '{}' has {} quest markers", npc.name, npc.questmarkers.len()); } } + +// Find items that drop from harvestables +for harvestable in harvestable_db.all_harvestables() { + for drop in &harvestable.drops { + if let Some(item) = item_db.get_by_id(drop.id) { + println!("'{}' drops: {} (rate: {})", + harvestable.name, item.name, drop.droprate.unwrap_or(0)); + } + } +} ``` ## Statistics from XML Files @@ -243,6 +287,18 @@ When loaded from `/home/connor/repos/CBAssets/Data/XMLs/`: - **Hidden Quests**: 2 - **Unique Quest Reward Items**: 70 +### Harvestables/HarvestableInfo.xml +- **Total Harvestables**: 96 +- **Trees**: 9 +- **Woodcutting**: 10 +- **Mining**: 11 +- **Fishing**: 11 +- **Alchemy**: 50 +- **Level 1-10**: 31 +- **Level 11-50**: 37 +- **Level 51-100**: 28 +- **Unique Items from Harvestables**: 98 + ## File Structure ``` @@ -255,11 +311,13 @@ cursebreaker-parser/ │ │ ├── item.rs # Item data structures │ │ ├── npc.rs # NPC data structures │ │ ├── quest.rs # Quest data structures +│ │ ├── harvestable.rs # Harvestable data structures │ │ └── interactable_resource.rs -│ ├── xml_parser.rs # XML parsing logic (Items, NPCs, Quests) +│ ├── xml_parser.rs # XML parsing logic (all types) │ ├── item_database.rs # ItemDatabase for runtime access │ ├── npc_database.rs # NpcDatabase for runtime access -│ └── quest_database.rs # QuestDatabase for runtime access +│ ├── quest_database.rs # QuestDatabase for runtime access +│ └── harvestable_database.rs # HarvestableDatabase for runtime access └── examples/ ├── item_database_demo.rs # Items usage example └── game_data_demo.rs # Full game data example @@ -280,6 +338,7 @@ thiserror = "1.0" # Error handling - ✅ Items (`/XMLs/Items/Items.xml`) - ✅ NPCs (`/XMLs/Npcs/NPCInfo.xml`) - ✅ Quests (`/XMLs/Quests/Quests.xml`) +- ✅ Harvestables (`/XMLs/Harvestables/HarvestableInfo.xml`) ## Future Enhancements diff --git a/cursebreaker-parser/examples/game_data_demo.rs b/cursebreaker-parser/examples/game_data_demo.rs index 12d3fc3..5bae60e 100644 --- a/cursebreaker-parser/examples/game_data_demo.rs +++ b/cursebreaker-parser/examples/game_data_demo.rs @@ -1,8 +1,8 @@ -//! Example demonstrating combined Items, NPCs, and Quests database usage +//! Example demonstrating combined Items, NPCs, Quests, and Harvestables database usage //! //! Run with: cargo run --example game_data_demo -use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase}; +use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase}; fn main() -> Result<(), Box> { println!("🎮 Cursebreaker Game Data Demo\n"); @@ -12,10 +12,12 @@ fn main() -> Result<(), Box> { let item_db = ItemDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml")?; let npc_db = NpcDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Npcs/NPCInfo.xml")?; let quest_db = QuestDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Quests/Quests.xml")?; + let harvestable_db = HarvestableDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Harvestables/HarvestableInfo.xml")?; println!("✅ Loaded {} items", item_db.len()); println!("✅ Loaded {} NPCs", npc_db.len()); - println!("✅ Loaded {} quests\n", quest_db.len()); + println!("✅ Loaded {} quests", quest_db.len()); + println!("✅ Loaded {} harvestables\n", harvestable_db.len()); // ======================================================================= // Items @@ -134,6 +136,61 @@ fn main() -> Result<(), Box> { } println!(); + // ======================================================================= + // Harvestables + // ======================================================================= + println!("=== Harvestables ==="); + let trees = harvestable_db.get_trees(); + let woodcutting = harvestable_db.get_by_skill("Woodcutting"); + let mining = harvestable_db.get_by_skill("mining"); + let fishing = harvestable_db.get_by_skill("Fishing"); + let alchemy = harvestable_db.get_by_skill("Alchemy"); + + println!("By skill:"); + println!(" • Trees: {}", trees.len()); + println!(" • Woodcutting: {}", woodcutting.len()); + println!(" • Mining: {}", mining.len()); + println!(" • Fishing: {}", fishing.len()); + println!(" • Alchemy: {}", alchemy.len()); + + // Sample harvestable + if let Some(spruce) = harvestable_db.get_by_typeid(1) { + println!("\nSample harvestable (TypeID 1):"); + println!(" Name: {}", spruce.name); + println!(" Action: {}", spruce.actionname.as_deref().unwrap_or("N/A")); + if let Some(level) = spruce.level { + println!(" Level: {}", level); + } + if let Some(skill) = &spruce.skill { + println!(" Skill: {}", skill); + } + if let Some(tool) = &spruce.tool { + println!(" Tool: {}", tool); + } + println!(" Drops: {} different items", spruce.drops.len()); + + // Show drops + println!(" Item drops:"); + for drop in &spruce.drops { + if let Some(item) = item_db.get_by_id(drop.id) { + println!(" - {} ({}x{}, rate: {})", + item.name, + drop.minamount.unwrap_or(1), + drop.maxamount.unwrap_or(1), + drop.droprate.unwrap_or(0)); + } + } + } + + println!("\nHarvestables by level:"); + let low_level = harvestable_db.get_by_level_range(1, 10); + let mid_level = harvestable_db.get_by_level_range(11, 50); + let high_level = harvestable_db.get_by_level_range(51, 100); + println!(" • Level 1-10: {}", low_level.len()); + println!(" • Level 11-50: {}", mid_level.len()); + println!(" • Level 51-100: {}", high_level.len()); + println!(); + // ======================================================================= // Cross-referencing data // ======================================================================= @@ -159,6 +216,15 @@ fn main() -> Result<(), Box> { } println!("Unique items used as quest rewards: {}", quest_reward_items.len()); + // Find items that are harvestable drops + let mut harvestable_items = std::collections::HashSet::new(); + for harvestable in harvestable_db.all_harvestables() { + for drop in &harvestable.drops { + harvestable_items.insert(drop.id); + } + } + println!("Unique items from harvestables: {}", harvestable_items.len()); + println!("\n✨ Demo complete!"); Ok(()) diff --git a/cursebreaker-parser/src/harvestable_database.rs b/cursebreaker-parser/src/harvestable_database.rs new file mode 100644 index 0000000..b4c2ca1 --- /dev/null +++ b/cursebreaker-parser/src/harvestable_database.rs @@ -0,0 +1,142 @@ +use crate::types::Harvestable; +use crate::xml_parser::{parse_harvestables_xml, XmlParseError}; +use std::collections::HashMap; +use std::path::Path; + +/// A database for managing Harvestables loaded from XML files +#[derive(Debug, Clone)] +pub struct HarvestableDatabase { + harvestables: Vec, + harvestables_by_typeid: HashMap, + harvestables_by_name: HashMap, +} + +impl HarvestableDatabase { + /// Create a new empty HarvestableDatabase + pub fn new() -> Self { + Self { + harvestables: Vec::new(), + harvestables_by_typeid: HashMap::new(), + harvestables_by_name: HashMap::new(), + } + } + + /// Load harvestables from an XML file + pub fn load_from_xml>(path: P) -> Result { + let harvestables = parse_harvestables_xml(path)?; + let mut db = Self::new(); + db.add_harvestables(harvestables); + Ok(db) + } + + /// Add harvestables to the database + pub fn add_harvestables(&mut self, harvestables: Vec) { + for harvestable in harvestables { + let index = self.harvestables.len(); + self.harvestables_by_typeid.insert(harvestable.typeid, index); + self.harvestables_by_name.insert(harvestable.name.clone(), index); + self.harvestables.push(harvestable); + } + } + + /// Get a harvestable by type ID + pub fn get_by_typeid(&self, typeid: i32) -> Option<&Harvestable> { + self.harvestables_by_typeid + .get(&typeid) + .and_then(|&index| self.harvestables.get(index)) + } + + /// Get a harvestable by name + pub fn get_by_name(&self, name: &str) -> Option<&Harvestable> { + self.harvestables_by_name + .get(name) + .and_then(|&index| self.harvestables.get(index)) + } + + /// Get all harvestables + pub fn all_harvestables(&self) -> &[Harvestable] { + &self.harvestables + } + + /// Get harvestables by skill + pub fn get_by_skill(&self, skill: &str) -> Vec<&Harvestable> { + self.harvestables + .iter() + .filter(|h| { + h.skill + .as_ref() + .map(|s| s.eq_ignore_ascii_case(skill)) + .unwrap_or(false) + }) + .collect() + } + + /// Get harvestables that require a specific tool + pub fn get_by_tool(&self, tool: &str) -> Vec<&Harvestable> { + self.harvestables + .iter() + .filter(|h| { + h.tool + .as_ref() + .map(|t| t.eq_ignore_ascii_case(tool)) + .unwrap_or(false) + }) + .collect() + } + + /// Get all trees (harvestables with tree=1) + pub fn get_trees(&self) -> Vec<&Harvestable> { + self.harvestables + .iter() + .filter(|h| h.is_tree()) + .collect() + } + + /// Get harvestables that require tools + pub fn get_requiring_tools(&self) -> Vec<&Harvestable> { + self.harvestables + .iter() + .filter(|h| h.requires_tool()) + .collect() + } + + /// Get harvestables by level range + pub fn get_by_level_range(&self, min_level: i32, max_level: i32) -> Vec<&Harvestable> { + self.harvestables + .iter() + .filter(|h| { + h.level + .map(|l| l >= min_level && l <= max_level) + .unwrap_or(false) + }) + .collect() + } + + /// Get number of harvestables in database + pub fn len(&self) -> usize { + self.harvestables.len() + } + + /// Check if database is empty + pub fn is_empty(&self) -> bool { + self.harvestables.is_empty() + } + + /// Prepare harvestables for SQL insertion + /// Returns a vector of tuples (typeid, name, json_data) + pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> { + self.harvestables + .iter() + .map(|harvestable| { + let json = serde_json::to_string(harvestable).unwrap_or_else(|_| "{}".to_string()); + (harvestable.typeid, harvestable.name.clone(), json) + }) + .collect() + } +} + +impl Default for HarvestableDatabase { + fn default() -> Self { + Self::new() + } +} diff --git a/cursebreaker-parser/src/lib.rs b/cursebreaker-parser/src/lib.rs index 5a14013..16b5a8b 100644 --- a/cursebreaker-parser/src/lib.rs +++ b/cursebreaker-parser/src/lib.rs @@ -54,13 +54,16 @@ mod xml_parser; mod item_database; mod npc_database; mod quest_database; +mod harvestable_database; pub use item_database::ItemDatabase; pub use npc_database::NpcDatabase; pub use quest_database::QuestDatabase; +pub use harvestable_database::HarvestableDatabase; pub use types::{ Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, InteractableResource, Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet, Quest, QuestPhase, QuestReward, + Harvestable, HarvestableDrop, }; pub use xml_parser::XmlParseError; diff --git a/cursebreaker-parser/src/main.rs b/cursebreaker-parser/src/main.rs index 2ee7da5..d8f19cb 100644 --- a/cursebreaker-parser/src/main.rs +++ b/cursebreaker-parser/src/main.rs @@ -6,7 +6,7 @@ //! 3. Extracting typeId and transform positions //! 4. Writing resource data to an output file -use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, InteractableResource}; +use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, InteractableResource}; use unity_parser::UnityProject; use std::path::Path; use unity_parser::log::DedupLogger; @@ -37,6 +37,10 @@ fn main() -> Result<(), Box> { let quest_db = QuestDatabase::load_from_xml(quests_path)?; info!("✅ Loaded {} quests", quest_db.len()); + let harvestables_path = "/home/connor/repos/CBAssets/Data/XMLs/Harvestables/HarvestableInfo.xml"; + let harvestable_db = HarvestableDatabase::load_from_xml(harvestables_path)?; + info!("✅ Loaded {} harvestables", harvestable_db.len()); + // Print statistics info!("\n📊 Game Data Statistics:"); info!(" Items:"); @@ -48,6 +52,12 @@ fn main() -> Result<(), Box> { info!(" Quests:"); info!(" • Main quests: {}", quest_db.get_main_quests().len()); info!(" • Side quests: {}", quest_db.get_side_quests().len()); + info!(" Harvestables:"); + info!(" • Trees: {}", harvestable_db.get_trees().len()); + info!(" • Woodcutting: {}", harvestable_db.get_by_skill("Woodcutting").len()); + info!(" • Mining: {}", harvestable_db.get_by_skill("mining").len()); + info!(" • Fishing: {}", harvestable_db.get_by_skill("Fishing").len()); + info!(" • Alchemy: {}", harvestable_db.get_by_skill("Alchemy").len()); // Initialize Unity project once - scans entire project for GUID mappings let project_root = Path::new("/home/connor/repos/CBAssets"); diff --git a/cursebreaker-parser/src/types/harvestable.rs b/cursebreaker-parser/src/types/harvestable.rs new file mode 100644 index 0000000..7ddea88 --- /dev/null +++ b/cursebreaker-parser/src/types/harvestable.rs @@ -0,0 +1,112 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Harvestable { + // Required fields + pub typeid: i32, + pub name: String, + + // Basic attributes + pub actionname: Option, + pub desc: Option, + pub comment: Option, + pub level: Option, + pub skill: Option, + pub tool: Option, + + // Health (can be range like "3-5" or single value) + pub health: Option, + + // Timing + pub harvesttime: Option, + pub hittime: Option, + pub respawntime: Option, + + // Audio + pub harvestsfx: Option, + pub endsfx: Option, + pub receiveitemsfx: Option, + + // Visuals + pub animation: Option, + pub takehitanimation: Option, + pub endgfx: Option, + + // Behavior flags + pub tree: Option, + pub hidemilestone: Option, + pub nohighlight: Option, + pub hideminimap: Option, + pub noleftclickinteract: Option, + + // Interaction + pub interactdistance: Option, + + // Drops + pub drops: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct HarvestableDrop { + pub id: i32, + pub minamount: Option, + pub maxamount: Option, + pub droprate: Option, + pub droprateboost: Option, + pub amountboost: Option, + pub checks: Option, + pub comment: Option, + pub dontconsumehealth: Option, +} + +impl Harvestable { + pub fn new(typeid: i32, name: String) -> Self { + Self { + typeid, + name, + actionname: None, + desc: None, + comment: None, + level: None, + skill: None, + tool: None, + health: None, + harvesttime: None, + hittime: None, + respawntime: None, + harvestsfx: None, + endsfx: None, + receiveitemsfx: None, + animation: None, + takehitanimation: None, + endgfx: None, + tree: None, + hidemilestone: None, + nohighlight: None, + hideminimap: None, + noleftclickinteract: None, + interactdistance: None, + drops: Vec::new(), + } + } + + /// Check if this is a tree + pub fn is_tree(&self) -> bool { + self.tree == Some(1) + } + + /// Check if this requires a tool + pub fn requires_tool(&self) -> bool { + self.tool.is_some() + } + + /// Get the skill associated with this harvestable + pub fn get_skill(&self) -> Option<&str> { + self.skill.as_deref() + } + + /// Get all item IDs that can drop from this harvestable + pub fn get_drop_item_ids(&self) -> Vec { + self.drops.iter().map(|d| d.id).collect() + } +} diff --git a/cursebreaker-parser/src/types/mod.rs b/cursebreaker-parser/src/types/mod.rs index 37e0e70..f09abf1 100644 --- a/cursebreaker-parser/src/types/mod.rs +++ b/cursebreaker-parser/src/types/mod.rs @@ -2,8 +2,10 @@ mod interactable_resource; mod item; mod npc; mod quest; +mod harvestable; pub use interactable_resource::InteractableResource; pub use item::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule}; pub use npc::{Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet}; pub use quest::{Quest, QuestPhase, QuestReward}; +pub use harvestable::{Harvestable, HarvestableDrop}; diff --git a/cursebreaker-parser/src/xml_parser.rs b/cursebreaker-parser/src/xml_parser.rs index 3ebce14..112ac79 100644 --- a/cursebreaker-parser/src/xml_parser.rs +++ b/cursebreaker-parser/src/xml_parser.rs @@ -2,6 +2,7 @@ use crate::types::{ Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet, Quest, QuestPhase, QuestReward, + Harvestable, HarvestableDrop, }; use quick_xml::events::Event; use quick_xml::reader::Reader; @@ -530,3 +531,121 @@ pub fn parse_quests_xml>(path: P) -> Result, XmlParseE Ok(quests) } + +// ============================================================================ +// Harvestable Parser +// ============================================================================ + +pub fn parse_harvestables_xml>(path: P) -> Result, XmlParseError> { + let file = File::open(path)?; + let buf_reader = BufReader::new(file); + let mut reader = Reader::from_reader(buf_reader); + reader.config_mut().trim_text(true); + + let mut harvestables = Vec::new(); + let mut buf = Vec::new(); + let mut current_harvestable: Option = None; + + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => { + match e.name().as_ref() { + b"harvestable" => { + let attrs = parse_attributes(e)?; + let typeid = attrs.get("typeid") + .ok_or_else(|| XmlParseError::MissingAttribute("typeid".to_string()))? + .parse::() + .map_err(|_| XmlParseError::InvalidAttribute("typeid".to_string()))?; + + let name = attrs.get("name") + .ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))? + .clone(); + + let mut harvestable = Harvestable::new(typeid, name); + + // Parse optional attributes + 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::() { + 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) +}