harvestables

This commit is contained in:
2026-01-07 10:17:30 +00:00
parent beecbd33c6
commit 185d324efe
8 changed files with 521 additions and 8 deletions

View File

@@ -8,7 +8,7 @@ The parser now supports loading game data from Cursebreaker's XML files and stor
## Features ## 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 - ✅ In-memory databases with fast lookups by ID, name, and various filters
- ✅ JSON serialization for SQL database storage - ✅ JSON serialization for SQL database storage
- ✅ Type-safe data structures with serde support - ✅ Type-safe data structures with serde support
@@ -131,7 +131,7 @@ Run the demos to see all features in action:
# Items only # Items only
cargo run --example item_database_demo 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 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(); 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 ## Cross-referencing Data
```rust ```rust
@@ -213,6 +247,16 @@ for npc in npc_db.all_npcs() {
println!("NPC '{}' has {} quest markers", npc.name, npc.questmarkers.len()); 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 ## Statistics from XML Files
@@ -243,6 +287,18 @@ When loaded from `/home/connor/repos/CBAssets/Data/XMLs/`:
- **Hidden Quests**: 2 - **Hidden Quests**: 2
- **Unique Quest Reward Items**: 70 - **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 ## File Structure
``` ```
@@ -255,11 +311,13 @@ cursebreaker-parser/
│ │ ├── item.rs # Item data structures │ │ ├── item.rs # Item data structures
│ │ ├── npc.rs # NPC data structures │ │ ├── npc.rs # NPC data structures
│ │ ├── quest.rs # Quest data structures │ │ ├── quest.rs # Quest data structures
│ │ ├── harvestable.rs # Harvestable data structures
│ │ └── interactable_resource.rs │ │ └── 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 │ ├── item_database.rs # ItemDatabase for runtime access
│ ├── npc_database.rs # NpcDatabase 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/ └── examples/
├── item_database_demo.rs # Items usage example ├── item_database_demo.rs # Items usage example
└── game_data_demo.rs # Full game data example └── game_data_demo.rs # Full game data example
@@ -280,6 +338,7 @@ thiserror = "1.0" # Error handling
- ✅ Items (`/XMLs/Items/Items.xml`) - ✅ Items (`/XMLs/Items/Items.xml`)
- ✅ NPCs (`/XMLs/Npcs/NPCInfo.xml`) - ✅ NPCs (`/XMLs/Npcs/NPCInfo.xml`)
- ✅ Quests (`/XMLs/Quests/Quests.xml`) - ✅ Quests (`/XMLs/Quests/Quests.xml`)
- ✅ Harvestables (`/XMLs/Harvestables/HarvestableInfo.xml`)
## Future Enhancements ## Future Enhancements

View File

@@ -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 //! 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<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🎮 Cursebreaker Game Data Demo\n"); println!("🎮 Cursebreaker Game Data Demo\n");
@@ -12,10 +12,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let item_db = ItemDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml")?; 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 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 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 {} items", item_db.len());
println!("✅ Loaded {} NPCs", npc_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 // Items
@@ -134,6 +136,61 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
println!(); 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 // Cross-referencing data
// ======================================================================= // =======================================================================
@@ -159,6 +216,15 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
println!("Unique items used as quest rewards: {}", quest_reward_items.len()); 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!"); println!("\n✨ Demo complete!");
Ok(()) Ok(())

View File

@@ -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<Harvestable>,
harvestables_by_typeid: HashMap<i32, usize>,
harvestables_by_name: HashMap<String, usize>,
}
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<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
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<Harvestable>) {
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()
}
}

View File

@@ -54,13 +54,16 @@ mod xml_parser;
mod item_database; mod item_database;
mod npc_database; mod npc_database;
mod quest_database; mod quest_database;
mod harvestable_database;
pub use item_database::ItemDatabase; pub use item_database::ItemDatabase;
pub use npc_database::NpcDatabase; pub use npc_database::NpcDatabase;
pub use quest_database::QuestDatabase; pub use quest_database::QuestDatabase;
pub use harvestable_database::HarvestableDatabase;
pub use types::{ pub use types::{
Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, InteractableResource, Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, InteractableResource,
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet, Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet,
Quest, QuestPhase, QuestReward, Quest, QuestPhase, QuestReward,
Harvestable, HarvestableDrop,
}; };
pub use xml_parser::XmlParseError; pub use xml_parser::XmlParseError;

