This commit is contained in:
2026-01-07 09:29:03 +00:00
parent be061cb3a4
commit 2efa1aa86d
16 changed files with 1517 additions and 12 deletions

View File

@@ -3,9 +3,22 @@ name = "cursebreaker-parser"
version = "0.1.0"
edition = "2021"
[lib]
name = "cursebreaker_parser"
path = "src/lib.rs"
[[bin]]
name = "cursebreaker-parser"
path = "src/main.rs"
[dependencies]
unity-parser = { path = "../unity-parser" }
serde_yaml = "0.9"
inventory = "0.3"
sparsey = "0.13"
log = { version = "0.4", features = ["std"] }
quick-xml = "0.37"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
diesel = { version = "2.2", features = ["sqlite"], optional = true }
thiserror = "1.0"

View File

@@ -0,0 +1,197 @@
# XML Parsing in Cursebreaker Parser
This document describes the XML parsing functionality added to the cursebreaker-parser project.
## Overview
The parser now supports loading game data from Cursebreaker's XML files and storing them in efficient data structures for runtime access and SQL database serialization.
## Features
- ✅ Parse Items.xml with full attribute and nested element support
- ✅ In-memory database with fast lookups by ID, name, category, slot, and skill
- ✅ JSON serialization for SQL database storage
- ✅ Type-safe data structures with serde support
- ✅ Easy-to-use API
## Quick Start
### Loading Items
```rust
use cursebreaker_parser::ItemDatabase;
let item_db = ItemDatabase::load_from_xml("Data/XMLs/Items/Items.xml")?;
println!("Loaded {} items", item_db.len());
```
### Querying Items
```rust
// Get by ID
if let Some(item) = item_db.get_by_id(150) {
println!("Found: {}", item.name);
}
// Get by category
let bows = item_db.get_by_category("bow");
// Get by slot
let weapons = item_db.get_by_slot("weapon");
// Get by skill requirement
let magic_items = item_db.get_by_skill("magic");
// Get all items
for item in item_db.all_items() {
println!("{}: {}", item.id, item.name);
}
```
### SQL Serialization
```rust
// Prepare items for SQL insertion
let sql_data = item_db.prepare_for_sql();
for (id, name, json_data) in sql_data {
// INSERT INTO items (id, name, data) VALUES (?, ?, ?)
// Use your preferred SQL library to insert
}
```
## Data Structures
### Item
The main `Item` struct contains all item attributes from the XML:
```rust
pub struct Item {
// Required
pub id: i32,
pub name: String,
// Optional attributes
pub level: Option<i32>,
pub description: Option<String>,
pub price: Option<i32>,
pub slot: Option<String>,
pub category: Option<String>,
pub skill: Option<String>,
// ... many more fields
// Nested elements
pub stats: Vec<ItemStat>,
pub crafting_recipes: Vec<CraftingRecipe>,
pub animations: Option<AnimationSet>,
pub generate_rules: Vec<GenerateRule>,
}
```
### ItemStat
Represents item statistics:
```rust
pub struct ItemStat {
// Damage
pub damagephysical: Option<i32>,
pub damagemagical: Option<i32>,
pub damageranged: Option<i32>,
// Accuracy
pub accuracyphysical: Option<i32>,
pub accuracymagical: Option<i32>,
pub accuracyranged: Option<i32>,
// Resistance
pub resistancephysical: Option<i32>,
pub resistancemagical: Option<i32>,
pub resistanceranged: Option<i32>,
// Core stats
pub health: Option<i32>,
pub mana: Option<i32>,
pub manaregen: Option<i32>,
pub healing: Option<i32>,
// Harvesting
pub harvestingspeedwoodcutting: Option<i32>,
}
```
## Example Program
Run the demo to see all features in action:
```bash
cargo run --example item_database_demo
```
## Statistics from Items.xml
When loaded from `/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml`:
- **Total Items**: 1,360
- **Weapons**: 166
- **Armor**: 148
- **Consumables**: 294
- **Trinkets**: 59
- **Bows**: 18
- **Magic Items**: 76
## File Structure
```
cursebreaker-parser/
├── src/
│ ├── lib.rs # Library exports
│ ├── main.rs # Main binary (includes Unity + XML parsing)
│ ├── types/
│ │ ├── mod.rs
│ │ ├── item.rs # Item data structures
│ │ └── interactable_resource.rs
│ ├── xml_parser.rs # XML parsing logic
│ └── item_database.rs # ItemDatabase for runtime access
└── examples/
└── item_database_demo.rs # Full usage example
```
## Dependencies Added
```toml
quick-xml = "0.37" # XML parsing
serde = { version = "1.0", features = ["derive"] } # Serialization
serde_json = "1.0" # JSON serialization
diesel = { version = "2.2", features = ["sqlite"], optional = true } # SQL (optional)
thiserror = "1.0" # Error handling
```
## Future Enhancements
The same pattern can be extended to parse other XML files:
- [ ] NPCs (`/XMLs/Npcs/*.xml`)
- [ ] Quests (`/XMLs/Quests/*.xml`)
- [ ] Loot tables (`/XMLs/Loot/*.xml`)
- [ ] Maps (`/XMLs/Maps/*.xml`)
- [ ] Dialogue (`/XMLs/Dialogue/*.xml`)
- [ ] Events (`/XMLs/Events/*.xml`)
Each would follow the same pattern:
1. Define data structures in `src/types/`
2. Create parser in `src/xml_parser.rs`
3. Create database wrapper for runtime access
4. Add to `lib.rs` exports
## Integration with Unity Parser
The main binary (`src/main.rs`) demonstrates integration of both systems:
1. Load game data from XML files (Items, etc.)
2. Parse Unity scenes for game objects
3. Cross-reference data (e.g., item IDs in loot spawners)
This creates a complete game data pipeline from source files to runtime.

