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
|
## 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
|
||||||
|
|
||||||
|
|||||||
@@ -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(())
|
||||||
|
|||||||
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 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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
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 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};
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user