npc + achievements
This commit is contained in:
@@ -8,11 +8,12 @@ The parser now supports loading game data from Cursebreaker's XML files and stor
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- ✅ Parse Items.xml with full attribute and nested element support
|
- ✅ Parse Items, NPCs, and Quests XML files with full attribute and nested element support
|
||||||
- ✅ In-memory database with fast lookups by ID, name, category, slot, and skill
|
- ✅ 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
|
||||||
- ✅ Easy-to-use API
|
- ✅ Easy-to-use API with query methods
|
||||||
|
- ✅ Cross-referencing support between different data types
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
@@ -122,18 +123,103 @@ pub struct ItemStat {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Example Program
|
## Example Programs
|
||||||
|
|
||||||
Run the demo to see all features in action:
|
Run the demos to see all features in action:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Items only
|
||||||
cargo run --example item_database_demo
|
cargo run --example item_database_demo
|
||||||
|
|
||||||
|
# All game data (Items, NPCs, Quests)
|
||||||
|
cargo run --example game_data_demo
|
||||||
```
|
```
|
||||||
|
|
||||||
## Statistics from Items.xml
|
## Loading NPCs
|
||||||
|
|
||||||
When loaded from `/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml`:
|
```rust
|
||||||
|
use cursebreaker_parser::NpcDatabase;
|
||||||
|
|
||||||
|
let npc_db = NpcDatabase::load_from_xml("Data/XMLs/Npcs/NPCInfo.xml")?;
|
||||||
|
println!("Loaded {} NPCs", npc_db.len());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Querying NPCs
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Get by ID
|
||||||
|
if let Some(npc) = npc_db.get_by_id(1) {
|
||||||
|
println!("Found: {}", npc.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get hostile NPCs
|
||||||
|
let hostile = npc_db.get_hostile();
|
||||||
|
|
||||||
|
// Get interactable NPCs
|
||||||
|
let interactable = npc_db.get_interactable();
|
||||||
|
|
||||||
|
// Get NPCs by tag
|
||||||
|
let undead = npc_db.get_by_tag("Undead");
|
||||||
|
|
||||||
|
// Get shopkeepers
|
||||||
|
let shops = npc_db.get_shopkeepers();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loading Quests
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use cursebreaker_parser::QuestDatabase;
|
||||||
|
|
||||||
|
let quest_db = QuestDatabase::load_from_xml("Data/XMLs/Quests/Quests.xml")?;
|
||||||
|
println!("Loaded {} quests", quest_db.len());
|
||||||
|
```
|
||||||
|
|
||||||
|
### Querying Quests
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Get by ID
|
||||||
|
if let Some(quest) = quest_db.get_by_id(1) {
|
||||||
|
println!("Quest: {}", quest.name);
|
||||||
|
println!("Phases: {}", quest.phase_count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get main quests
|
||||||
|
let main_quests = quest_db.get_main_quests();
|
||||||
|
|
||||||
|
// Get side quests
|
||||||
|
let side_quests = quest_db.get_side_quests();
|
||||||
|
|
||||||
|
// Get hidden quests
|
||||||
|
let hidden = quest_db.get_hidden_quests();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cross-referencing Data
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Find items rewarded by quests
|
||||||
|
for quest in quest_db.all_quests() {
|
||||||
|
for reward in &quest.rewards {
|
||||||
|
if let Some(item_id) = reward.item {
|
||||||
|
if let Some(item) = item_db.get_by_id(item_id) {
|
||||||
|
println!("Quest '{}' rewards: {}", quest.name, item.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find NPCs that give quests
|
||||||
|
for npc in npc_db.all_npcs() {
|
||||||
|
if !npc.questmarkers.is_empty() {
|
||||||
|
println!("NPC '{}' has {} quest markers", npc.name, npc.questmarkers.len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Statistics from XML Files
|
||||||
|
|
||||||
|
When loaded from `/home/connor/repos/CBAssets/Data/XMLs/`:
|
||||||
|
|
||||||
|
### Items.xml
|
||||||
- **Total Items**: 1,360
|
- **Total Items**: 1,360
|
||||||
- **Weapons**: 166
|
- **Weapons**: 166
|
||||||
- **Armor**: 148
|
- **Armor**: 148
|
||||||
@@ -142,21 +228,41 @@ When loaded from `/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml`:
|
|||||||
- **Bows**: 18
|
- **Bows**: 18
|
||||||
- **Magic Items**: 76
|
- **Magic Items**: 76
|
||||||
|
|
||||||
|
### NPCs/NPCInfo.xml
|
||||||
|
- **Total NPCs**: 1,242
|
||||||
|
- **Hostile NPCs**: 328
|
||||||
|
- **Interactable NPCs**: 512
|
||||||
|
- **Undead**: 71
|
||||||
|
- **Predators**: 13
|
||||||
|
- **Quest Givers**: 108
|
||||||
|
|
||||||
|
### Quests/Quests.xml
|
||||||
|
- **Total Quests**: 108
|
||||||
|
- **Main Quests**: 19
|
||||||
|
- **Side Quests**: 89
|
||||||
|
- **Hidden Quests**: 2
|
||||||
|
- **Unique Quest Reward Items**: 70
|
||||||
|
|
||||||
## File Structure
|
## File Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
cursebreaker-parser/
|
cursebreaker-parser/
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── lib.rs # Library exports
|
│ ├── lib.rs # Library exports
|
||||||
│ ├── main.rs # Main binary (includes Unity + XML parsing)
|
│ ├── main.rs # Main binary (Unity + XML parsing)
|
||||||
│ ├── types/
|
│ ├── types/
|
||||||
│ │ ├── mod.rs
|
│ │ ├── mod.rs
|
||||||
│ │ ├── item.rs # Item data structures
|
│ │ ├── item.rs # Item data structures
|
||||||
|
│ │ ├── npc.rs # NPC data structures
|
||||||
|
│ │ ├── quest.rs # Quest data structures
|
||||||
│ │ └── interactable_resource.rs
|
│ │ └── interactable_resource.rs
|
||||||
│ ├── xml_parser.rs # XML parsing logic
|
│ ├── xml_parser.rs # XML parsing logic (Items, NPCs, Quests)
|
||||||
│ └── item_database.rs # ItemDatabase for runtime access
|
│ ├── item_database.rs # ItemDatabase for runtime access
|
||||||
|
│ ├── npc_database.rs # NpcDatabase for runtime access
|
||||||
|
│ └── quest_database.rs # QuestDatabase for runtime access
|
||||||
└── examples/
|
└── examples/
|
||||||
└── item_database_demo.rs # Full usage example
|
├── item_database_demo.rs # Items usage example
|
||||||
|
└── game_data_demo.rs # Full game data example
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dependencies Added
|
## Dependencies Added
|
||||||
@@ -169,18 +275,25 @@ diesel = { version = "2.2", features = ["sqlite"], optional = true } # SQL (opt
|
|||||||
thiserror = "1.0" # Error handling
|
thiserror = "1.0" # Error handling
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Completed Features
|
||||||
|
|
||||||
|
- ✅ Items (`/XMLs/Items/Items.xml`)
|
||||||
|
- ✅ NPCs (`/XMLs/Npcs/NPCInfo.xml`)
|
||||||
|
- ✅ Quests (`/XMLs/Quests/Quests.xml`)
|
||||||
|
|
||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|
||||||
The same pattern can be extended to parse other XML files:
|
The same pattern can be extended to parse other XML files:
|
||||||
|
|
||||||
- [ ] NPCs (`/XMLs/Npcs/*.xml`)
|
|
||||||
- [ ] Quests (`/XMLs/Quests/*.xml`)
|
|
||||||
- [ ] Loot tables (`/XMLs/Loot/*.xml`)
|
- [ ] Loot tables (`/XMLs/Loot/*.xml`)
|
||||||
- [ ] Maps (`/XMLs/Maps/*.xml`)
|
- [ ] Maps (`/XMLs/Maps/*.xml`)
|
||||||
- [ ] Dialogue (`/XMLs/Dialogue/*.xml`)
|
- [ ] Dialogue (`/XMLs/Dialogue/*.xml`)
|
||||||
- [ ] Events (`/XMLs/Events/*.xml`)
|
- [ ] Events (`/XMLs/Events/*.xml`)
|
||||||
|
- [ ] Achievements (`/XMLs/Achievements/*.xml`)
|
||||||
|
- [ ] Traits (`/XMLs/Traits/*.xml`)
|
||||||
|
- [ ] Shops (`/XMLs/Shops/*.xml`)
|
||||||
|
|
||||||
Each would follow the same pattern:
|
Each follows the same pattern:
|
||||||
1. Define data structures in `src/types/`
|
1. Define data structures in `src/types/`
|
||||||
2. Create parser in `src/xml_parser.rs`
|
2. Create parser in `src/xml_parser.rs`
|
||||||
3. Create database wrapper for runtime access
|
3. Create database wrapper for runtime access
|
||||||
|
|||||||
165
cursebreaker-parser/examples/game_data_demo.rs
Normal file
165
cursebreaker-parser/examples/game_data_demo.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
//! Example demonstrating combined Items, NPCs, and Quests database usage
|
||||||
|
//!
|
||||||
|
//! Run with: cargo run --example game_data_demo
|
||||||
|
|
||||||
|
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🎮 Cursebreaker Game Data Demo\n");
|
||||||
|
|
||||||
|
// Load all game data
|
||||||
|
println!("📚 Loading game data...");
|
||||||
|
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")?;
|
||||||
|
|
||||||
|
println!("✅ Loaded {} items", item_db.len());
|
||||||
|
println!("✅ Loaded {} NPCs", npc_db.len());
|
||||||
|
println!("✅ Loaded {} quests\n", quest_db.len());
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// Items
|
||||||
|
// =======================================================================
|
||||||
|
println!("=== Items ===");
|
||||||
|
let weapons = item_db.get_by_slot("weapon");
|
||||||
|
let armor = item_db.get_by_slot("armor");
|
||||||
|
let consumables = item_db.get_by_slot("consumable");
|
||||||
|
|
||||||
|
println!("By slot:");
|
||||||
|
println!(" • Weapons: {}", weapons.len());
|
||||||
|
println!(" • Armor: {}", armor.len());
|
||||||
|
println!(" • Consumables: {}", consumables.len());
|
||||||
|
|
||||||
|
// Find specific item
|
||||||
|
if let Some(sword) = item_db.get_by_id(150) {
|
||||||
|
println!("\nSample item (ID 150):");
|
||||||
|
println!(" Name: {}", sword.name);
|
||||||
|
if let Some(desc) = &sword.description {
|
||||||
|
println!(" Description: {}", desc);
|
||||||
|
}
|
||||||
|
if let Some(skill) = &sword.skill {
|
||||||
|
println!(" Skill: {}", skill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// NPCs
|
||||||
|
// =======================================================================
|
||||||
|
println!("=== NPCs ===");
|
||||||
|
let hostile = npc_db.get_hostile();
|
||||||
|
let interactable = npc_db.get_interactable();
|
||||||
|
let shopkeepers = npc_db.get_shopkeepers();
|
||||||
|
|
||||||
|
println!("By type:");
|
||||||
|
println!(" • Hostile NPCs: {}", hostile.len());
|
||||||
|
println!(" • Interactable NPCs: {}", interactable.len());
|
||||||
|
println!(" • Shopkeepers: {}", shopkeepers.len());
|
||||||
|
|
||||||
|
// Find NPCs by tag
|
||||||
|
let undead = npc_db.get_by_tag("Undead");
|
||||||
|
let predators = npc_db.get_by_tag("Predator");
|
||||||
|
println!("\nBy tag:");
|
||||||
|
println!(" • Undead: {}", undead.len());
|
||||||
|
println!(" • Predators: {}", predators.len());
|
||||||
|
|
||||||
|
// Sample hostile NPC
|
||||||
|
if let Some(wolf) = npc_db.get_by_id(1) {
|
||||||
|
println!("\nSample hostile NPC (ID 1):");
|
||||||
|
println!(" Name: {}", wolf.name);
|
||||||
|
if let Some(level) = wolf.level {
|
||||||
|
println!(" Level: {}", level);
|
||||||
|
}
|
||||||
|
if let Some(aggro) = wolf.aggrodistance {
|
||||||
|
println!(" Aggro Distance: {}", aggro);
|
||||||
|
}
|
||||||
|
if let Some(speed) = wolf.movementspeed {
|
||||||
|
println!(" Movement Speed: {}", speed);
|
||||||
|
}
|
||||||
|
println!(" Stats: {} stat entries", wolf.stats.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample interactable NPC
|
||||||
|
println!("\nSample shopkeepers:");
|
||||||
|
for shopkeeper in shopkeepers.iter().take(3) {
|
||||||
|
println!(" • {} (Shop ID: {:?})", shopkeeper.name, shopkeeper.shop);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// Quests
|
||||||
|
// =======================================================================
|
||||||
|
println!("=== Quests ===");
|
||||||
|
let main_quests = quest_db.get_main_quests();
|
||||||
|
let side_quests = quest_db.get_side_quests();
|
||||||
|
let hidden_quests = quest_db.get_hidden_quests();
|
||||||
|
|
||||||
|
println!("By type:");
|
||||||
|
println!(" • Main quests: {}", main_quests.len());
|
||||||
|
println!(" • Side quests: {}", side_quests.len());
|
||||||
|
println!(" • Hidden quests: {}", hidden_quests.len());
|
||||||
|
|
||||||
|
// Main quest details
|
||||||
|
println!("\nMain quests:");
|
||||||
|
for quest in main_quests.iter().take(5) {
|
||||||
|
println!(" • {} (ID: {}, {} phases)",
|
||||||
|
quest.name, quest.id, quest.phase_count());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample quest details
|
||||||
|
if let Some(quest) = quest_db.get_by_id(1) {
|
||||||
|
println!("\nSample quest (ID 1):");
|
||||||
|
println!(" Name: {}", quest.name);
|
||||||
|
println!(" Phases: {}", quest.phases.len());
|
||||||
|
println!(" Rewards: {}", quest.rewards.len());
|
||||||
|
|
||||||
|
if let Some(phase) = quest.get_phase(1) {
|
||||||
|
if let Some(desc) = &phase.trackerdescription {
|
||||||
|
println!(" Phase 1: {}", desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !quest.rewards.is_empty() {
|
||||||
|
println!(" Quest rewards:");
|
||||||
|
for reward in &quest.rewards {
|
||||||
|
if let Some(item_id) = reward.item {
|
||||||
|
if let Some(item) = item_db.get_by_id(item_id) {
|
||||||
|
println!(" - {} x{}", item.name, reward.amount.unwrap_or(1));
|
||||||
|
}
|
||||||
|
} else if let Some(skill) = &reward.skill {
|
||||||
|
println!(" - {} XP: {}", skill, reward.xp.unwrap_or(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// =======================================================================
|
||||||
|
// Cross-referencing data
|
||||||
|
// =======================================================================
|
||||||
|
println!("=== Cross-referencing Data ===");
|
||||||
|
|
||||||
|
// Find NPCs that give quests
|
||||||
|
let mut quest_givers = 0;
|
||||||
|
for npc in npc_db.all_npcs() {
|
||||||
|
if !npc.questmarkers.is_empty() {
|
||||||
|
quest_givers += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("NPCs with quest markers: {}", quest_givers);
|
||||||
|
|
||||||
|
// Find items that are quest rewards
|
||||||
|
let mut quest_reward_items = std::collections::HashSet::new();
|
||||||
|
for quest in quest_db.all_quests() {
|
||||||
|
for reward in &quest.rewards {
|
||||||
|
if let Some(item_id) = reward.item {
|
||||||
|
quest_reward_items.insert(item_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("Unique items used as quest rewards: {}", quest_reward_items.len());
|
||||||
|
|
||||||
|
println!("\n✨ Demo complete!");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -52,7 +52,15 @@
|
|||||||
pub mod types;
|
pub mod types;
|
||||||
mod xml_parser;
|
mod xml_parser;
|
||||||
mod item_database;
|
mod item_database;
|
||||||
|
mod npc_database;
|
||||||
|
mod quest_database;
|
||||||
|
|
||||||
pub use item_database::ItemDatabase;
|
pub use item_database::ItemDatabase;
|
||||||
pub use types::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, InteractableResource};
|
pub use npc_database::NpcDatabase;
|
||||||
|
pub use quest_database::QuestDatabase;
|
||||||
|
pub use types::{
|
||||||
|
Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, InteractableResource,
|
||||||
|
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet,
|
||||||
|
Quest, QuestPhase, QuestReward,
|
||||||
|
};
|
||||||
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, InteractableResource};
|
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, 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;
|
||||||
@@ -23,28 +23,31 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
info!("🎮 Cursebreaker - Resource Parser");
|
info!("🎮 Cursebreaker - Resource Parser");
|
||||||
|
|
||||||
// Load items from XML
|
// Load items from XML
|
||||||
info!("📚 Loading items from XML...");
|
info!("📚 Loading game data from XML...");
|
||||||
|
|
||||||
let items_path = "/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml";
|
let items_path = "/home/connor/repos/CBAssets/Data/XMLs/Items/Items.xml";
|
||||||
let item_db = ItemDatabase::load_from_xml(items_path)?;
|
let item_db = ItemDatabase::load_from_xml(items_path)?;
|
||||||
info!("✅ Loaded {} items from XML", item_db.len());
|
info!("✅ Loaded {} items", item_db.len());
|
||||||
|
|
||||||
// Print some item statistics
|
let npcs_path = "/home/connor/repos/CBAssets/Data/XMLs/Npcs/NPCInfo.xml";
|
||||||
let weapons = item_db.get_by_slot("weapon");
|
let npc_db = NpcDatabase::load_from_xml(npcs_path)?;
|
||||||
let consumables = item_db.get_by_slot("consumable");
|
info!("✅ Loaded {} NPCs", npc_db.len());
|
||||||
info!(" • Weapons: {}", weapons.len());
|
|
||||||
info!(" • Consumables: {}", consumables.len());
|
|
||||||
|
|
||||||
// Example: Print first few items
|
let quests_path = "/home/connor/repos/CBAssets/Data/XMLs/Quests/Quests.xml";
|
||||||
info!("\n📦 Sample Items:");
|
let quest_db = QuestDatabase::load_from_xml(quests_path)?;
|
||||||
for item in item_db.all_items().iter().take(5) {
|
info!("✅ Loaded {} quests", quest_db.len());
|
||||||
info!(" ID: {}, Name: \"{}\"", item.id, item.name);
|
|
||||||
if let Some(desc) = &item.description {
|
// Print statistics
|
||||||
info!(" Description: {}", desc);
|
info!("\n📊 Game Data Statistics:");
|
||||||
}
|
info!(" Items:");
|
||||||
if let Some(price) = item.price {
|
info!(" • Weapons: {}", item_db.get_by_slot("weapon").len());
|
||||||
info!(" Price: {}", price);
|
info!(" • Consumables: {}", item_db.get_by_slot("consumable").len());
|
||||||
}
|
info!(" NPCs:");
|
||||||
}
|
info!(" • Hostile: {}", npc_db.get_hostile().len());
|
||||||
|
info!(" • Interactable: {}", npc_db.get_interactable().len());
|
||||||
|
info!(" Quests:");
|
||||||
|
info!(" • Main quests: {}", quest_db.get_main_quests().len());
|
||||||
|
info!(" • Side quests: {}", quest_db.get_side_quests().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");
|
||||||
|
|||||||
138
cursebreaker-parser/src/npc_database.rs
Normal file
138
cursebreaker-parser/src/npc_database.rs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
use crate::types::Npc;
|
||||||
|
use crate::xml_parser::{parse_npcs_xml, XmlParseError};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// A database for managing NPCs loaded from XML files
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct NpcDatabase {
|
||||||
|
npcs: Vec<Npc>,
|
||||||
|
npcs_by_id: HashMap<i32, usize>,
|
||||||
|
npcs_by_name: HashMap<String, Vec<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NpcDatabase {
|
||||||
|
/// Create a new empty NpcDatabase
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
npcs: Vec::new(),
|
||||||
|
npcs_by_id: HashMap::new(),
|
||||||
|
npcs_by_name: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load NPCs from an XML file
|
||||||
|
pub fn load_from_xml<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
|
||||||
|
let npcs = parse_npcs_xml(path)?;
|
||||||
|
let mut db = Self::new();
|
||||||
|
db.add_npcs(npcs);
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add NPCs to the database
|
||||||
|
pub fn add_npcs(&mut self, npcs: Vec<Npc>) {
|
||||||
|
for npc in npcs {
|
||||||
|
let index = self.npcs.len();
|
||||||
|
self.npcs_by_id.insert(npc.id, index);
|
||||||
|
|
||||||
|
self.npcs_by_name
|
||||||
|
.entry(npc.name.clone())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push(index);
|
||||||
|
|
||||||
|
self.npcs.push(npc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get an NPC by ID
|
||||||
|
pub fn get_by_id(&self, id: i32) -> Option<&Npc> {
|
||||||
|
self.npcs_by_id
|
||||||
|
.get(&id)
|
||||||
|
.and_then(|&index| self.npcs.get(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get NPCs by name
|
||||||
|
pub fn get_by_name(&self, name: &str) -> Vec<&Npc> {
|
||||||
|
self.npcs_by_name
|
||||||
|
.get(name)
|
||||||
|
.map(|indices| {
|
||||||
|
indices
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&index| self.npcs.get(index))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all NPCs
|
||||||
|
pub fn all_npcs(&self) -> &[Npc] {
|
||||||
|
&self.npcs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all hostile NPCs (can fight and aggressive)
|
||||||
|
pub fn get_hostile(&self) -> Vec<&Npc> {
|
||||||
|
self.npcs
|
||||||
|
.iter()
|
||||||
|
.filter(|npc| {
|
||||||
|
npc.canfight == Some(1) && npc.aggressive == Some(1)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all interactable NPCs
|
||||||
|
pub fn get_interactable(&self) -> Vec<&Npc> {
|
||||||
|
self.npcs
|
||||||
|
.iter()
|
||||||
|
.filter(|npc| npc.interactable == Some(1))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get NPCs by tag
|
||||||
|
pub fn get_by_tag(&self, tag: &str) -> Vec<&Npc> {
|
||||||
|
self.npcs
|
||||||
|
.iter()
|
||||||
|
.filter(|npc| {
|
||||||
|
npc.tags
|
||||||
|
.as_ref()
|
||||||
|
.map(|tags| tags.split(',').any(|t| t.trim() == tag))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get NPCs that offer shops
|
||||||
|
pub fn get_shopkeepers(&self) -> Vec<&Npc> {
|
||||||
|
self.npcs
|
||||||
|
.iter()
|
||||||
|
.filter(|npc| npc.shop.is_some())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of NPCs in database
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.npcs.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if database is empty
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.npcs.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare NPCs for SQL insertion
|
||||||
|
/// Returns a vector of tuples (id, name, json_data)
|
||||||
|
pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> {
|
||||||
|
self.npcs
|
||||||
|
.iter()
|
||||||
|
.map(|npc| {
|
||||||
|
let json = serde_json::to_string(npc).unwrap_or_else(|_| "{}".to_string());
|
||||||
|
(npc.id, npc.name.clone(), json)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NpcDatabase {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
112
cursebreaker-parser/src/quest_database.rs
Normal file
112
cursebreaker-parser/src/quest_database.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use crate::types::Quest;
|
||||||
|
use crate::xml_parser::{parse_quests_xml, XmlParseError};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// A database for managing Quests loaded from XML files
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct QuestDatabase {
|
||||||
|
quests: Vec<Quest>,
|
||||||
|
quests_by_id: HashMap<i32, usize>,
|
||||||
|
quests_by_name: HashMap<String, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QuestDatabase {
|
||||||
|
/// Create a new empty QuestDatabase
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
quests: Vec::new(),
|
||||||
|
quests_by_id: HashMap::new(),
|
||||||
|
quests_by_name: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load quests from an XML file
|
||||||
|
pub fn load_from_xml<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
|
||||||
|
let quests = parse_quests_xml(path)?;
|
||||||
|
let mut db = Self::new();
|
||||||
|
db.add_quests(quests);
|
||||||
|
Ok(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add quests to the database
|
||||||
|
pub fn add_quests(&mut self, quests: Vec<Quest>) {
|
||||||
|
for quest in quests {
|
||||||
|
let index = self.quests.len();
|
||||||
|
self.quests_by_id.insert(quest.id, index);
|
||||||
|
self.quests_by_name.insert(quest.name.clone(), index);
|
||||||
|
self.quests.push(quest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a quest by ID
|
||||||
|
pub fn get_by_id(&self, id: i32) -> Option<&Quest> {
|
||||||
|
self.quests_by_id
|
||||||
|
.get(&id)
|
||||||
|
.and_then(|&index| self.quests.get(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a quest by name
|
||||||
|
pub fn get_by_name(&self, name: &str) -> Option<&Quest> {
|
||||||
|
self.quests_by_name
|
||||||
|
.get(name)
|
||||||
|
.and_then(|&index| self.quests.get(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all quests
|
||||||
|
pub fn all_quests(&self) -> &[Quest] {
|
||||||
|
&self.quests
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all main quests
|
||||||
|
pub fn get_main_quests(&self) -> Vec<&Quest> {
|
||||||
|
self.quests
|
||||||
|
.iter()
|
||||||
|
.filter(|quest| quest.is_main_quest())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all side quests (non-main quests)
|
||||||
|
pub fn get_side_quests(&self) -> Vec<&Quest> {
|
||||||
|
self.quests
|
||||||
|
.iter()
|
||||||
|
.filter(|quest| !quest.is_main_quest())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all hidden quests
|
||||||
|
pub fn get_hidden_quests(&self) -> Vec<&Quest> {
|
||||||
|
self.quests
|
||||||
|
.iter()
|
||||||
|
.filter(|quest| quest.is_hidden())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get number of quests in database
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.quests.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if database is empty
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.quests.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prepare quests for SQL insertion
|
||||||
|
/// Returns a vector of tuples (id, name, json_data)
|
||||||
|
pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> {
|
||||||
|
self.quests
|
||||||
|
.iter()
|
||||||
|
.map(|quest| {
|
||||||
|
let json = serde_json::to_string(quest).unwrap_or_else(|_| "{}".to_string());
|
||||||
|
(quest.id, quest.name.clone(), json)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for QuestDatabase {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
mod interactable_resource;
|
mod interactable_resource;
|
||||||
mod item;
|
mod item;
|
||||||
|
mod npc;
|
||||||
|
mod quest;
|
||||||
|
|
||||||
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 quest::{Quest, QuestPhase, QuestReward};
|
||||||
|
|||||||
226
cursebreaker-parser/src/types/npc.rs
Normal file
226
cursebreaker-parser/src/types/npc.rs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Npc {
|
||||||
|
// Required fields
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
// Basic attributes
|
||||||
|
pub tags: Option<String>,
|
||||||
|
pub level: Option<i32>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub comment: Option<String>,
|
||||||
|
pub model: Option<String>,
|
||||||
|
|
||||||
|
// Combat attributes
|
||||||
|
pub canfight: Option<i32>,
|
||||||
|
pub aggressive: Option<i32>,
|
||||||
|
pub team: Option<i32>,
|
||||||
|
pub aggrodistance: Option<i32>,
|
||||||
|
pub respawntime: Option<i32>,
|
||||||
|
pub health: Option<i32>,
|
||||||
|
pub mana: Option<i32>,
|
||||||
|
pub accuracy: Option<i32>,
|
||||||
|
pub damagetype: Option<i32>,
|
||||||
|
pub damageblock: Option<i32>,
|
||||||
|
pub ability: Option<i32>,
|
||||||
|
|
||||||
|
// Attack attributes
|
||||||
|
pub attackdistance: Option<i32>,
|
||||||
|
pub attackspeed: Option<i32>,
|
||||||
|
pub attackdelay: Option<i32>,
|
||||||
|
pub gfxattack: Option<String>,
|
||||||
|
|
||||||
|
// Projectile attributes
|
||||||
|
pub projectile: Option<i32>,
|
||||||
|
pub projectilerate: Option<i32>,
|
||||||
|
pub projectileendgfx: Option<String>,
|
||||||
|
pub projectileattackdistance: Option<i32>,
|
||||||
|
|
||||||
|
// Movement
|
||||||
|
pub movementspeed: Option<i32>,
|
||||||
|
pub walkspeed: Option<i32>,
|
||||||
|
pub wandering: Option<i32>,
|
||||||
|
pub wanderingdistance: Option<i32>,
|
||||||
|
|
||||||
|
// AI behavior
|
||||||
|
pub aibehaviour: Option<i32>,
|
||||||
|
pub nobestiary: Option<i32>,
|
||||||
|
|
||||||
|
// Interaction
|
||||||
|
pub interactable: Option<i32>,
|
||||||
|
pub interactdistance: Option<i32>,
|
||||||
|
pub dontrotateoninteract: Option<i32>,
|
||||||
|
pub shop: Option<i32>,
|
||||||
|
|
||||||
|
// Sound effects
|
||||||
|
pub sfxattack: Option<String>,
|
||||||
|
pub sfxdeath: Option<String>,
|
||||||
|
pub sfxtakehit: Option<String>,
|
||||||
|
pub sfxidle: Option<String>,
|
||||||
|
pub idlesoundtext: Option<String>,
|
||||||
|
|
||||||
|
// Animations
|
||||||
|
pub anim_attack: Option<String>,
|
||||||
|
pub anim_death: Option<String>,
|
||||||
|
pub anim_idle: Option<String>,
|
||||||
|
pub anim_run: Option<String>,
|
||||||
|
pub anim_walk: Option<String>,
|
||||||
|
pub anim_takehit: Option<String>,
|
||||||
|
pub startanim: Option<String>,
|
||||||
|
|
||||||
|
// Nested elements
|
||||||
|
pub stats: Vec<NpcStat>,
|
||||||
|
pub levels: Vec<NpcLevel>,
|
||||||
|
pub rightclick: Option<RightClick>,
|
||||||
|
pub barks: Vec<BarkGroup>,
|
||||||
|
pub exitdialoguebarks: Vec<BarkGroup>,
|
||||||
|
pub questmarkers: Vec<QuestMarker>,
|
||||||
|
pub animations: Option<NpcAnimationSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NpcStat {
|
||||||
|
// 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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NpcLevel {
|
||||||
|
pub swordsmanship: Option<i32>,
|
||||||
|
pub archery: Option<i32>,
|
||||||
|
pub magic: Option<i32>,
|
||||||
|
pub defence: Option<i32>,
|
||||||
|
pub mining: Option<i32>,
|
||||||
|
pub woodcutting: Option<i32>,
|
||||||
|
pub fishing: Option<i32>,
|
||||||
|
pub cooking: Option<i32>,
|
||||||
|
pub carpentry: Option<i32>,
|
||||||
|
pub blacksmithy: Option<i32>,
|
||||||
|
pub tailoring: Option<i32>,
|
||||||
|
pub alchemy: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct RightClick {
|
||||||
|
pub option: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct BarkGroup {
|
||||||
|
pub cooldown: Option<i32>,
|
||||||
|
pub rate: Option<i32>,
|
||||||
|
pub range: Option<i32>,
|
||||||
|
pub checks: Option<String>,
|
||||||
|
pub npcs: Option<String>,
|
||||||
|
pub barks: Vec<Bark>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Bark {
|
||||||
|
pub text: String,
|
||||||
|
pub pausetime: Option<i32>,
|
||||||
|
pub rate: Option<i32>,
|
||||||
|
pub anim: Option<String>,
|
||||||
|
pub npc: Option<String>,
|
||||||
|
pub dontrotate: Option<i32>,
|
||||||
|
pub dontrotateothers: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct QuestMarker {
|
||||||
|
pub id: i32,
|
||||||
|
pub phase: i32,
|
||||||
|
pub checks: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct NpcAnimationSet {
|
||||||
|
pub idle: Option<String>,
|
||||||
|
pub walk: Option<String>,
|
||||||
|
pub run: Option<String>,
|
||||||
|
pub attack: Option<String>,
|
||||||
|
pub death: Option<String>,
|
||||||
|
pub talk: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Npc {
|
||||||
|
pub fn new(id: i32, name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
tags: None,
|
||||||
|
level: None,
|
||||||
|
description: None,
|
||||||
|
comment: None,
|
||||||
|
model: None,
|
||||||
|
canfight: None,
|
||||||
|
aggressive: None,
|
||||||
|
team: None,
|
||||||
|
aggrodistance: None,
|
||||||
|
respawntime: None,
|
||||||
|
health: None,
|
||||||
|
mana: None,
|
||||||
|
accuracy: None,
|
||||||
|
damagetype: None,
|
||||||
|
damageblock: None,
|
||||||
|
ability: None,
|
||||||
|
attackdistance: None,
|
||||||
|
attackspeed: None,
|
||||||
|
attackdelay: None,
|
||||||
|
gfxattack: None,
|
||||||
|
projectile: None,
|
||||||
|
projectilerate: None,
|
||||||
|
projectileendgfx: None,
|
||||||
|
projectileattackdistance: None,
|
||||||
|
movementspeed: None,
|
||||||
|
walkspeed: None,
|
||||||
|
wandering: None,
|
||||||
|
wanderingdistance: None,
|
||||||
|
aibehaviour: None,
|
||||||
|
nobestiary: None,
|
||||||
|
interactable: None,
|
||||||
|
interactdistance: None,
|
||||||
|
dontrotateoninteract: None,
|
||||||
|
shop: None,
|
||||||
|
sfxattack: None,
|
||||||
|
sfxdeath: None,
|
||||||
|
sfxtakehit: None,
|
||||||
|
sfxidle: None,
|
||||||
|
idlesoundtext: None,
|
||||||
|
anim_attack: None,
|
||||||
|
anim_death: None,
|
||||||
|
anim_idle: None,
|
||||||
|
anim_run: None,
|
||||||
|
anim_walk: None,
|
||||||
|
anim_takehit: None,
|
||||||
|
startanim: None,
|
||||||
|
stats: Vec::new(),
|
||||||
|
levels: Vec::new(),
|
||||||
|
rightclick: None,
|
||||||
|
barks: Vec::new(),
|
||||||
|
exitdialoguebarks: Vec::new(),
|
||||||
|
questmarkers: Vec::new(),
|
||||||
|
animations: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
77
cursebreaker-parser/src/types/quest.rs
Normal file
77
cursebreaker-parser/src/types/quest.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Quest {
|
||||||
|
// Required fields
|
||||||
|
pub id: i32,
|
||||||
|
pub name: String,
|
||||||
|
|
||||||
|
// Optional attributes
|
||||||
|
pub mainquest: Option<i32>,
|
||||||
|
pub hidden: Option<i32>,
|
||||||
|
pub questdescription: Option<String>,
|
||||||
|
pub completiontext: Option<String>,
|
||||||
|
pub dontshowcompletionscreen: Option<i32>,
|
||||||
|
pub comment: Option<String>,
|
||||||
|
|
||||||
|
// Nested elements
|
||||||
|
pub phases: Vec<QuestPhase>,
|
||||||
|
pub rewards: Vec<QuestReward>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct QuestPhase {
|
||||||
|
pub id: i32,
|
||||||
|
pub trackerdescription: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub helperarrownpc: Option<String>,
|
||||||
|
pub helperarrowpos: Option<String>,
|
||||||
|
pub checks: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct QuestReward {
|
||||||
|
pub item: Option<i32>,
|
||||||
|
pub skill: Option<String>,
|
||||||
|
pub amount: Option<i32>,
|
||||||
|
pub xp: Option<i32>,
|
||||||
|
pub checks: Option<String>,
|
||||||
|
pub comment: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Quest {
|
||||||
|
pub fn new(id: i32, name: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
mainquest: None,
|
||||||
|
hidden: None,
|
||||||
|
questdescription: None,
|
||||||
|
completiontext: None,
|
||||||
|
dontshowcompletionscreen: None,
|
||||||
|
comment: None,
|
||||||
|
phases: Vec::new(),
|
||||||
|
rewards: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this is a main quest
|
||||||
|
pub fn is_main_quest(&self) -> bool {
|
||||||
|
self.mainquest == Some(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if this quest is hidden
|
||||||
|
pub fn is_hidden(&self) -> bool {
|
||||||
|
self.hidden == Some(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of phases in this quest
|
||||||
|
pub fn phase_count(&self) -> usize {
|
||||||
|
self.phases.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a specific phase by ID
|
||||||
|
pub fn get_phase(&self, phase_id: i32) -> Option<&QuestPhase> {
|
||||||
|
self.phases.iter().find(|p| p.id == phase_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
use crate::types::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule};
|
use crate::types::{
|
||||||
|
Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule,
|
||||||
|
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet,
|
||||||
|
Quest, QuestPhase, QuestReward,
|
||||||
|
};
|
||||||
use quick_xml::events::Event;
|
use quick_xml::events::Event;
|
||||||
use quick_xml::reader::Reader;
|
use quick_xml::reader::Reader;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -188,3 +192,341 @@ fn parse_stat(attrs: &HashMap<String, String>) -> ItemStat {
|
|||||||
harvestingspeedwoodcutting: attrs.get("harvestingspeedwoodcutting").and_then(|v| v.parse().ok()),
|
harvestingspeedwoodcutting: attrs.get("harvestingspeedwoodcutting").and_then(|v| v.parse().ok()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// NPC Parser
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
pub fn parse_npcs_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Npc>, 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 npcs = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_npc: Option<Npc> = None;
|
||||||
|
let mut current_bark_group: Option<BarkGroup> = None;
|
||||||
|
let mut in_exitdialoguebarks = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"npc" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
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").cloned().unwrap_or_default();
|
||||||
|
let mut npc = Npc::new(id, name);
|
||||||
|
|
||||||
|
// Parse all optional attributes
|
||||||
|
if let Some(v) = attrs.get("tags") { npc.tags = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("level") { npc.level = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("description") { npc.description = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("comment") { npc.comment = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("model") { npc.model = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("canfight") { npc.canfight = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("aggressive") { npc.aggressive = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("team") { npc.team = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("aggrodistance") { npc.aggrodistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("respawntime") { npc.respawntime = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("health") { npc.health = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("mana") { npc.mana = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("accuracy") { npc.accuracy = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("damagetype") { npc.damagetype = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("damageblock") { npc.damageblock = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("ability") { npc.ability = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("attackdistance") { npc.attackdistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("attackspeed") { npc.attackspeed = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("attackdelay") { npc.attackdelay = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("gfxattack") { npc.gfxattack = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("projectile") { npc.projectile = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("projectilerate") { npc.projectilerate = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("projectileendgfx") { npc.projectileendgfx = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("projectileattackdistance") { npc.projectileattackdistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("movementspeed") { npc.movementspeed = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("walkspeed") { npc.walkspeed = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("wandering") { npc.wandering = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("wanderingdistance") { npc.wanderingdistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("aibehaviour") { npc.aibehaviour = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("nobestiary") { npc.nobestiary = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("interactable") { npc.interactable = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("interactdistance") { npc.interactdistance = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("dontrotateoninteract") { npc.dontrotateoninteract = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("shop") { npc.shop = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("sfxattack") { npc.sfxattack = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("sfxdeath") { npc.sfxdeath = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("sfxtakehit") { npc.sfxtakehit = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("sfxidle") { npc.sfxidle = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("idlesoundtext") { npc.idlesoundtext = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_attack") { npc.anim_attack = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_death") { npc.anim_death = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_idle") { npc.anim_idle = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_run") { npc.anim_run = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_walk") { npc.anim_walk = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("anim_takehit") { npc.anim_takehit = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("startanim") { npc.startanim = Some(v.clone()); }
|
||||||
|
|
||||||
|
current_npc = Some(npc);
|
||||||
|
}
|
||||||
|
b"stat" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
let stat = parse_npc_stat(&attrs);
|
||||||
|
npc.stats.push(stat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"level" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
let level = parse_npc_level(&attrs);
|
||||||
|
npc.levels.push(level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"rightclick" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
if let Some(option) = attrs.get("option") {
|
||||||
|
npc.rightclick = Some(RightClick { option: option.clone() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"questmarker" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
if let (Some(id), Some(phase)) = (attrs.get("id"), attrs.get("phase")) {
|
||||||
|
if let (Ok(id), Ok(phase)) = (id.parse::<i32>(), phase.parse::<i32>()) {
|
||||||
|
npc.questmarkers.push(QuestMarker {
|
||||||
|
id,
|
||||||
|
phase,
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"barks" if current_npc.is_some() => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
current_bark_group = Some(BarkGroup {
|
||||||
|
cooldown: attrs.get("cooldown").and_then(|v| v.parse().ok()),
|
||||||
|
rate: attrs.get("rate").and_then(|v| v.parse().ok()),
|
||||||
|
range: attrs.get("range").and_then(|v| v.parse().ok()),
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
npcs: attrs.get("npcs").cloned(),
|
||||||
|
barks: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
b"exitdialoguebarks" if current_npc.is_some() => {
|
||||||
|
in_exitdialoguebarks = true;
|
||||||
|
current_bark_group = Some(BarkGroup {
|
||||||
|
cooldown: None,
|
||||||
|
rate: None,
|
||||||
|
range: None,
|
||||||
|
checks: None,
|
||||||
|
npcs: None,
|
||||||
|
barks: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
b"anim" if current_npc.is_some() => {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
npc.animations = Some(NpcAnimationSet {
|
||||||
|
idle: attrs.get("idle").cloned(),
|
||||||
|
walk: attrs.get("walk").cloned(),
|
||||||
|
run: attrs.get("run").cloned(),
|
||||||
|
attack: attrs.get("attack").cloned(),
|
||||||
|
death: attrs.get("death").cloned(),
|
||||||
|
talk: attrs.get("talk").cloned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Text(e)) => {
|
||||||
|
// Handle bark text content
|
||||||
|
if current_bark_group.is_some() {
|
||||||
|
let text = e.unescape()?.to_string().trim().to_string();
|
||||||
|
if !text.is_empty() {
|
||||||
|
// Text will be added to bark when we hit the end tag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"npc" => {
|
||||||
|
if let Some(npc) = current_npc.take() {
|
||||||
|
npcs.push(npc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"barks" => {
|
||||||
|
if let Some(bark_group) = current_bark_group.take() {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
if in_exitdialoguebarks {
|
||||||
|
npc.exitdialoguebarks.push(bark_group);
|
||||||
|
} else {
|
||||||
|
npc.barks.push(bark_group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"exitdialoguebarks" => {
|
||||||
|
in_exitdialoguebarks = false;
|
||||||
|
if let Some(bark_group) = current_bark_group.take() {
|
||||||
|
if let Some(ref mut npc) = current_npc {
|
||||||
|
npc.exitdialoguebarks.push(bark_group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(npcs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_npc_stat(attrs: &HashMap<String, String>) -> NpcStat {
|
||||||
|
NpcStat {
|
||||||
|
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").or_else(|| attrs.get("resistancePhysical")).and_then(|v| v.parse().ok()),
|
||||||
|
resistancemagical: attrs.get("resistancemagical").or_else(|| attrs.get("resistanceMagical")).and_then(|v| v.parse().ok()),
|
||||||
|
resistanceranged: attrs.get("resistanceranged").or_else(|| 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_npc_level(attrs: &HashMap<String, String>) -> NpcLevel {
|
||||||
|
NpcLevel {
|
||||||
|
swordsmanship: attrs.get("swordsmanship").and_then(|v| v.parse().ok()),
|
||||||
|
archery: attrs.get("archery").and_then(|v| v.parse().ok()),
|
||||||
|
magic: attrs.get("magic").and_then(|v| v.parse().ok()),
|
||||||
|
defence: attrs.get("defence").and_then(|v| v.parse().ok()),
|
||||||
|
mining: attrs.get("mining").and_then(|v| v.parse().ok()),
|
||||||
|
woodcutting: attrs.get("woodcutting").and_then(|v| v.parse().ok()),
|
||||||
|
fishing: attrs.get("fishing").and_then(|v| v.parse().ok()),
|
||||||
|
cooking: attrs.get("cooking").and_then(|v| v.parse().ok()),
|
||||||
|
carpentry: attrs.get("carpentry").and_then(|v| v.parse().ok()),
|
||||||
|
blacksmithy: attrs.get("blacksmithy").and_then(|v| v.parse().ok()),
|
||||||
|
tailoring: attrs.get("tailoring").and_then(|v| v.parse().ok()),
|
||||||
|
alchemy: attrs.get("alchemy").and_then(|v| v.parse().ok()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Quest Parser
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
pub fn parse_quests_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Quest>, 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 quests = Vec::new();
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
let mut current_quest: Option<Quest> = None;
|
||||||
|
let mut in_rewards = false;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match reader.read_event_into(&mut buf) {
|
||||||
|
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"quest" => {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
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 quest = Quest::new(id, name);
|
||||||
|
|
||||||
|
// Parse optional attributes
|
||||||
|
if let Some(v) = attrs.get("mainquest") { quest.mainquest = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("hidden") { quest.hidden = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("questdescription") { quest.questdescription = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("completiontext") { quest.completiontext = Some(v.clone()); }
|
||||||
|
if let Some(v) = attrs.get("dontshowcompletionscreen") { quest.dontshowcompletionscreen = v.parse().ok(); }
|
||||||
|
if let Some(v) = attrs.get("comment") { quest.comment = Some(v.clone()); }
|
||||||
|
|
||||||
|
current_quest = Some(quest);
|
||||||
|
}
|
||||||
|
b"phase" if current_quest.is_some() => {
|
||||||
|
if let Some(ref mut quest) = current_quest {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
if let Some(id) = attrs.get("id") {
|
||||||
|
if let Ok(id) = id.parse::<i32>() {
|
||||||
|
quest.phases.push(QuestPhase {
|
||||||
|
id,
|
||||||
|
trackerdescription: attrs.get("trackerdescription").cloned(),
|
||||||
|
description: attrs.get("description").cloned(),
|
||||||
|
helperarrownpc: attrs.get("helperarrownpc").cloned(),
|
||||||
|
helperarrowpos: attrs.get("helperarrowpos").cloned(),
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"rewards" if current_quest.is_some() => {
|
||||||
|
in_rewards = true;
|
||||||
|
}
|
||||||
|
b"reward" if current_quest.is_some() && in_rewards => {
|
||||||
|
if let Some(ref mut quest) = current_quest {
|
||||||
|
let attrs = parse_attributes(e)?;
|
||||||
|
quest.rewards.push(QuestReward {
|
||||||
|
item: attrs.get("item").and_then(|v| v.parse().ok()),
|
||||||
|
skill: attrs.get("skill").cloned(),
|
||||||
|
amount: attrs.get("amount").and_then(|v| v.parse().ok()),
|
||||||
|
xp: attrs.get("xp").and_then(|v| v.parse().ok()),
|
||||||
|
checks: attrs.get("checks").cloned(),
|
||||||
|
comment: attrs.get("comment").cloned(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::End(ref e)) => {
|
||||||
|
match e.name().as_ref() {
|
||||||
|
b"quest" => {
|
||||||
|
if let Some(quest) = current_quest.take() {
|
||||||
|
quests.push(quest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b"rewards" => {
|
||||||
|
in_rewards = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Event::Eof) => break,
|
||||||
|
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(quests)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user