harvestables
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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 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<dyn std::error::Error>> {
|
||||
}
|
||||
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<dyn std::error::Error>> {
|
||||
}
|
||||
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(())
|
||||
|
||||
142
cursebreaker-parser/src/harvestable_database.rs
Normal file
142
cursebreaker-parser/src/harvestable_database.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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");
|
||||
|
||||
112
cursebreaker-parser/src/types/harvestable.rs
Normal file
112
cursebreaker-parser/src/types/harvestable.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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<P: AsRef<Path>>(path: P) -> Result<Vec<Quest>, XmlParseE
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user