View File

@@ -0,0 +1,101 @@
//! Example demonstrating ItemDatabase usage
//!
//! Run with: cargo run --example item_database_demo
use cursebreaker_parser::ItemDatabase;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🎮 Cursebreaker Item Database Demo\n");
// Load items from XML
let items_path = "/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml";
println!("📚 Loading items from: {}", items_path);
let item_db = ItemDatabase::load_from_xml(items_path)?;
println!("✅ Loaded {} items\n", item_db.len());
// Example 1: Get item by ID
println!("=== Example 1: Get Item by ID ===");
if let Some(item) = item_db.get_by_id(150) {
println!("Item ID 150:");
println!(" Name: {}", item.name);
if let Some(desc) = &item.description {
println!(" Description: {}", desc);
}
if let Some(slot) = &item.slot {
println!(" Slot: {}", slot);
}
if let Some(skill) = &item.skill {
println!(" Skill: {}", skill);
}
println!(" Stats: {} stat entries", item.stats.len());
}
println!();
// Example 2: Get items by category
println!("=== Example 2: Get Items by Category ===");
let bows = item_db.get_by_category("bow");
println!("Found {} bows:", bows.len());
for item in bows.iter().take(5) {
println!(" - {} (ID: {})", item.name, item.id);
}
println!();
// Example 3: Get items by slot
println!("=== Example 3: Get Items by Slot ===");
let consumables = item_db.get_by_slot("consumable");
println!("Found {} consumables (showing first 10):", consumables.len());
for item in consumables.iter().take(10) {
let name = &item.name;
let id = item.id;
if let Some(desc) = &item.description {
println!(" - {} (ID: {}) - {}", name, id, desc.chars().take(50).collect::<String>());
} else {
println!(" - {} (ID: {})", name, id);
}
}
println!();
// Example 4: Get items by skill
println!("=== Example 4: Get Items by Skill ===");
let magic_items = item_db.get_by_skill("magic");
println!("Found {} magic items:", magic_items.len());
for item in magic_items.iter().take(5) {
println!(" - {} (ID: {}, Level: {:?})",
item.name, item.id, item.level);
}
println!();
// Example 5: Statistics
println!("=== Example 5: Database Statistics ===");
let weapons = item_db.get_by_slot("weapon");
let armor = item_db.get_by_slot("armor");
let consumables = item_db.get_by_slot("consumable");
let trinkets = item_db.get_by_slot("trinket");
println!("Item Distribution by Slot:");
println!(" Weapons: {}", weapons.len());
println!(" Armor: {}", armor.len());
println!(" Consumables: {}", consumables.len());
println!(" Trinkets: {}", trinkets.len());
println!();
// Example 6: Prepare for SQL (showing how it would be used)
println!("=== Example 6: SQL Serialization ===");
let sql_data = item_db.prepare_for_sql();
println!("Prepared {} items for SQL insertion", sql_data.len());
println!("Sample SQL inserts (first 3):");
for (id, name, json) in sql_data.iter().take(3) {
let json_preview = if json.len() > 100 {
format!("{}...", &json[..100])
} else {
json.clone()
};
println!(" INSERT INTO items (id, name, data) VALUES ({}, '{}', '{}');",
id, name, json_preview);
}
println!("\n✨ Demo complete!");
Ok(())
}