View File

@@ -6,7 +6,7 @@
//! 3. Extracting typeId and transform positions //! 3. Extracting typeId and transform positions
//! 4. Writing resource data to an output file //! 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 unity_parser::UnityProject;
use std::path::Path; use std::path::Path;
use unity_parser::log::DedupLogger; use unity_parser::log::DedupLogger;
@@ -37,6 +37,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let quest_db = QuestDatabase::load_from_xml(quests_path)?; let quest_db = QuestDatabase::load_from_xml(quests_path)?;
info!("✅ Loaded {} quests", quest_db.len()); 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 // Print statistics
info!("\n📊 Game Data Statistics:"); info!("\n📊 Game Data Statistics:");
info!(" Items:"); info!(" Items:");
@@ -48,6 +52,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
info!(" Quests:"); info!(" Quests:");
info!(" • Main quests: {}", quest_db.get_main_quests().len()); info!(" • Main quests: {}", quest_db.get_main_quests().len());
info!(" • Side quests: {}", quest_db.get_side_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 // Initialize Unity project once - scans entire project for GUID mappings
let project_root = Path::new("/home/connor/repos/CBAssets"); let project_root = Path::new("/home/connor/repos/CBAssets");

View File

@@ -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<String>,
pub desc: Option<String>,
pub comment: Option<String>,
pub level: Option<i32>,
pub skill: Option<String>,
pub tool: Option<String>,
// Health (can be range like "3-5" or single value)
pub health: Option<String>,
// Timing
pub harvesttime: Option<i32>,
pub hittime: Option<i32>,
pub respawntime: Option<i32>,
// Audio
pub harvestsfx: Option<String>,
pub endsfx: Option<String>,
pub receiveitemsfx: Option<String>,
// Visuals
pub animation: Option<String>,
pub takehitanimation: Option<String>,
pub endgfx: Option<String>,
// Behavior flags
pub tree: Option<i32>,
pub hidemilestone: Option<i32>,
pub nohighlight: Option<i32>,
pub hideminimap: Option<i32>,
pub noleftclickinteract: Option<i32>,
// Interaction
pub interactdistance: Option<String>,
// Drops
pub drops: Vec<HarvestableDrop>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HarvestableDrop {
pub id: i32,
pub minamount: Option<i32>,
pub maxamount: Option<i32>,
pub droprate: Option<i32>,
pub droprateboost: Option<i32>,
pub amountboost: Option<i32>,
pub checks: Option<String>,
pub comment: Option<String>,
pub dontconsumehealth: Option<i32>,
}
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<i32> {
self.drops.iter().map(|d| d.id).collect()
}
}

View File

@@ -2,8 +2,10 @@ mod interactable_resource;
mod item; mod item;
mod npc; mod npc;
mod quest; mod quest;
mod harvestable;
pub use interactable_resource::InteractableResource; pub use interactable_resource::InteractableResource;
pub use item::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule}; pub use item::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule};
pub use npc::{Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet}; pub use npc::{Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet};
pub use quest::{Quest, QuestPhase, QuestReward}; pub use quest::{Quest, QuestPhase, QuestReward};
pub use harvestable::{Harvestable, HarvestableDrop};

View File

