items DB
This commit is contained in:
@@ -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"
|
||||
|
||||
197
cursebreaker-parser/XML_PARSING.md
Normal file
197
cursebreaker-parser/XML_PARSING.md
Normal 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.
|
||||
101
cursebreaker-parser/examples/item_database_demo.rs
Normal file
101
cursebreaker-parser/examples/item_database_demo.rs
Normal 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(())
|
||||
}
|
||||
157
cursebreaker-parser/src/item_database.rs
Normal file
157
cursebreaker-parser/src/item_database.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
58
cursebreaker-parser/src/lib.rs
Normal file
58
cursebreaker-parser/src/lib.rs
Normal 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;
|
||||
@@ -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)?;
|
||||
|
||||
|
||||
158
cursebreaker-parser/src/types/item.rs
Normal file
158
cursebreaker-parser/src/types/item.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod interactable_resource;
|
||||
mod item;
|
||||
|
||||
pub use interactable_resource::InteractableResource;
|
||||
pub use item::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule};
|
||||
|
||||
190
cursebreaker-parser/src/xml_parser.rs
Normal file
190
cursebreaker-parser/src/xml_parser.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user