View File

@@ -0,0 +1,157 @@
use crate::types::Item;
use crate::xml_parser::{parse_items_xml, XmlParseError};
use std::collections::HashMap;
use std::path::Path;
/// A database for managing game items loaded from XML files
#[derive(Debug, Clone)]
pub struct ItemDatabase {
items: Vec<Item>,
items_by_id: HashMap<i32, usize>,
items_by_name: HashMap<String, Vec<usize>>,
}
impl ItemDatabase {
/// Create a new empty ItemDatabase
pub fn new() -> Self {
Self {
items: Vec::new(),
items_by_id: HashMap::new(),
items_by_name: HashMap::new(),
}
}
/// Load items from an XML file
pub fn load_from_xml<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
let items = parse_items_xml(path)?;
let mut db = Self::new();
db.add_items(items);
Ok(db)
}
/// Add items to the database
pub fn add_items(&mut self, items: Vec<Item>) {
for item in items {
let index = self.items.len();
self.items_by_id.insert(item.id, index);
// Add to name index (can have multiple items with same name)
self.items_by_name
.entry(item.name.clone())
.or_insert_with(Vec::new)
.push(index);
self.items.push(item);
}
}
/// Get an item by ID
pub fn get_by_id(&self, id: i32) -> Option<&Item> {
self.items_by_id
.get(&id)
.and_then(|&index| self.items.get(index))
}
/// Get items by name (returns all items with matching name)
pub fn get_by_name(&self, name: &str) -> Vec<&Item> {
self.items_by_name
.get(name)
.map(|indices| {
indices
.iter()
.filter_map(|&index| self.items.get(index))
.collect()
})
.unwrap_or_default()
}
/// Get all items
pub fn all_items(&self) -> &[Item] {
&self.items
}
/// Get items by category
pub fn get_by_category(&self, category: &str) -> Vec<&Item> {
self.items
.iter()
.filter(|item| {
item.category
.as_ref()
.map(|c| c == category)
.unwrap_or(false)
})
.collect()
}
/// Get items by slot
pub fn get_by_slot(&self, slot: &str) -> Vec<&Item> {
self.items
.iter()
.filter(|item| {
item.slot
.as_ref()
.map(|s| s == slot)
.unwrap_or(false)
})
.collect()
}
/// Get items by skill requirement
pub fn get_by_skill(&self, skill: &str) -> Vec<&Item> {
self.items
.iter()
.filter(|item| {
item.skill
.as_ref()
.map(|s| s == skill)
.unwrap_or(false)
})
.collect()
}
/// Get number of items in database
pub fn len(&self) -> usize {
self.items.len()
}
/// Check if database is empty
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
/// Serialize items to JSON for SQL storage
#[cfg(feature = "diesel")]
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(&self.items)
}
/// Prepare items for SQL insertion
/// Returns a vector of tuples (id, name, json_data)
pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> {
self.items
.iter()
.map(|item| {
let json = serde_json::to_string(item).unwrap_or_else(|_| "{}".to_string());
(item.id, item.name.clone(), json)
})
.collect()
}
}
impl Default for ItemDatabase {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_item_database_basic() {
let mut db = ItemDatabase::new();
assert!(db.is_empty());
assert_eq!(db.len(), 0);
}
}

