diff --git a/cursebreaker-parser/DATABASE_MIGRATION_GUIDE.md b/cursebreaker-parser/DATABASE_MIGRATION_GUIDE.md deleted file mode 100644 index 11eb0da..0000000 --- a/cursebreaker-parser/DATABASE_MIGRATION_GUIDE.md +++ /dev/null @@ -1,273 +0,0 @@ -# Database Migration Guide - -This guide shows how to update all databases to use actual SQL storage with Diesel instead of just `prepare_for_sql()`. - -## Status - -✅ **Completed**: ItemDatabase -✅ **Completed**: Database tables created (migration) -✅ **Completed**: Main.rs integration example - -⏳ **Remaining**: 9 databases need the same updates - -## Pattern to Follow - -For each database file in `src/databases/`, follow this pattern (using ItemDatabase as the reference): - -### Step 1: Add Diesel Imports - -At the top of the file, add: -```rust -use diesel::prelude::*; -use diesel::sqlite::SqliteConnection; -``` - -### Step 2: Add `save_to_db()` Method - -Replace or add after the `prepare_for_sql()` method: - -```rust -/// Save all [items/npcs/quests/etc] to SQLite database -pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { - use crate::schema::TABLE_NAME; // Replace TABLE_NAME - - let records: Vec<_> = self - .ITEMS_FIELD // Replace with actual field name (e.g., items, npcs, quests) - .iter() - .map(|item| { - let json = serde_json::to_string(item).unwrap_or_else(|_| "{}".to_string()); - ( - TABLE_NAME::id.eq(item.ID_FIELD), // Replace ID_FIELD - TABLE_NAME::name.eq(&item.NAME_FIELD), // Replace NAME_FIELD - TABLE_NAME::data.eq(json), - ) - }) - .collect(); - - let mut count = 0; - for record in records { - diesel::insert_into(TABLE_NAME::table) - .values(&record) - .execute(conn)?; - count += 1; - } - - Ok(count) -} -``` - -### Step 3: Add `load_from_db()` Method - -```rust -/// Load all [items/npcs/quests/etc] from SQLite database -pub fn load_from_db(conn: &mut SqliteConnection) -> Result { - use crate::schema::TABLE_NAME::dsl::*; // Replace TABLE_NAME - - #[derive(Queryable)] - struct Record { - id: Option, // Or Option for text keys - name: String, // Adjust based on schema - data: String, - } - - let records = TABLE_NAME.load::(conn)?; // Replace TABLE_NAME - - let mut loaded_items = Vec::new(); - for record in records { - if let Ok(item) = serde_json::from_str::(&record.data) { // Replace TYPE - loaded_items.push(item); - } - } - - let mut db = Self::new(); - db.add_ITEMS(loaded_items); // Replace add_ITEMS with actual method - Ok(db) -} -``` - -### Step 4: Mark `prepare_for_sql()` as Deprecated - -```rust -#[deprecated(note = "Use save_to_db() to save directly to SQLite database")] -pub fn prepare_for_sql(&self) -> Vec<...> { - // existing implementation -} -``` - -## Database-Specific Mappings - -### Simple Databases (id: i32, name: String, data: String) - -| Database | Table | Items Field | ID Field | Name Field | Type | -|----------|-------|-------------|----------|------------|------| -| NpcDatabase | `npcs` | `npcs` | `type_id` | `npc_name` | `Npc` | -| QuestDatabase | `quests` | `quests` | `id` | `name` | `Quest` | -| HarvestableDatabase | `harvestables` | `harvestables` | `type_id` | `name` | `Harvestable` | - -**Example for NpcDatabase:** -```rust -pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { - use crate::schema::npcs; - - let records: Vec<_> = self - .npcs - .iter() - .map(|npc| { - let json = serde_json::to_string(npc).unwrap_or_else(|_| "{}".to_string()); - ( - npcs::id.eq(npc.type_id), - npcs::name.eq(&npc.npc_name), - npcs::data.eq(json), - ) - }) - .collect(); - - let mut count = 0; - for record in records { - diesel::insert_into(npcs::table) - .values(&record) - .execute(conn)?; - count += 1; - } - - Ok(count) -} -``` - -### Text-Key Databases - -| Database | Table | Primary Key Field | Type | -|----------|-------|-------------------|------| -| LootDatabase | `loot_tables` | `table_id: String` | `LootTable` | -| MapDatabase | `maps` | `scene_id: String` | `Map` | - -**Example for LootDatabase:** -```rust -pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { - use crate::schema::loot_tables; - - let records: Vec<_> = self - .loot_tables // Check actual field name - .iter() - .map(|loot| { - let json = serde_json::to_string(loot).unwrap_or_else(|_| "{}".to_string()); - ( - loot_tables::table_id.eq(&loot.table_id), - loot_tables::npc_id.eq(loot.npc_id.as_ref()), // Optional field - loot_tables::data.eq(json), - ) - }) - .collect(); - - let mut count = 0; - for record in records { - diesel::insert_into(loot_tables::table) - .values(&record) - .execute(conn)?; - count += 1; - } - - Ok(count) -} -``` - -### Complex Databases (Multiple Columns) - -| Database | Table | Additional Columns | Notes | -|----------|-------|-------------------|-------| -| FastTravelDatabase | `fast_travel_locations` | `map_name: String` | Has map reference | -| PlayerHouseDatabase | `player_houses` | `map_id: i32` | Has map ID | -| TraitDatabase | `traits` | `description: Option`, `trainer_id: Option` | Multiple optional fields | -| ShopDatabase | `shops` | `unique_items: bool`, `item_count: usize` | Has metadata columns | - -**Example for ShopDatabase:** -```rust -pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { - use crate::schema::shops; - - let records: Vec<_> = self - .shops - .iter() - .map(|shop| { - let json = serde_json::to_string(shop).unwrap_or_else(|_| "{}".to_string()); - ( - shops::id.eq(shop.id), - shops::name.eq(&shop.name), - shops::unique_items.eq(if shop.unique_items { 1 } else { 0 }), - shops::item_count.eq(shop.items.len() as i32), - shops::data.eq(json), - ) - }) - .collect(); - - let mut count = 0; - for record in records { - diesel::insert_into(shops::table) - .values(&record) - .execute(conn)?; - count += 1; - } - - Ok(count) -} -``` - -## Usage in main.rs - -After loading all databases from XML, save them to SQL: - -```rust -// Establish database connection -let mut conn = SqliteConnection::establish("cursebreaker.db")?; - -// Save each database -match item_db.save_to_db(&mut conn) { - Ok(count) => info!("✅ Saved {} items to database", count), - Err(e) => warn!("⚠️ Failed to save items: {}", e), -} - -match npc_db.save_to_db(&mut conn) { - Ok(count) => info!("✅ Saved {} NPCs to database", count), - Err(e) => warn!("⚠️ Failed to save NPCs: {}", e), -} - -// ... repeat for all databases -``` - -## Testing - -After implementing for each database: - -1. **Build**: `cargo build` - Should compile without errors -2. **Run**: `cargo run` - Should show save confirmations -3. **Verify**: Check `cursebreaker.db` contains data - -## Implementation Order Recommendation - -1. ✅ ItemDatabase (DONE) -2. NpcDatabase (simple, same as items) -3. QuestDatabase (simple, same as items) -4. HarvestableDatabase (simple, same as items) -5. MapDatabase (text key, medium) -6. LootDatabase (text key with optional field, medium) -7. FastTravelDatabase (multiple columns, complex) -8. PlayerHouseDatabase (multiple columns, complex) -9. TraitDatabase (optional columns, complex) -10. ShopDatabase (boolean + count columns, complex) - -## Schema Reference - -The migration created these tables (see `src/schema.rs`): - -- `items(id, name, data)` -- `npcs(id, name, data)` -- `quests(id, name, data)` -- `harvestables(id, name, data)` -- `loot_tables(table_id, npc_id, data)` -- `maps(scene_id, name, data)` -- `fast_travel_locations(id, name, map_name, data)` -- `player_houses(id, name, map_id, data)` -- `traits(id, name, description, trainer_id, data)` -- `shops(id, name, unique_items, item_count, data)` - -All `data` columns store the full JSON-serialized object for complete data preservation. diff --git a/cursebreaker-parser/src/databases/fast_travel_database.rs b/cursebreaker-parser/src/databases/fast_travel_database.rs index 01d31ac..6bbd62c 100644 --- a/cursebreaker-parser/src/databases/fast_travel_database.rs +++ b/cursebreaker-parser/src/databases/fast_travel_database.rs @@ -3,6 +3,8 @@ use crate::xml_parser::{ parse_fast_travel_canoe_xml, parse_fast_travel_locations_xml, parse_fast_travel_portals_xml, XmlParseError, }; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::HashMap; use std::path::Path; @@ -234,8 +236,8 @@ impl FastTravelDatabase { self.locations.is_empty() } - /// Prepare fast travel locations for SQL insertion - /// Returns a vector of tuples (id, name, type, json_data) + /// Prepare fast travel locations for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(i32, String, String, String)> { self.locations .iter() @@ -251,6 +253,61 @@ impl FastTravelDatabase { }) .collect() } + + /// Save all fast travel locations to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::fast_travel_locations; + + let records: Vec<_> = self + .locations + .iter() + .map(|location| { + let json = serde_json::to_string(location).unwrap_or_else(|_| "{}".to_string()); + ( + fast_travel_locations::id.eq(location.id), + fast_travel_locations::name.eq(&location.name), + fast_travel_locations::map_name.eq(""), // TODO: determine actual map name + fast_travel_locations::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(fast_travel_locations::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all fast travel locations from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::fast_travel_locations::dsl::*; + + #[derive(Queryable)] + struct FastTravelLocationRecord { + id: Option, + name: String, + map_name: String, + data: String, + } + + let records = fast_travel_locations.load::(conn)?; + + let mut loaded_locations = Vec::new(); + for record in records { + if let Ok(location) = serde_json::from_str::(&record.data) { + loaded_locations.push(location); + } + } + + let mut db = Self::new(); + db.add_locations(loaded_locations); + Ok(db) + } } impl Default for FastTravelDatabase { diff --git a/cursebreaker-parser/src/databases/harvestable_database.rs b/cursebreaker-parser/src/databases/harvestable_database.rs index b4c2ca1..798f17d 100644 --- a/cursebreaker-parser/src/databases/harvestable_database.rs +++ b/cursebreaker-parser/src/databases/harvestable_database.rs @@ -1,5 +1,7 @@ use crate::types::Harvestable; use crate::xml_parser::{parse_harvestables_xml, XmlParseError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::HashMap; use std::path::Path; @@ -122,8 +124,8 @@ impl HarvestableDatabase { self.harvestables.is_empty() } - /// Prepare harvestables for SQL insertion - /// Returns a vector of tuples (typeid, name, json_data) + /// Prepare harvestables for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> { self.harvestables .iter() @@ -133,6 +135,59 @@ impl HarvestableDatabase { }) .collect() } + + /// Save all harvestables to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::harvestables; + + let records: Vec<_> = self + .harvestables + .iter() + .map(|harvestable| { + let json = serde_json::to_string(harvestable).unwrap_or_else(|_| "{}".to_string()); + ( + harvestables::id.eq(harvestable.typeid), + harvestables::name.eq(&harvestable.name), + harvestables::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(harvestables::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all harvestables from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::harvestables::dsl::*; + + #[derive(Queryable)] + struct HarvestableRecord { + id: Option, + name: String, + data: String, + } + + let records = harvestables.load::(conn)?; + + let mut loaded_harvestables = Vec::new(); + for record in records { + if let Ok(harvestable) = serde_json::from_str::(&record.data) { + loaded_harvestables.push(harvestable); + } + } + + let mut db = Self::new(); + db.add_harvestables(loaded_harvestables); + Ok(db) + } } impl Default for HarvestableDatabase { diff --git a/cursebreaker-parser/src/databases/loot_database.rs b/cursebreaker-parser/src/databases/loot_database.rs index 8d37109..07492f1 100644 --- a/cursebreaker-parser/src/databases/loot_database.rs +++ b/cursebreaker-parser/src/databases/loot_database.rs @@ -1,5 +1,7 @@ use crate::types::{LootTable, LootDrop}; use crate::xml_parser::{parse_loot_xml, XmlParseError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::HashMap; use std::path::Path; @@ -147,8 +149,8 @@ impl LootDatabase { self.tables.is_empty() } - /// Prepare loot tables for SQL insertion - /// Returns a vector of tuples (npc_ids_json, name, json_data) + /// Prepare loot tables for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(String, Option, String)> { self.tables .iter() @@ -159,6 +161,54 @@ impl LootDatabase { }) .collect() } + + /// Save all loot tables to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::loot_tables; + + let mut count = 0; + for table in &self.tables { + let table_id = serde_json::to_string(&table.npc_ids).unwrap_or_else(|_| "[]".to_string()); + let json = serde_json::to_string(table).unwrap_or_else(|_| "{}".to_string()); + let record = ( + loot_tables::table_id.eq(table_id), + loot_tables::npc_id.eq(None::), + loot_tables::data.eq(json), + ); + + diesel::insert_into(loot_tables::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all loot tables from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::loot_tables::dsl::*; + + #[derive(Queryable)] + struct LootTableRecord { + table_id: Option, + npc_id: Option, + data: String, + } + + let records = loot_tables.load::(conn)?; + + let mut loaded_tables = Vec::new(); + for record in records { + if let Ok(table) = serde_json::from_str::(&record.data) { + loaded_tables.push(table); + } + } + + let mut db = Self::new(); + db.add_tables(loaded_tables); + Ok(db) + } } impl Default for LootDatabase { diff --git a/cursebreaker-parser/src/databases/map_database.rs b/cursebreaker-parser/src/databases/map_database.rs index 50c475d..1505053 100644 --- a/cursebreaker-parser/src/databases/map_database.rs +++ b/cursebreaker-parser/src/databases/map_database.rs @@ -1,5 +1,7 @@ use crate::types::Map; use crate::xml_parser::{parse_maps_xml, XmlParseError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::HashMap; use std::path::Path; @@ -176,8 +178,8 @@ impl MapDatabase { self.maps.is_empty() } - /// Prepare maps for SQL insertion - /// Returns a vector of tuples (scene_id, name, json_data) + /// Prepare maps for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(String, String, String)> { self.maps .iter() @@ -187,6 +189,59 @@ impl MapDatabase { }) .collect() } + + /// Save all maps to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::maps; + + let records: Vec<_> = self + .maps + .iter() + .map(|map| { + let json = serde_json::to_string(map).unwrap_or_else(|_| "{}".to_string()); + ( + maps::scene_id.eq(&map.scene_id), + maps::name.eq(&map.name), + maps::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(maps::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all maps from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::maps::dsl::*; + + #[derive(Queryable)] + struct MapRecord { + scene_id: Option, + name: String, + data: String, + } + + let records = maps.load::(conn)?; + + let mut loaded_maps = Vec::new(); + for record in records { + if let Ok(map) = serde_json::from_str::(&record.data) { + loaded_maps.push(map); + } + } + + let mut db = Self::new(); + db.add_maps(loaded_maps); + Ok(db) + } } impl Default for MapDatabase { diff --git a/cursebreaker-parser/src/databases/npc_database.rs b/cursebreaker-parser/src/databases/npc_database.rs index 0c51a7b..74db8ae 100644 --- a/cursebreaker-parser/src/databases/npc_database.rs +++ b/cursebreaker-parser/src/databases/npc_database.rs @@ -1,5 +1,7 @@ use crate::types::Npc; use crate::xml_parser::{parse_npcs_xml, XmlParseError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::HashMap; use std::path::Path; @@ -118,8 +120,8 @@ impl NpcDatabase { self.npcs.is_empty() } - /// Prepare NPCs for SQL insertion - /// Returns a vector of tuples (id, name, json_data) + /// Prepare NPCs for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> { self.npcs .iter() @@ -129,6 +131,59 @@ impl NpcDatabase { }) .collect() } + + /// Save all NPCs to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::npcs; + + let records: Vec<_> = self + .npcs + .iter() + .map(|npc| { + let json = serde_json::to_string(npc).unwrap_or_else(|_| "{}".to_string()); + ( + npcs::id.eq(npc.id), + npcs::name.eq(&npc.name), + npcs::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(npcs::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all NPCs from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::npcs::dsl::*; + + #[derive(Queryable)] + struct NpcRecord { + id: Option, + name: String, + data: String, + } + + let records = npcs.load::(conn)?; + + let mut loaded_npcs = Vec::new(); + for record in records { + if let Ok(npc) = serde_json::from_str::(&record.data) { + loaded_npcs.push(npc); + } + } + + let mut db = Self::new(); + db.add_npcs(loaded_npcs); + Ok(db) + } } impl Default for NpcDatabase { diff --git a/cursebreaker-parser/src/databases/player_house_database.rs b/cursebreaker-parser/src/databases/player_house_database.rs index 86b56a6..03009b2 100644 --- a/cursebreaker-parser/src/databases/player_house_database.rs +++ b/cursebreaker-parser/src/databases/player_house_database.rs @@ -1,5 +1,7 @@ use crate::types::PlayerHouse; use crate::xml_parser::{parse_player_houses_xml, XmlParseError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::HashMap; use std::path::Path; @@ -150,8 +152,8 @@ impl PlayerHouseDatabase { self.houses.is_empty() } - /// Prepare player houses for SQL insertion - /// Returns a vector of tuples (id, name, price, json_data) + /// Prepare player houses for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(i32, String, i32, String)> { self.houses .iter() @@ -161,6 +163,61 @@ impl PlayerHouseDatabase { }) .collect() } + + /// Save all player houses to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::player_houses; + + let records: Vec<_> = self + .houses + .iter() + .map(|house| { + let json = serde_json::to_string(house).unwrap_or_else(|_| "{}".to_string()); + ( + player_houses::id.eq(house.id), + player_houses::name.eq(&house.name), + player_houses::map_id.eq(0), // TODO: determine actual map ID + player_houses::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(player_houses::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all player houses from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::player_houses::dsl::*; + + #[derive(Queryable)] + struct PlayerHouseRecord { + id: Option, + name: String, + map_id: i32, + data: String, + } + + let records = player_houses.load::(conn)?; + + let mut loaded_houses = Vec::new(); + for record in records { + if let Ok(house) = serde_json::from_str::(&record.data) { + loaded_houses.push(house); + } + } + + let mut db = Self::new(); + db.add_houses(loaded_houses); + Ok(db) + } } impl Default for PlayerHouseDatabase { diff --git a/cursebreaker-parser/src/databases/quest_database.rs b/cursebreaker-parser/src/databases/quest_database.rs index 91c7a22..8f75387 100644 --- a/cursebreaker-parser/src/databases/quest_database.rs +++ b/cursebreaker-parser/src/databases/quest_database.rs @@ -1,5 +1,7 @@ use crate::types::Quest; use crate::xml_parser::{parse_quests_xml, XmlParseError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::HashMap; use std::path::Path; @@ -92,8 +94,8 @@ impl QuestDatabase { self.quests.is_empty() } - /// Prepare quests for SQL insertion - /// Returns a vector of tuples (id, name, json_data) + /// Prepare quests for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> { self.quests .iter() @@ -103,6 +105,59 @@ impl QuestDatabase { }) .collect() } + + /// Save all quests to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::quests; + + let records: Vec<_> = self + .quests + .iter() + .map(|quest| { + let json = serde_json::to_string(quest).unwrap_or_else(|_| "{}".to_string()); + ( + quests::id.eq(quest.id), + quests::name.eq(&quest.name), + quests::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(quests::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all quests from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::quests::dsl::*; + + #[derive(Queryable)] + struct QuestRecord { + id: Option, + name: String, + data: String, + } + + let records = quests.load::(conn)?; + + let mut loaded_quests = Vec::new(); + for record in records { + if let Ok(quest) = serde_json::from_str::(&record.data) { + loaded_quests.push(quest); + } + } + + let mut db = Self::new(); + db.add_quests(loaded_quests); + Ok(db) + } } impl Default for QuestDatabase { diff --git a/cursebreaker-parser/src/databases/shop_database.rs b/cursebreaker-parser/src/databases/shop_database.rs index 3a6ffad..5bdadbe 100644 --- a/cursebreaker-parser/src/databases/shop_database.rs +++ b/cursebreaker-parser/src/databases/shop_database.rs @@ -1,5 +1,7 @@ use crate::types::Shop; use crate::xml_parser::{parse_shops_xml, XmlParseError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::HashMap; use std::path::Path; @@ -143,8 +145,8 @@ impl ShopDatabase { self.shops.is_empty() } - /// Prepare shops for SQL insertion - /// Returns a vector of tuples (shop_id, name, is_general_store, item_count, json_data) + /// Prepare shops for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(i32, String, bool, usize, String)> { self.shops .iter() @@ -160,6 +162,63 @@ impl ShopDatabase { }) .collect() } + + /// Save all shops to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::shops; + + let records: Vec<_> = self + .shops + .iter() + .map(|shop| { + let json = serde_json::to_string(shop).unwrap_or_else(|_| "{}".to_string()); + ( + shops::id.eq(shop.shop_id), + shops::name.eq(&shop.name), + shops::unique_items.eq(if shop.is_general_store { 0 } else { 1 }), + shops::item_count.eq(shop.items.len() as i32), + shops::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(shops::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all shops from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::shops::dsl::*; + + #[derive(Queryable)] + struct ShopRecord { + id: Option, + name: String, + unique_items: i32, + item_count: i32, + data: String, + } + + let records = shops.load::(conn)?; + + let mut loaded_shops = Vec::new(); + for record in records { + if let Ok(shop) = serde_json::from_str::(&record.data) { + loaded_shops.push(shop); + } + } + + let mut db = Self::new(); + db.add_shops(loaded_shops); + Ok(db) + } } impl Default for ShopDatabase { diff --git a/cursebreaker-parser/src/databases/trait_database.rs b/cursebreaker-parser/src/databases/trait_database.rs index 9e20f5d..b52e828 100644 --- a/cursebreaker-parser/src/databases/trait_database.rs +++ b/cursebreaker-parser/src/databases/trait_database.rs @@ -1,5 +1,7 @@ use crate::types::Trait; use crate::xml_parser::{parse_traits_xml, XmlParseError}; +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; use std::collections::HashMap; use std::path::Path; @@ -172,8 +174,8 @@ impl TraitDatabase { self.traits.is_empty() } - /// Prepare traits for SQL insertion - /// Returns a vector of tuples (id, name, skill, level, json_data) + /// Prepare traits for SQL insertion (deprecated - use save_to_db instead) + #[deprecated(note = "Use save_to_db() to save directly to SQLite database")] pub fn prepare_for_sql(&self) -> Vec<(i32, String, Option, Option, String)> { self.traits .iter() @@ -186,6 +188,63 @@ impl TraitDatabase { }) .collect() } + + /// Save all traits to SQLite database + pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result { + use crate::schema::traits; + + let records: Vec<_> = self + .traits + .iter() + .map(|trait_obj| { + let json = serde_json::to_string(trait_obj).unwrap_or_else(|_| "{}".to_string()); + ( + traits::id.eq(trait_obj.id), + traits::name.eq(&trait_obj.name), + traits::description.eq(Some(&trait_obj.description)), + traits::trainer_id.eq(None::), // TODO: determine actual trainer ID + traits::data.eq(json), + ) + }) + .collect(); + + let mut count = 0; + for record in records { + diesel::insert_into(traits::table) + .values(&record) + .execute(conn)?; + count += 1; + } + + Ok(count) + } + + /// Load all traits from SQLite database + pub fn load_from_db(conn: &mut SqliteConnection) -> Result { + use crate::schema::traits::dsl::*; + + #[derive(Queryable)] + struct TraitRecord { + id: Option, + name: String, + description: Option, + trainer_id: Option, + data: String, + } + + let records = traits.load::(conn)?; + + let mut loaded_traits = Vec::new(); + for record in records { + if let Ok(trait_obj) = serde_json::from_str::(&record.data) { + loaded_traits.push(trait_obj); + } + } + + let mut db = Self::new(); + db.add_traits(loaded_traits); + Ok(db) + } } impl Default for TraitDatabase { diff --git a/cursebreaker-parser/src/main.rs b/cursebreaker-parser/src/main.rs index 02bdfba..ad37f57 100644 --- a/cursebreaker-parser/src/main.rs +++ b/cursebreaker-parser/src/main.rs @@ -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, LootDatabase, InteractableResource, MinimapDatabase}; +use cursebreaker_parser::{ItemDatabase, NpcDatabase, QuestDatabase, HarvestableDatabase, LootDatabase, MapDatabase, FastTravelDatabase, PlayerHouseDatabase, TraitDatabase, ShopDatabase, InteractableResource, MinimapDatabase}; use unity_parser::UnityProject; use std::path::Path; use unity_parser::log::DedupLogger; @@ -47,6 +47,26 @@ fn main() -> Result<(), Box> { let loot_db = LootDatabase::load_from_xml(loot_path)?; info!("✅ Loaded {} loot tables", loot_db.len()); + let maps_path = "/home/connor/repos/CBAssets/Data/XMLs/Maps/Maps.xml"; + let map_db = MapDatabase::load_from_xml(maps_path)?; + info!("✅ Loaded {} maps", map_db.len()); + + let fast_travel_dir = "/home/connor/repos/CBAssets/Data/XMLs"; + let fast_travel_db = FastTravelDatabase::load_from_directory(fast_travel_dir)?; + info!("✅ Loaded {} fast travel locations", fast_travel_db.len()); + + let player_houses_path = "/home/connor/repos/CBAssets/Data/XMLs/PlayerHouses/PlayerHouses.xml"; + let player_house_db = PlayerHouseDatabase::load_from_xml(player_houses_path)?; + info!("✅ Loaded {} player houses", player_house_db.len()); + + let traits_path = "/home/connor/repos/CBAssets/Data/XMLs/Traits/Traits.xml"; + let trait_db = TraitDatabase::load_from_xml(traits_path)?; + info!("✅ Loaded {} traits", trait_db.len()); + + let shops_path = "/home/connor/repos/CBAssets/Data/XMLs/Shops/Shops.xml"; + let shop_db = ShopDatabase::load_from_xml(shops_path)?; + info!("✅ Loaded {} shops", shop_db.len()); + // Save to SQLite database info!("\n💾 Saving game data to SQLite database..."); let mut conn = SqliteConnection::establish("cursebreaker.db")?; @@ -56,6 +76,51 @@ fn main() -> Result<(), Box> { Err(e) => warn!("⚠️ Failed to save items: {}", e), } + match npc_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} NPCs to database", count), + Err(e) => warn!("⚠️ Failed to save NPCs: {}", e), + } + + match quest_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} quests to database", count), + Err(e) => warn!("⚠️ Failed to save quests: {}", e), + } + + match harvestable_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} harvestables to database", count), + Err(e) => warn!("⚠️ Failed to save harvestables: {}", e), + } + + match loot_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} loot tables to database", count), + Err(e) => warn!("⚠️ Failed to save loot tables: {}", e), + } + + match map_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} maps to database", count), + Err(e) => warn!("⚠️ Failed to save maps: {}", e), + } + + match fast_travel_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} fast travel locations to database", count), + Err(e) => warn!("⚠️ Failed to save fast travel locations: {}", e), + } + + match player_house_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} player houses to database", count), + Err(e) => warn!("⚠️ Failed to save player houses: {}", e), + } + + match trait_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} traits to database", count), + Err(e) => warn!("⚠️ Failed to save traits: {}", e), + } + + match shop_db.save_to_db(&mut conn) { + Ok(count) => info!("✅ Saved {} shops to database", count), + Err(e) => warn!("⚠️ Failed to save shops: {}", e), + } + // Print statistics info!("\n📊 Game Data Statistics:"); info!(" Items:");