loot
This commit is contained in:
@@ -227,6 +227,46 @@ let beginner = harvestable_db.get_by_level_range(1, 10);
|
||||
let advanced = harvestable_db.get_by_level_range(50, 100);
|
||||
```
|
||||
|
||||
## Loading Loot Tables
|
||||
|
||||
```rust
|
||||
use cursebreaker_parser::LootDatabase;
|
||||
|
||||
let loot_db = LootDatabase::load_from_xml("Data/XMLs/Loot/Loot.xml")?;
|
||||
println!("Loaded {} loot tables", loot_db.len());
|
||||
```
|
||||
|
||||
### Querying Loot Tables
|
||||
|
||||
```rust
|
||||
// Get all loot tables for a specific NPC
|
||||
let npc_id = 45;
|
||||
let tables = loot_db.get_tables_for_npc(npc_id);
|
||||
|
||||
// Get all drops for a specific NPC
|
||||
let drops = loot_db.get_drops_for_npc(npc_id);
|
||||
for drop in drops {
|
||||
println!("Item ID: {}, Rate: {:?}", drop.item, drop.rate);
|
||||
}
|
||||
|
||||
// Find which NPCs drop a specific item
|
||||
let item_id = 180;
|
||||
let npcs = loot_db.get_npcs_dropping_item(item_id);
|
||||
println!("Item {} drops from {} NPCs", item_id, npcs.len());
|
||||
|
||||
// Get all tables with conditional drops (checks field)
|
||||
let conditional = loot_db.get_conditional_tables();
|
||||
|
||||
// Get all tables with guaranteed drops (rate = 1)
|
||||
let guaranteed = loot_db.get_tables_with_guaranteed_drops();
|
||||
|
||||
// Get all unique item IDs that can drop
|
||||
let droppable_items = loot_db.get_all_droppable_items();
|
||||
|
||||
// Get all NPCs that have loot tables
|
||||
let npcs_with_loot = loot_db.get_all_npcs_with_loot();
|
||||
```
|
||||
|
||||
## Cross-referencing Data
|
||||
|
||||
```rust
|
||||
@@ -257,6 +297,30 @@ for harvestable in harvestable_db.all_harvestables() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find what items an NPC drops
|
||||
let npc_id = 45;
|
||||
if let Some(npc) = npc_db.get_by_id(npc_id) {
|
||||
let drops = loot_db.get_drops_for_npc(npc_id);
|
||||
println!("NPC '{}' drops {} items:", npc.name, drops.len());
|
||||
for drop in drops {
|
||||
if let Some(item) = item_db.get_by_id(drop.item) {
|
||||
println!(" - {}", item.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find which NPCs drop a specific item
|
||||
let item_id = 180;
|
||||
if let Some(item) = item_db.get_by_id(item_id) {
|
||||
let npcs = loot_db.get_npcs_dropping_item(item_id);
|
||||
println!("Item '{}' drops from:", item.name);
|
||||
for npc_id in npcs {
|
||||
if let Some(npc) = npc_db.get_by_id(npc_id) {
|
||||
println!(" - {}", npc.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Statistics from XML Files
|
||||
@@ -299,6 +363,13 @@ When loaded from `/home/connor/repos/CBAssets/Data/XMLs/`:
|
||||
- **Level 51-100**: 28
|
||||
- **Unique Items from Harvestables**: 98
|
||||
|
||||
### Loot/Loot.xml
|
||||
- **Total Loot Tables**: 175
|
||||
- **NPCs with Loot**: 267
|
||||
- **Droppable Items**: 405
|
||||
- **Tables with Conditional Drops**: 33
|
||||
- **Tables with Guaranteed Drops**: Multiple tables include guaranteed (rate=1) drops
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
@@ -312,12 +383,14 @@ cursebreaker-parser/
|
||||
│ │ ├── npc.rs # NPC data structures
|
||||
│ │ ├── quest.rs # Quest data structures
|
||||
│ │ ├── harvestable.rs # Harvestable data structures
|
||||
│ │ ├── loot.rs # Loot table data structures
|
||||
│ │ └── interactable_resource.rs
|
||||
│ ├── xml_parser.rs # XML parsing logic (all types)
|
||||
│ ├── item_database.rs # ItemDatabase for runtime access
|
||||
│ ├── npc_database.rs # NpcDatabase for runtime access
|
||||
│ ├── quest_database.rs # QuestDatabase for runtime access
|
||||
│ └── harvestable_database.rs # HarvestableDatabase for runtime access
|
||||
│ ├── harvestable_database.rs # HarvestableDatabase for runtime access
|
||||
│ └── loot_database.rs # LootDatabase for runtime access
|
||||
└── examples/
|
||||
├── item_database_demo.rs # Items usage example
|
||||
└── game_data_demo.rs # Full game data example
|
||||
@@ -339,12 +412,12 @@ thiserror = "1.0" # Error handling
|
||||
- ✅ NPCs (`/XMLs/Npcs/NPCInfo.xml`)
|
||||
- ✅ Quests (`/XMLs/Quests/Quests.xml`)
|
||||
- ✅ Harvestables (`/XMLs/Harvestables/HarvestableInfo.xml`)
|
||||
- ✅ Loot tables (`/XMLs/Loot/Loot.xml`)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
The same pattern can be extended to parse other XML files:
|
||||
|
||||
- [ ] Loot tables (`/XMLs/Loot/*.xml`)
|
||||
- [ ] Maps (`/XMLs/Maps/*.xml`)
|
||||
- [ ] Dialogue (`/XMLs/Dialogue/*.xml`)
|
||||
- [ ] Events (`/XMLs/Events/*.xml`)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Run with: cargo run --example game_data_demo
|
||||
|
||||
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase};
|
||||
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, LootDatabase};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🎮 Cursebreaker Game Data Demo\n");
|
||||
@@ -13,11 +13,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let npc_db = NpcDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Npcs/NPCInfo.xml")?;
|
||||
let quest_db = QuestDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Quests/Quests.xml")?;
|
||||
let harvestable_db = HarvestableDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Harvestables/HarvestableInfo.xml")?;
|
||||
let loot_db = LootDatabase::load_from_xml("/home/connor/repos/CBAssets/Data/XMLs/Loot/Loot.xml")?;
|
||||
|
||||
println!("✅ Loaded {} items", item_db.len());
|
||||
println!("✅ Loaded {} NPCs", npc_db.len());
|
||||
println!("✅ Loaded {} quests", quest_db.len());
|
||||
println!("✅ Loaded {} harvestables\n", harvestable_db.len());
|
||||
println!("✅ Loaded {} harvestables", harvestable_db.len());
|
||||
println!("✅ Loaded {} loot tables\n", loot_db.len());
|
||||
|
||||
// =======================================================================
|
||||
// Items
|
||||
@@ -191,6 +193,74 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!(" • Level 51-100: {}", high_level.len());
|
||||
println!();
|
||||
|
||||
// =======================================================================
|
||||
// Loot Tables
|
||||
// =======================================================================
|
||||
println!("=== Loot Tables ===");
|
||||
let all_tables = loot_db.all_tables();
|
||||
let conditional_tables = loot_db.get_conditional_tables();
|
||||
let guaranteed_tables = loot_db.get_tables_with_guaranteed_drops();
|
||||
|
||||
println!("Statistics:");
|
||||
println!(" • Total loot tables: {}", loot_db.len());
|
||||
println!(" • NPCs with loot: {}", loot_db.get_all_npcs_with_loot().len());
|
||||
println!(" • Droppable items: {}", loot_db.get_all_droppable_items().len());
|
||||
println!(" • Tables with conditional drops: {}", conditional_tables.len());
|
||||
println!(" • Tables with guaranteed drops: {}", guaranteed_tables.len());
|
||||
|
||||
// Sample loot table
|
||||
if let Some(table) = all_tables.first() {
|
||||
println!("\nSample loot table:");
|
||||
if let Some(name) = &table.name {
|
||||
println!(" Name: {}", name);
|
||||
}
|
||||
println!(" NPCs: {:?}", table.npc_ids);
|
||||
println!(" Drops: {} items", table.drops.len());
|
||||
|
||||
// Show first few drops
|
||||
println!(" Sample drops:");
|
||||
for drop in table.drops.iter().take(3) {
|
||||
if let Some(item) = item_db.get_by_id(drop.item) {
|
||||
let rate_str = drop.rate.map(|r| r.to_string()).unwrap_or_else(|| "N/A".to_string());
|
||||
let amount_str = if let (Some(min), Some(max)) = (drop.minamount, drop.maxamount) {
|
||||
format!("{}x{}", min, max)
|
||||
} else {
|
||||
"1x1".to_string()
|
||||
};
|
||||
println!(" - {} ({}, rate: {})", item.name, amount_str, rate_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-reference: Find what an NPC drops
|
||||
println!("\nSample NPC drops:");
|
||||
if let Some(npc) = npc_db.get_hostile().first() {
|
||||
println!(" NPC: {} (ID: {})", npc.name, npc.id);
|
||||
let drops = loot_db.get_drops_for_npc(npc.id);
|
||||
if !drops.is_empty() {
|
||||
println!(" Drops {} different items:", drops.len());
|
||||
for drop in drops.iter().take(5) {
|
||||
if let Some(item) = item_db.get_by_id(drop.item) {
|
||||
println!(" - {}", item.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(" No drops configured");
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-reference: Find what NPCs drop an item
|
||||
if let Some(item) = item_db.get_by_id(180) {
|
||||
println!("\nItem '{}' drops from:", item.name);
|
||||
let npcs = loot_db.get_npcs_dropping_item(180);
|
||||
for npc_id in npcs.iter().take(5) {
|
||||
if let Some(npc) = npc_db.get_by_id(*npc_id) {
|
||||
println!(" • {}", npc.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
// =======================================================================
|
||||
// Cross-referencing data
|
||||
// =======================================================================
|
||||
|
||||
@@ -55,15 +55,18 @@ mod item_database;
|
||||
mod npc_database;
|
||||
mod quest_database;
|
||||
mod harvestable_database;
|
||||
mod loot_database;
|
||||
|
||||
pub use item_database::ItemDatabase;
|
||||
pub use npc_database::NpcDatabase;
|
||||
pub use quest_database::QuestDatabase;
|
||||
pub use harvestable_database::HarvestableDatabase;
|
||||
pub use loot_database::LootDatabase;
|
||||
pub use types::{
|
||||
Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule, InteractableResource,
|
||||
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet,
|
||||
Quest, QuestPhase, QuestReward,
|
||||
Harvestable, HarvestableDrop,
|
||||
LootTable, LootDrop,
|
||||
};
|
||||
pub use xml_parser::XmlParseError;
|
||||
|
||||
168
cursebreaker-parser/src/loot_database.rs
Normal file
168
cursebreaker-parser/src/loot_database.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
use crate::types::{LootTable, LootDrop};
|
||||
use crate::xml_parser::{parse_loot_xml, XmlParseError};
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
/// A database for managing Loot Tables loaded from XML files
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LootDatabase {
|
||||
tables: Vec<LootTable>,
|
||||
// Map NPC ID -> list of table indices that apply to this NPC
|
||||
tables_by_npc: HashMap<i32, Vec<usize>>,
|
||||
// Map item ID -> list of table indices that drop this item
|
||||
tables_by_item: HashMap<i32, Vec<usize>>,
|
||||
}
|
||||
|
||||
impl LootDatabase {
|
||||
/// Create a new empty LootDatabase
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tables: Vec::new(),
|
||||
tables_by_npc: HashMap::new(),
|
||||
tables_by_item: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load loot tables from an XML file
|
||||
pub fn load_from_xml<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
|
||||
let tables = parse_loot_xml(path)?;
|
||||
let mut db = Self::new();
|
||||
db.add_tables(tables);
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
/// Add loot tables to the database
|
||||
pub fn add_tables(&mut self, tables: Vec<LootTable>) {
|
||||
for table in tables {
|
||||
let index = self.tables.len();
|
||||
|
||||
// Index by NPC IDs
|
||||
for &npc_id in &table.npc_ids {
|
||||
self.tables_by_npc
|
||||
.entry(npc_id)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(index);
|
||||
}
|
||||
|
||||
// Index by item IDs
|
||||
for drop in &table.drops {
|
||||
self.tables_by_item
|
||||
.entry(drop.item)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(index);
|
||||
}
|
||||
|
||||
self.tables.push(table);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all loot tables that apply to a specific NPC ID
|
||||
pub fn get_tables_for_npc(&self, npc_id: i32) -> Vec<&LootTable> {
|
||||
self.tables_by_npc
|
||||
.get(&npc_id)
|
||||
.map(|indices| {
|
||||
indices
|
||||
.iter()
|
||||
.filter_map(|&idx| self.tables.get(idx))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get all loot tables that drop a specific item
|
||||
pub fn get_tables_with_item(&self, item_id: i32) -> Vec<&LootTable> {
|
||||
self.tables_by_item
|
||||
.get(&item_id)
|
||||
.map(|indices| {
|
||||
indices
|
||||
.iter()
|
||||
.filter_map(|&idx| self.tables.get(idx))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Get all possible drops for a specific NPC
|
||||
pub fn get_drops_for_npc(&self, npc_id: i32) -> Vec<&LootDrop> {
|
||||
let mut drops = Vec::new();
|
||||
for table in self.get_tables_for_npc(npc_id) {
|
||||
drops.extend(&table.drops);
|
||||
}
|
||||
drops
|
||||
}
|
||||
|
||||
/// Get all NPCs that can drop a specific item
|
||||
pub fn get_npcs_dropping_item(&self, item_id: i32) -> Vec<i32> {
|
||||
let mut npcs = std::collections::HashSet::new();
|
||||
|
||||
if let Some(table_indices) = self.tables_by_item.get(&item_id) {
|
||||
for &idx in table_indices {
|
||||
if let Some(table) = self.tables.get(idx) {
|
||||
npcs.extend(&table.npc_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
npcs.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Get all loot tables
|
||||
pub fn all_tables(&self) -> &[LootTable] {
|
||||
&self.tables
|
||||
}
|
||||
|
||||
/// Get tables with conditional drops (that have checks)
|
||||
pub fn get_conditional_tables(&self) -> Vec<&LootTable> {
|
||||
self.tables
|
||||
.iter()
|
||||
.filter(|t| !t.get_conditional_drops().is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get tables with guaranteed drops (rate = 1)
|
||||
pub fn get_tables_with_guaranteed_drops(&self) -> Vec<&LootTable> {
|
||||
self.tables
|
||||
.iter()
|
||||
.filter(|t| !t.get_guaranteed_drops().is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get all unique item IDs that can drop
|
||||
pub fn get_all_droppable_items(&self) -> Vec<i32> {
|
||||
self.tables_by_item.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Get all unique NPC IDs that have loot tables
|
||||
pub fn get_all_npcs_with_loot(&self) -> Vec<i32> {
|
||||
self.tables_by_npc.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Get number of loot tables in database
|
||||
pub fn len(&self) -> usize {
|
||||
self.tables.len()
|
||||
}
|
||||
|
||||
/// Check if database is empty
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.tables.is_empty()
|
||||
}
|
||||
|
||||
/// Prepare loot tables for SQL insertion
|
||||
/// Returns a vector of tuples (npc_ids_json, name, json_data)
|
||||
pub fn prepare_for_sql(&self) -> Vec<(String, Option<String>, String)> {
|
||||
self.tables
|
||||
.iter()
|
||||
.map(|table| {
|
||||
let npc_ids_json = serde_json::to_string(&table.npc_ids).unwrap_or_else(|_| "[]".to_string());
|
||||
let json = serde_json::to_string(table).unwrap_or_else(|_| "{}".to_string());
|
||||
(npc_ids_json, table.name.clone(), json)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LootDatabase {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
//! 3. Extracting typeId and transform positions
|
||||
//! 4. Writing resource data to an output file
|
||||
|
||||
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, InteractableResource};
|
||||
use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, LootDatabase, InteractableResource};
|
||||
use unity_parser::UnityProject;
|
||||
use std::path::Path;
|
||||
use unity_parser::log::DedupLogger;
|
||||
@@ -41,6 +41,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let harvestable_db = HarvestableDatabase::load_from_xml(harvestables_path)?;
|
||||
info!("✅ Loaded {} harvestables", harvestable_db.len());
|
||||
|
||||
let loot_path = "/home/connor/repos/CBAssets/Data/XMLs/Loot/Loot.xml";
|
||||
let loot_db = LootDatabase::load_from_xml(loot_path)?;
|
||||
info!("✅ Loaded {} loot tables", loot_db.len());
|
||||
|
||||
// Print statistics
|
||||
info!("\n📊 Game Data Statistics:");
|
||||
info!(" Items:");
|
||||
@@ -58,6 +62,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
info!(" • Mining: {}", harvestable_db.get_by_skill("mining").len());
|
||||
info!(" • Fishing: {}", harvestable_db.get_by_skill("Fishing").len());
|
||||
info!(" • Alchemy: {}", harvestable_db.get_by_skill("Alchemy").len());
|
||||
info!(" Loot:");
|
||||
info!(" • Total tables: {}", loot_db.len());
|
||||
info!(" • NPCs with loot: {}", loot_db.get_all_npcs_with_loot().len());
|
||||
info!(" • Droppable items: {}", loot_db.get_all_droppable_items().len());
|
||||
info!(" • Tables with conditional drops: {}", loot_db.get_conditional_tables().len());
|
||||
|
||||
// Initialize Unity project once - scans entire project for GUID mappings
|
||||
let project_root = Path::new("/home/connor/repos/CBAssets");
|
||||
|
||||
76
cursebreaker-parser/src/types/loot.rs
Normal file
76
cursebreaker-parser/src/types/loot.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LootTable {
|
||||
// NPC IDs this table applies to (can be multiple, comma-separated in XML)
|
||||
pub npc_ids: Vec<i32>,
|
||||
|
||||
// Optional name/description of the loot table
|
||||
pub name: Option<String>,
|
||||
|
||||
// List of possible drops
|
||||
pub drops: Vec<LootDrop>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LootDrop {
|
||||
// Item ID that can drop
|
||||
pub item: i32,
|
||||
|
||||
// Drop rate (higher = rarer, e.g., rate=1 means common, rate=100 means very rare)
|
||||
pub rate: Option<i32>,
|
||||
|
||||
// Amount range
|
||||
pub minamount: Option<i32>,
|
||||
pub maxamount: Option<i32>,
|
||||
|
||||
// Optional requirements/checks
|
||||
pub checks: Option<String>,
|
||||
|
||||
// Optional comment/description
|
||||
pub comment: Option<String>,
|
||||
}
|
||||
|
||||
impl LootTable {
|
||||
pub fn new(npc_ids: Vec<i32>) -> Self {
|
||||
Self {
|
||||
npc_ids,
|
||||
name: None,
|
||||
drops: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this loot table applies to a given NPC ID
|
||||
pub fn applies_to_npc(&self, npc_id: i32) -> bool {
|
||||
self.npc_ids.contains(&npc_id)
|
||||
}
|
||||
|
||||
/// Get all item IDs that can drop from this table
|
||||
pub fn get_drop_item_ids(&self) -> Vec<i32> {
|
||||
self.drops.iter().map(|d| d.item).collect()
|
||||
}
|
||||
|
||||
/// Get drops that have conditional checks
|
||||
pub fn get_conditional_drops(&self) -> Vec<&LootDrop> {
|
||||
self.drops.iter().filter(|d| d.checks.is_some()).collect()
|
||||
}
|
||||
|
||||
/// Get guaranteed drops (rate = 1)
|
||||
pub fn get_guaranteed_drops(&self) -> Vec<&LootDrop> {
|
||||
self.drops.iter().filter(|d| d.rate == Some(1)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl LootDrop {
|
||||
/// Check if this drop has requirements
|
||||
pub fn has_requirements(&self) -> bool {
|
||||
self.checks.is_some()
|
||||
}
|
||||
|
||||
/// Get the average drop amount
|
||||
pub fn average_amount(&self) -> f32 {
|
||||
let min = self.minamount.unwrap_or(1) as f32;
|
||||
let max = self.maxamount.unwrap_or(1) as f32;
|
||||
(min + max) / 2.0
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,11 @@ mod item;
|
||||
mod npc;
|
||||
mod quest;
|
||||
mod harvestable;
|
||||
mod loot;
|
||||
|
||||
pub use interactable_resource::InteractableResource;
|
||||
pub use item::{Item, ItemStat, CraftingRecipe, AnimationSet, GenerateRule};
|
||||
pub use npc::{Npc, NpcStat, NpcLevel, RightClick, BarkGroup, Bark, QuestMarker, NpcAnimationSet};
|
||||
pub use quest::{Quest, QuestPhase, QuestReward};
|
||||
pub use harvestable::{Harvestable, HarvestableDrop};
|
||||
pub use loot::{LootTable, LootDrop};
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::types::{
|
||||
Npc, NpcStat, NpcLevel, RightClick, BarkGroup, QuestMarker, NpcAnimationSet,
|
||||
Quest, QuestPhase, QuestReward,
|
||||
Harvestable, HarvestableDrop,
|
||||
LootTable, LootDrop,
|
||||
};
|
||||
use quick_xml::events::Event;
|
||||
use quick_xml::reader::Reader;
|
||||
@@ -649,3 +650,86 @@ pub fn parse_harvestables_xml<P: AsRef<Path>>(path: P) -> Result<Vec<Harvestable
|
||||
|
||||
Ok(harvestables)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Loot Parser
|
||||
// ============================================================================
|
||||
|
||||
pub fn parse_loot_xml<P: AsRef<Path>>(path: P) -> Result<Vec<LootTable>, 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 loot_tables = Vec::new();
|
||||
let mut buf = Vec::new();
|
||||
let mut current_table: Option<LootTable> = None;
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
|
||||
match e.name().as_ref() {
|
||||
b"table" => {
|
||||
let attrs = parse_attributes(e)?;
|
||||
|
||||
// Parse npcid - can be comma-separated like "45,459"
|
||||
let npc_ids = if let Some(npcid_str) = attrs.get("npcid") {
|
||||
npcid_str
|
||||
.split(',')
|
||||
.filter_map(|s| s.trim().parse::<i32>().ok())
|
||||
.collect::<Vec<i32>>()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut table = LootTable::new(npc_ids);
|
||||
|
||||
// Parse optional name
|
||||
if let Some(v) = attrs.get("name") {
|
||||
table.name = Some(v.clone());
|
||||
}
|
||||
|
||||
current_table = Some(table);
|
||||
}
|
||||
b"drop" if current_table.is_some() => {
|
||||
if let Some(ref mut table) = current_table {
|
||||
let attrs = parse_attributes(e)?;
|
||||
|
||||
// Parse item ID (required for a drop)
|
||||
if let Some(item_str) = attrs.get("item") {
|
||||
if let Ok(item) = item_str.parse::<i32>() {
|
||||
let drop = LootDrop {
|
||||
item,
|
||||
rate: attrs.get("rate").and_then(|v| v.parse().ok()),
|
||||
minamount: attrs.get("minamount").and_then(|v| v.parse().ok()),
|
||||
maxamount: attrs.get("maxamount").and_then(|v| v.parse().ok()),
|
||||
checks: attrs.get("checks").cloned(),
|
||||
comment: attrs.get("comment").cloned(),
|
||||
};
|
||||
table.drops.push(drop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
match e.name().as_ref() {
|
||||
b"table" => {
|
||||
if let Some(table) = current_table.take() {
|
||||
loot_tables.push(table);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
Err(e) => return Err(XmlParseError::XmlError(e)),
|
||||
_ => {}
|
||||
}
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
Ok(loot_tables)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user