View File

@@ -0,0 +1,58 @@
//! Cursebreaker Parser - A library for parsing Cursebreaker game data
//!
//! This library provides functionality to:
//! - Parse Unity scenes and extract game objects
//! - Load game data from XML files (Items, NPCs, Quests, etc.)
//! - Store and query game data at runtime
//! - Serialize data to SQL databases
//!
//! # Example - Loading Items from XML
//!
//! ```no_run
//! use cursebreaker_parser::ItemDatabase;
//!
//! // Load all items from XML
//! let item_db = ItemDatabase::load_from_xml("Data/XMLs/Items/Items.xml")?;
//! println!("Loaded {} items", item_db.len());
//!
//! // Get item by ID
//! if let Some(item) = item_db.get_by_id(150) {
//! println!("Found: {}", item.name);
//! }
//!
//! // Query items by category
//! let weapons = item_db.get_by_category("bow");
//! println!("Found {} bows", weapons.len());
//!
//! // Query items by slot
//! let consumables = item_db.get_by_slot("consumable");
//! for item in consumables {
//! println!("Consumable: {}", item.name);
//! }
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
//!
//! # Example - Preparing Data for SQL
//!
//! ```no_run
//! use cursebreaker_parser::ItemDatabase;
//!
//! let item_db = ItemDatabase::load_from_xml("Data/XMLs/Items/Items.xml")?;
//!
//! // Prepare data for SQL insertion
//! // Returns Vec<(id, name, json_data)>
//! let sql_data = item_db.prepare_for_sql();
//!
//! for (id, name, json) in sql_data.iter().take(5) {
//! println!("INSERT INTO items VALUES ({}, '{}', '{}')", id, name, json);
//! }
//! # Ok::<(), Box<dyn std::error::Error>>(())
//! ```
pub mod types;
mod xml_parser;
mod item_database;
pub use item_database::ItemDatabase;
pub use types::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, InteractableResource};
pub use xml_parser::XmlParseError;

View File

@@ -6,9 +6,7 @@
//! 3. Extracting typeId and transform positions
//! 4. Writing resource data to an output file
mod types;
use types::InteractableResource;
use cursebreaker_parser::{ItemDatabase, InteractableResource};
use unity_parser::UnityProject;
use std::path::Path;
use unity_parser::log::DedupLogger;
@@ -24,9 +22,33 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
info!("🎮 Cursebreaker - Resource Parser");
// Load items from XML
info!("📚 Loading items from XML...");
let items_path = "/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml";
let item_db = ItemDatabase::load_from_xml(items_path)?;
info!("✅ Loaded {} items from XML", item_db.len());
// Print some item statistics
let weapons = item_db.get_by_slot("weapon");
let consumables = item_db.get_by_slot("consumable");
info!(" • Weapons: {}", weapons.len());
info!(" • Consumables: {}", consumables.len());
// Example: Print first few items
info!("\n📦 Sample Items:");
for item in item_db.all_items().iter().take(5) {
info!(" ID: {}, Name: \"{}\"", item.id, item.name);
if let Some(desc) = &item.description {
info!(" Description: {}", desc);
}
if let Some(price) = item.price {
info!(" Price: {}", price);
}
}
// Initialize Unity project once - scans entire project for GUID mappings
let project_root = Path::new("/home/connor/repos/CBAssets");
info!("📦 Initializing Unity project from: {}", project_root.display());
info!("\n📦 Initializing Unity project from: {}", project_root.display());
let project = UnityProject::from_path(project_root)?;

View File