@@ -2,6 +2,7 @@ use crate::types::{
Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule,
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet, Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet,
Quest, QuestPhase, QuestReward, Quest, QuestPhase, QuestReward,
Harvestable, HarvestableDrop,
}; };
use quick_xml::events::Event; use quick_xml::events::Event;
use quick_xml::reader::Reader; use quick_xml::reader::Reader;
@@ -530,3 +531,121 @@ pub fn parse_quests_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Quest>, XmlParseE
Ok(quests) Ok(quests)
} }
// ============================================================================
// Harvestable Parser
// ============================================================================
pub fn parse_harvestables_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Harvestable>, XmlParseError> {
let file = File::open(path)?;
let buf_reader = BufReader::new(file);
let mut reader = Reader::from_reader(buf_reader);
reader.config_mut().trim_text(true);
let mut harvestables = Vec::new();
let mut buf = Vec::new();
let mut current_harvestable: Option<Harvestable> = None;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
match e.name().as_ref() {
b"harvestable" => {
let attrs = parse_attributes(e)?;
let typeid = attrs.get("typeid")
.ok_or_else(|| XmlParseError::MissingAttribute("typeid".to_string()))?
.parse::<i32>()
.map_err(|_| XmlParseError::InvalidAttribute("typeid".to_string()))?;
let name = attrs.get("name")
.ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))?
.clone();
let mut harvestable = Harvestable::new(typeid, name);
// Parse optional attributes
if let Some(v) = attrs.get("actionname") { harvestable.actionname = Some(v.clone()); }
if let Some(v) = attrs.get("desc") { harvestable.desc = Some(v.clone()); }
if let Some(v) = attrs.get("comment") { harvestable.comment = Some(v.clone()); }
if let Some(v) = attrs.get("level") { harvestable.level = v.parse().ok(); }
if let Some(v) = attrs.get("skill") { harvestable.skill = Some(v.clone()); }
if let Some(v) = attrs.get("tool") { harvestable.tool = Some(v.clone()); }
if let Some(v) = attrs.get("health") { harvestable.health = Some(v.clone()); }
if let Some(v) = attrs.get("harvesttime") { harvestable.harvesttime = v.parse().ok(); }
if let Some(v) = attrs.get("hittime") { harvestable.hittime = v.parse().ok(); }
if let Some(v) = attrs.get("respawntime") { harvestable.respawntime = v.parse().ok(); }
// Audio (handle both cases: harvestSfx and harvestsfx)
if let Some(v) = attrs.get("harvestSfx").or_else(|| attrs.get("harvestsfx")) {
harvestable.harvestsfx = Some(v.clone());
}
if let Some(v) = attrs.get("endSfx").or_else(|| attrs.get("endsfx")) {
harvestable.endsfx = Some(v.clone());
}
if let Some(v) = attrs.get("receiveItemSfx").or_else(|| attrs.get("receiveitemsfx")) {
harvestable.receiveitemsfx = Some(v.clone());
}
if let Some(v) = attrs.get("animation") { harvestable.animation = Some(v.clone()); }
if let Some(v) = attrs.get("takehitanimation") { harvestable.takehitanimation = Some(v.clone()); }
if let Some(v) = attrs.get("endgfx") { harvestable.endgfx = Some(v.clone()); }
if let Some(v) = attrs.get("tree") { harvestable.tree = v.parse().ok(); }
if let Some(v) = attrs.get("hidemilestone") { harvestable.hidemilestone = v.parse().ok(); }
if let Some(v) = attrs.get("nohighlight") { harvestable.nohighlight = v.parse().ok(); }
// Handle both cases: hideMinimap and hideminimap
if let Some(v) = attrs.get("hideMinimap").or_else(|| attrs.get("hideminimap")) {
harvestable.hideminimap = v.parse().ok();
}
if let Some(v) = attrs.get("noLeftClickInteract").or_else(|| attrs.get("noleftclickinteract")) {
harvestable.noleftclickinteract = v.parse().ok();
}
if let Some(v) = attrs.get("interactDistance").or_else(|| attrs.get("interactdistance")) {
harvestable.interactdistance = Some(v.clone());
}
current_harvestable = Some(harvestable);
}
b"item" if current_harvestable.is_some() => {
if let Some(ref mut harvestable) = current_harvestable {
let attrs = parse_attributes(e)?;
if let Some(id_str) = attrs.get("id") {
if let Ok(id) = id_str.parse::<i32>() {
let drop = HarvestableDrop {
id,
minamount: attrs.get("minamount").and_then(|v| v.parse().ok()),
maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()),
droprate: attrs.get("droprate").and_then(|v| v.parse().ok()),
droprateboost: attrs.get("droprateboost").and_then(|v| v.parse().ok()),
amountboost: attrs.get("amountboost").and_then(|v| v.parse().ok()),
checks: attrs.get("checks").cloned(),
comment: attrs.get("comment").cloned(),
dontconsumehealth: attrs.get("dontconsumehealth").and_then(|v| v.parse().ok()),
};
harvestable.drops.push(drop);
}
}
}
}
_ => {}
}
}
Ok(Event::End(ref e)) => {
match e.name().as_ref() {
b"harvestable" => {
if let Some(harvestable) = current_harvestable.take() {
harvestables.push(harvestable);
}
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(XmlParseError::XmlError(e)),
_ => {}
}
buf.clear();
}
Ok(harvestables)
}