database migration

This commit is contained in:
2026-01-10 09:22:58 +00:00
parent 389327b6dd
commit 580cecbfce
11 changed files with 586 additions and 292 deletions

View File

@@ -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<usize, diesel::result::Error> {
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<Self, diesel::result::Error> {
use crate::schema::TABLE_NAME::dsl::*; // Replace TABLE_NAME
#[derive(Queryable)]
struct Record {
id: Option<i32>, // Or Option<String> for text keys
name: String, // Adjust based on schema
data: String,
}
let records = TABLE_NAME.load::<Record>(conn)?; // Replace TABLE_NAME
let mut loaded_items = Vec::new();
for record in records {
if let Ok(item) = serde_json::from_str::<TYPE>(&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<usize, diesel::result::Error> {
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<usize, diesel::result::Error> {
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<String>`, `trainer_id: Option<i32>` | 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<usize, diesel::result::Error> {
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.

View File

@@ -3,6 +3,8 @@ use crate::xml_parser::{
parse_fast_travel_canoe_xml, parse_fast_travel_locations_xml, parse_fast_travel_portals_xml, parse_fast_travel_canoe_xml, parse_fast_travel_locations_xml, parse_fast_travel_portals_xml,
XmlParseError, XmlParseError,
}; };
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@@ -234,8 +236,8 @@ impl FastTravelDatabase {
self.locations.is_empty() self.locations.is_empty()
} }
/// Prepare fast travel locations for SQL insertion /// Prepare fast travel locations for SQL insertion (deprecated - use save_to_db instead)
/// Returns a vector of tuples (id, name, type, json_data) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
pub fn prepare_for_sql(&self) -> Vec<(i32, String, String, String)> { pub fn prepare_for_sql(&self) -> Vec<(i32, String, String, String)> {
self.locations self.locations
.iter() .iter()
@@ -251,6 +253,61 @@ impl FastTravelDatabase {
}) })
.collect() .collect()
} }
/// Save all fast travel locations to SQLite database
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
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<Self, diesel::result::Error> {
use crate::schema::fast_travel_locations::dsl::*;
#[derive(Queryable)]
struct FastTravelLocationRecord {
id: Option<i32>,
name: String,
map_name: String,
data: String,
}
let records = fast_travel_locations.load::<FastTravelLocationRecord>(conn)?;
let mut loaded_locations = Vec::new();
for record in records {
if let Ok(location) = serde_json::from_str::<FastTravelLocation>(&record.data) {
loaded_locations.push(location);
}
}
let mut db = Self::new();
db.add_locations(loaded_locations);
Ok(db)
}
} }
impl Default for FastTravelDatabase { impl Default for FastTravelDatabase {

View File

@@ -1,5 +1,7 @@
use crate::types::Harvestable; use crate::types::Harvestable;
use crate::xml_parser::{parse_harvestables_xml, XmlParseError}; use crate::xml_parser::{parse_harvestables_xml, XmlParseError};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@@ -122,8 +124,8 @@ impl HarvestableDatabase {
self.harvestables.is_empty() self.harvestables.is_empty()
} }
/// Prepare harvestables for SQL insertion /// Prepare harvestables for SQL insertion (deprecated - use save_to_db instead)
/// Returns a vector of tuples (typeid, name, json_data) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> { pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> {
self.harvestables self.harvestables
.iter() .iter()
@@ -133,6 +135,59 @@ impl HarvestableDatabase {
}) })
.collect() .collect()
} }
/// Save all harvestables to SQLite database
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
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<Self, diesel::result::Error> {
use crate::schema::harvestables::dsl::*;
#[derive(Queryable)]
struct HarvestableRecord {
id: Option<i32>,
name: String,
data: String,
}
let records = harvestables.load::<HarvestableRecord>(conn)?;
let mut loaded_harvestables = Vec::new();
for record in records {
if let Ok(harvestable) = serde_json::from_str::<Harvestable>(&record.data) {
loaded_harvestables.push(harvestable);
}
}
let mut db = Self::new();
db.add_harvestables(loaded_harvestables);
Ok(db)
}
} }
impl Default for HarvestableDatabase { impl Default for HarvestableDatabase {

View File

@@ -1,5 +1,7 @@
use crate::types::{LootTable, LootDrop}; use crate::types::{LootTable, LootDrop};
use crate::xml_parser::{parse_loot_xml, XmlParseError}; use crate::xml_parser::{parse_loot_xml, XmlParseError};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@@ -147,8 +149,8 @@ impl LootDatabase {
self.tables.is_empty() self.tables.is_empty()
} }
/// Prepare loot tables for SQL insertion /// Prepare loot tables for SQL insertion (deprecated - use save_to_db instead)
/// Returns a vector of tuples (npc_ids_json, name, json_data) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
pub fn prepare_for_sql(&self) -> Vec<(String, Option<String>, String)> { pub fn prepare_for_sql(&self) -> Vec<(String, Option<String>, String)> {
self.tables self.tables
.iter() .iter()
@@ -159,6 +161,54 @@ impl LootDatabase {
}) })
.collect() .collect()
} }
/// Save all loot tables to SQLite database
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
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::<String>),
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<Self, diesel::result::Error> {
use crate::schema::loot_tables::dsl::*;
#[derive(Queryable)]
struct LootTableRecord {
table_id: Option<String>,
npc_id: Option<String>,
data: String,
}
let records = loot_tables.load::<LootTableRecord>(conn)?;
let mut loaded_tables = Vec::new();
for record in records {
if let Ok(table) = serde_json::from_str::<LootTable>(&record.data) {
loaded_tables.push(table);
}
}
let mut db = Self::new();
db.add_tables(loaded_tables);
Ok(db)
}
} }
impl Default for LootDatabase { impl Default for LootDatabase {

View File

@@ -1,5 +1,7 @@
use crate::types::Map; use crate::types::Map;
use crate::xml_parser::{parse_maps_xml, XmlParseError}; use crate::xml_parser::{parse_maps_xml, XmlParseError};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@@ -176,8 +178,8 @@ impl MapDatabase {
self.maps.is_empty() self.maps.is_empty()
} }
/// Prepare maps for SQL insertion /// Prepare maps for SQL insertion (deprecated - use save_to_db instead)
/// Returns a vector of tuples (scene_id, name, json_data) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
pub fn prepare_for_sql(&self) -> Vec<(String, String, String)> { pub fn prepare_for_sql(&self) -> Vec<(String, String, String)> {
self.maps self.maps
.iter() .iter()
@@ -187,6 +189,59 @@ impl MapDatabase {
}) })
.collect() .collect()
} }
/// Save all maps to SQLite database
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
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<Self, diesel::result::Error> {
use crate::schema::maps::dsl::*;
#[derive(Queryable)]
struct MapRecord {
scene_id: Option<String>,
name: String,
data: String,
}
let records = maps.load::<MapRecord>(conn)?;
let mut loaded_maps = Vec::new();
for record in records {
if let Ok(map) = serde_json::from_str::<Map>(&record.data) {
loaded_maps.push(map);
}
}
let mut db = Self::new();
db.add_maps(loaded_maps);
Ok(db)
}
} }
impl Default for MapDatabase { impl Default for MapDatabase {

View File

@@ -1,5 +1,7 @@
use crate::types::Npc; use crate::types::Npc;
use crate::xml_parser::{parse_npcs_xml, XmlParseError}; use crate::xml_parser::{parse_npcs_xml, XmlParseError};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@@ -118,8 +120,8 @@ impl NpcDatabase {
self.npcs.is_empty() self.npcs.is_empty()
} }
/// Prepare NPCs for SQL insertion /// Prepare NPCs for SQL insertion (deprecated - use save_to_db instead)
/// Returns a vector of tuples (id, name, json_data) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> { pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> {
self.npcs self.npcs
.iter() .iter()
@@ -129,6 +131,59 @@ impl NpcDatabase {
}) })
.collect() .collect()
} }
/// Save all NPCs to SQLite database
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
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<Self, diesel::result::Error> {
use crate::schema::npcs::dsl::*;
#[derive(Queryable)]
struct NpcRecord {
id: Option<i32>,
name: String,
data: String,
}
let records = npcs.load::<NpcRecord>(conn)?;
let mut loaded_npcs = Vec::new();
for record in records {
if let Ok(npc) = serde_json::from_str::<Npc>(&record.data) {
loaded_npcs.push(npc);
}
}
let mut db = Self::new();
db.add_npcs(loaded_npcs);
Ok(db)
}
} }
impl Default for NpcDatabase { impl Default for NpcDatabase {

View File

@@ -1,5 +1,7 @@
use crate::types::PlayerHouse; use crate::types::PlayerHouse;
use crate::xml_parser::{parse_player_houses_xml, XmlParseError}; use crate::xml_parser::{parse_player_houses_xml, XmlParseError};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@@ -150,8 +152,8 @@ impl PlayerHouseDatabase {
self.houses.is_empty() self.houses.is_empty()
} }
/// Prepare player houses for SQL insertion /// Prepare player houses for SQL insertion (deprecated - use save_to_db instead)
/// Returns a vector of tuples (id, name, price, json_data) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
pub fn prepare_for_sql(&self) -> Vec<(i32, String, i32, String)> { pub fn prepare_for_sql(&self) -> Vec<(i32, String, i32, String)> {
self.houses self.houses
.iter() .iter()
@@ -161,6 +163,61 @@ impl PlayerHouseDatabase {
}) })
.collect() .collect()
} }
/// Save all player houses to SQLite database
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
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<Self, diesel::result::Error> {
use crate::schema::player_houses::dsl::*;
#[derive(Queryable)]
struct PlayerHouseRecord {
id: Option<i32>,
name: String,
map_id: i32,
data: String,
}
let records = player_houses.load::<PlayerHouseRecord>(conn)?;
let mut loaded_houses = Vec::new();
for record in records {
if let Ok(house) = serde_json::from_str::<PlayerHouse>(&record.data) {
loaded_houses.push(house);
}
}
let mut db = Self::new();
db.add_houses(loaded_houses);
Ok(db)
}
} }
impl Default for PlayerHouseDatabase { impl Default for PlayerHouseDatabase {

View File

@@ -1,5 +1,7 @@
use crate::types::Quest; use crate::types::Quest;
use crate::xml_parser::{parse_quests_xml, XmlParseError}; use crate::xml_parser::{parse_quests_xml, XmlParseError};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@@ -92,8 +94,8 @@ impl QuestDatabase {
self.quests.is_empty() self.quests.is_empty()
} }
/// Prepare quests for SQL insertion /// Prepare quests for SQL insertion (deprecated - use save_to_db instead)
/// Returns a vector of tuples (id, name, json_data) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> { pub fn prepare_for_sql(&self) -> Vec<(i32, String, String)> {
self.quests self.quests
.iter() .iter()
@@ -103,6 +105,59 @@ impl QuestDatabase {
}) })
.collect() .collect()
} }
/// Save all quests to SQLite database
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
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<Self, diesel::result::Error> {
use crate::schema::quests::dsl::*;
#[derive(Queryable)]
struct QuestRecord {
id: Option<i32>,
name: String,
data: String,
}
let records = quests.load::<QuestRecord>(conn)?;
let mut loaded_quests = Vec::new();
for record in records {
if let Ok(quest) = serde_json::from_str::<Quest>(&record.data) {
loaded_quests.push(quest);
}
}
let mut db = Self::new();
db.add_quests(loaded_quests);
Ok(db)
}
} }
impl Default for QuestDatabase { impl Default for QuestDatabase {

View File

@@ -1,5 +1,7 @@
use crate::types::Shop; use crate::types::Shop;
use crate::xml_parser::{parse_shops_xml, XmlParseError}; use crate::xml_parser::{parse_shops_xml, XmlParseError};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@@ -143,8 +145,8 @@ impl ShopDatabase {
self.shops.is_empty() self.shops.is_empty()
} }
/// Prepare shops for SQL insertion /// Prepare shops for SQL insertion (deprecated - use save_to_db instead)
/// Returns a vector of tuples (shop_id, name, is_general_store, item_count, json_data) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
pub fn prepare_for_sql(&self) -> Vec<(i32, String, bool, usize, String)> { pub fn prepare_for_sql(&self) -> Vec<(i32, String, bool, usize, String)> {
self.shops self.shops
.iter() .iter()
@@ -160,6 +162,63 @@ impl ShopDatabase {
}) })
.collect() .collect()
} }
/// Save all shops to SQLite database
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
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<Self, diesel::result::Error> {
use crate::schema::shops::dsl::*;
#[derive(Queryable)]
struct ShopRecord {
id: Option<i32>,
name: String,
unique_items: i32,
item_count: i32,
data: String,
}
let records = shops.load::<ShopRecord>(conn)?;
let mut loaded_shops = Vec::new();
for record in records {
if let Ok(shop) = serde_json::from_str::<Shop>(&record.data) {
loaded_shops.push(shop);
}
}
let mut db = Self::new();
db.add_shops(loaded_shops);
Ok(db)
}
} }
impl Default for ShopDatabase { impl Default for ShopDatabase {

View File

@@ -1,5 +1,7 @@
use crate::types::Trait; use crate::types::Trait;
use crate::xml_parser::{parse_traits_xml, XmlParseError}; use crate::xml_parser::{parse_traits_xml, XmlParseError};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::Path; use std::path::Path;
@@ -172,8 +174,8 @@ impl TraitDatabase {
self.traits.is_empty() self.traits.is_empty()
} }
/// Prepare traits for SQL insertion /// Prepare traits for SQL insertion (deprecated - use save_to_db instead)
/// Returns a vector of tuples (id, name, skill, level, json_data) #[deprecated(note = "Use save_to_db() to save directly to SQLite database")]
pub fn prepare_for_sql(&self) -> Vec<(i32, String, Option<String>, Option<i32>, String)> { pub fn prepare_for_sql(&self) -> Vec<(i32, String, Option<String>, Option<i32>, String)> {
self.traits self.traits
.iter() .iter()
@@ -186,6 +188,63 @@ impl TraitDatabase {
}) })
.collect() .collect()
} }
/// Save all traits to SQLite database
pub fn save_to_db(&self, conn: &mut SqliteConnection) -> Result<usize, diesel::result::Error> {
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::<i32>), // 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<Self, diesel::result::Error> {
use crate::schema::traits::dsl::*;
#[derive(Queryable)]
struct TraitRecord {
id: Option<i32>,
name: String,
description: Option<String>,
trainer_id: Option<i32>,
data: String,
}
let records = traits.load::<TraitRecord>(conn)?;
let mut loaded_traits = Vec::new();
for record in records {
if let Ok(trait_obj) = serde_json::from_str::<Trait>(&record.data) {
loaded_traits.push(trait_obj);
}
}
let mut db = Self::new();
db.add_traits(loaded_traits);
Ok(db)
}
} }
impl Default for TraitDatabase { impl Default for TraitDatabase {

View File

@@ -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, 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 unity_parser::UnityProject;
use std::path::Path; use std::path::Path;
use unity_parser::log::DedupLogger; use unity_parser::log::DedupLogger;
@@ -47,6 +47,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let loot_db = LootDatabase::load_from_xml(loot_path)?; let loot_db = LootDatabase::load_from_xml(loot_path)?;
info!("✅ Loaded {} loot tables", loot_db.len()); 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 // Save to SQLite database
info!("\n💾 Saving game data to SQLite database..."); info!("\n💾 Saving game data to SQLite database...");
let mut conn = SqliteConnection::establish("cursebreaker.db")?; let mut conn = SqliteConnection::establish("cursebreaker.db")?;
@@ -56,6 +76,51 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Err(e) => warn!("⚠️ Failed to save items: {}", e), 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 // Print statistics
info!("\n📊 Game Data Statistics:"); info!("\n📊 Game Data Statistics:");
info!(" Items:"); info!(" Items:");