@@ -0,0 +1,158 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
// Required fields
pub id: i32,
pub name: String,
// Optional basic attributes
pub level: Option<i32>,
pub description: Option<String>,
pub price: Option<i32>,
pub slot: Option<String>,
pub category: Option<String>,
pub skill: Option<String>,
pub tool: Option<String>,
// Item behavior
pub stackable: Option<i32>,
pub maxstack: Option<i32>,
pub abilityid: Option<i32>,
pub swap: Option<i32>,
pub twohanded: Option<i32>,
// Food/consumable properties
pub foodamount: Option<i32>,
pub foodfrequency: Option<i32>,
pub foodtime: Option<i32>,
pub foodlevel: Option<i32>,
// Crafting
pub craftingskill: Option<String>,
pub workbench: Option<i32>,
pub craftingitems: Option<String>,
// Visual/audio
pub handmodel: Option<String>,
pub groundmodel: Option<String>,
pub usingitemmodel: Option<String>,
pub dropsfx: Option<String>,
pub pickupsfx: Option<String>,
pub hitgfx: Option<String>,
pub attackanimations: Option<String>,
pub attackanimationspeed: Option<String>,
pub attackhitsounds: Option<String>,
// Storage
pub storageitem: Option<String>,
pub storagesize: Option<i32>,
// Other flags
pub hidemilestone: Option<i32>,
pub generateicon: Option<i32>,
pub comment: Option<String>,
// Nested elements
pub stats: Vec<ItemStat>,
pub crafting_recipes: Vec<CraftingRecipe>,
pub animations: Option<AnimationSet>,
pub generate_rules: Vec<GenerateRule>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ItemStat {
// Damage stats
pub damagephysical: Option<i32>,
pub damagemagical: Option<i32>,
pub damageranged: Option<i32>,
// Accuracy stats
pub accuracyphysical: Option<i32>,
pub accuracymagical: Option<i32>,
pub accuracyranged: Option<i32>,
// Resistance stats
pub resistancephysical: Option<i32>,
pub resistancemagical: Option<i32>,
pub resistanceranged: Option<i32>,
// Core stats
pub health: Option<i32>,
pub mana: Option<i32>,
pub manaregen: Option<i32>,
pub healing: Option<i32>,
// Harvesting stats
pub harvestingspeedwoodcutting: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CraftingRecipe {
pub workbench: Option<i32>,
pub craftingitems: Option<String>,
pub craftingskill: Option<String>,
pub checks: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnimationSet {
pub idle: Option<String>,
pub walk: Option<String>,
pub run: Option<String>,
pub weaponattack: Option<String>,
pub takehit: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateRule {
pub generatestats: Option<String>,
pub generatecrafting: Option<i32>,
pub generateicon: Option<i32>,
}
impl Item {
pub fn new(id: i32, name: String) -> Self {
Self {
id,
name,
level: None,
description: None,
price: None,
slot: None,
category: None,
skill: None,
tool: None,
stackable: None,
maxstack: None,
abilityid: None,
swap: None,
twohanded: None,
foodamount: None,
foodfrequency: None,
foodtime: None,
foodlevel: None,
craftingskill: None,
workbench: None,
craftingitems: None,
handmodel: None,
groundmodel: None,
usingitemmodel: None,
dropsfx: None,
pickupsfx: None,
hitgfx: None,
attackanimations: None,
attackanimationspeed: None,
attackhitsounds: None,
storageitem: None,
storagesize: None,
hidemilestone: None,
generateicon: None,
comment: None,
stats: Vec::new(),
crafting_recipes: Vec::new(),
animations: None,
generate_rules: Vec::new(),
}
}
}

View File

@@ -1,3 +1,5 @@
mod interactable_resource;
mod item;
pub use interactable_resource::InteractableResource;
pub use item::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule};

View File

@@ -0,0 +1,190 @@
use crate::types::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule};
use quick_xml::events::Event;
use quick_xml::reader::Reader;
use std::collections::HashMap;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum XmlParseError {
#[error("XML parsing error: {0}")]
XmlError(#[from] quick_xml::Error),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Attribute error: {0}")]
AttrError(#[from] quick_xml::events::attributes::AttrError),
#[error("Missing required attribute: {0}")]
MissingAttribute(String),
#[error("Invalid attribute value: {0}")]
InvalidAttribute(String),
}
pub fn parse_items_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Item>, XmlParseError> {
let file = File::open(path)?;
let buf_reader = BufReader::new(file);
let mut reader = Reader::from_reader(buf_reader);
reader.config_mut().trim_text(true);
let mut items = Vec::new();
let mut buf = Vec::new();
let mut current_item: Option<Item> = None;
loop {
match reader.read_event_into(&mut buf) {
Ok(Event::Start(e)) | Ok(Event::Empty(e)) => {
match e.name().as_ref() {
b"item" => {
let attrs = parse_attributes(&e)?;
// Get required attributes
let id = attrs.get("id")
.ok_or_else(|| XmlParseError::MissingAttribute("id".to_string()))?
.parse::<i32>()
.map_err(|_| XmlParseError::InvalidAttribute("id".to_string()))?;
let name = attrs.get("name")
.ok_or_else(|| XmlParseError::MissingAttribute("name".to_string()))?
.clone();
let mut item = Item::new(id, name);
// Parse optional attributes
if let Some(v) = attrs.get("level") { item.level = v.parse().ok(); }
if let Some(v) = attrs.get("description") { item.description = Some(v.clone()); }
if let Some(v) = attrs.get("price") { item.price = v.parse().ok(); }
if let Some(v) = attrs.get("slot") { item.slot = Some(v.clone()); }
if let Some(v) = attrs.get("category") { item.category = Some(v.clone()); }
if let Some(v) = attrs.get("skill") { item.skill = Some(v.clone()); }
if let Some(v) = attrs.get("tool") { item.tool = Some(v.clone()); }
if let Some(v) = attrs.get("stackable") { item.stackable = v.parse().ok(); }
if let Some(v) = attrs.get("maxstack") { item.maxstack = v.parse().ok(); }
if let Some(v) = attrs.get("abilityid") { item.abilityid = v.parse().ok(); }
if let Some(v) = attrs.get("swap") { item.swap = v.parse().ok(); }
if let Some(v) = attrs.get("twohanded") { item.twohanded = v.parse().ok(); }
if let Some(v) = attrs.get("foodamount") { item.foodamount = v.parse().ok(); }
if let Some(v) = attrs.get("foodfrequency") { item.foodfrequency = v.parse().ok(); }
if let Some(v) = attrs.get("foodtime") { item.foodtime = v.parse().ok(); }
if let Some(v) = attrs.get("foodlevel") { item.foodlevel = v.parse().ok(); }
if let Some(v) = attrs.get("craftingskill") { item.craftingskill = Some(v.clone()); }
if let Some(v) = attrs.get("workbench") { item.workbench = v.parse().ok(); }
if let Some(v) = attrs.get("craftingitems") { item.craftingitems = Some(v.clone()); }
if let Some(v) = attrs.get("handmodel") { item.handmodel = Some(v.clone()); }
if let Some(v) = attrs.get("groundmodel") { item.groundmodel = Some(v.clone()); }
if let Some(v) = attrs.get("usingitemmodel") { item.usingitemmodel = Some(v.clone()); }
if let Some(v) = attrs.get("dropsfx") { item.dropsfx = Some(v.clone()); }
if let Some(v) = attrs.get("pickupsfx") { item.pickupsfx = Some(v.clone()); }
if let Some(v) = attrs.get("hitgfx") { item.hitgfx = Some(v.clone()); }
if let Some(v) = attrs.get("attackanimations") { item.attackanimations = Some(v.clone()); }
if let Some(v) = attrs.get("attackanimationspeed") { item.attackanimationspeed = Some(v.clone()); }
if let Some(v) = attrs.get("attackhitsounds") { item.attackhitsounds = Some(v.clone()); }
if let Some(v) = attrs.get("storageitem") { item.storageitem = Some(v.clone()); }
if let Some(v) = attrs.get("storagesize") { item.storagesize = v.parse().ok(); }
if let Some(v) = attrs.get("hidemilestone") { item.hidemilestone = v.parse().ok(); }
if let Some(v) = attrs.get("generateicon") { item.generateicon = v.parse().ok(); }
if let Some(v) = attrs.get("comment") { item.comment = Some(v.clone()); }
current_item = Some(item);
}
b"stat" => {
if let Some(ref mut item) = current_item {
let attrs = parse_attributes(&e)?;
let stat = parse_stat(&attrs);
item.stats.push(stat);
}
}
b"crafting" => {
if let Some(ref mut item) = current_item {
let attrs = parse_attributes(&e)?;
let recipe = CraftingRecipe {
workbench: attrs.get("workbench").and_then(|v| v.parse().ok()),
craftingitems: attrs.get("craftingitems").cloned(),
craftingskill: attrs.get("craftingskill").cloned(),
checks: attrs.get("checks").cloned(),
};
item.crafting_recipes.push(recipe);
}
}
b"anim" => {
if let Some(ref mut item) = current_item {
let attrs = parse_attributes(&e)?;
let anim = AnimationSet {
idle: attrs.get("idle").cloned(),
walk: attrs.get("walk").cloned(),
run: attrs.get("run").cloned(),
weaponattack: attrs.get("weaponattack").cloned(),
takehit: attrs.get("takehit").cloned(),
};
item.animations = Some(anim);
}
}
b"generate" => {
if let Some(ref mut item) = current_item {
let attrs = parse_attributes(&e)?;
let rule = GenerateRule {
generatestats: attrs.get("generatestats").cloned(),
generatecrafting: attrs.get("generatecrafting").and_then(|v| v.parse().ok()),
generateicon: attrs.get("generateicon").and_then(|v| v.parse().ok()),
};
item.generate_rules.push(rule);
}
}
_ => {}
}
}
Ok(Event::End(e)) => {
match e.name().as_ref() {
b"item" => {
if let Some(item) = current_item.take() {
items.push(item);
}
}
_ => {}
}
}
Ok(Event::Eof) => break,
Err(e) => return Err(XmlParseError::XmlError(e)),
_ => {}
}
buf.clear();
}
Ok(items)
}
fn parse_attributes(element: &quick_xml::events::BytesStart) -> Result<HashMap<String, String>, XmlParseError> {
let mut attrs = HashMap::new();
for attr in element.attributes() {
let attr = attr?;
let key = String::from_utf8_lossy(attr.key.as_ref()).to_string();
let value = attr.unescape_value()?.to_string();
attrs.insert(key, value);
}
Ok(attrs)
}
fn parse_stat(attrs: &HashMap<String, String>) -> ItemStat {
ItemStat {
damagephysical: attrs.get("damagephysical").and_then(|v| v.parse().ok()),
damagemagical: attrs.get("damagemagical").and_then(|v| v.parse().ok()),
damageranged: attrs.get("damageranged").and_then(|v| v.parse().ok()),
accuracyphysical: attrs.get("accuracyphysical").and_then(|v| v.parse().ok()),
accuracymagical: attrs.get("accuracymagical").and_then(|v| v.parse().ok()),
accuracyranged: attrs.get("accuracyranged").and_then(|v| v.parse().ok()),
resistancephysical: attrs.get("resistancephysical").and_then(|v| v.parse().ok()),
resistancemagical: attrs.get("resistancemagical").and_then(|v| v.parse().ok()),
resistanceranged: attrs.get("resistanceranged").and_then(|v| v.parse().ok()),
health: attrs.get("health").and_then(|v| v.parse().ok()),
mana: attrs.get("mana").and_then(|v| v.parse().ok()),
manaregen: attrs.get("manaregen").and_then(|v| v.parse().ok()),
healing: attrs.get("healing").and_then(|v| v.parse().ok()),
harvestingspeedwoodcutting: attrs.get("harvestingspeedwoodcutting").and_then(|v| v.parse().ok()),
}
}