Files
cursebreaker-parser-rust/cursebreaker-parser/src/databases/fast_travel_database.rs
2026-01-10 09:22:58 +00:00

330 lines
10 KiB
Rust

use crate::types::{FastTravelLocation, FastTravelType};
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;
/// A database for managing Fast Travel Locations loaded from XML files
#[derive(Debug, Clone)]
pub struct FastTravelDatabase {
locations: Vec<FastTravelLocation>,
// Map ID -> location index
locations_by_id: HashMap<i32, usize>,
// Map name -> list of location indices
locations_by_name: HashMap<String, Vec<usize>>,
// Map type -> list of location indices
locations_by_type: HashMap<FastTravelType, Vec<usize>>,
}
impl FastTravelDatabase {
/// Create a new empty FastTravelDatabase
pub fn new() -> Self {
Self {
locations: Vec::new(),
locations_by_id: HashMap::new(),
locations_by_name: HashMap::new(),
locations_by_type: HashMap::new(),
}
}
/// Load all fast travel types from their respective XML files in a directory
/// Expects the directory structure:
/// - dir/FastTravelLocations/FastTravelLocations.xml
/// - dir/FastTravelCanoe/FastTravelCanoe.xml
/// - dir/FastTravelPortals/FastTravelPortals.xml
pub fn load_from_directory<P: AsRef<Path>>(dir: P) -> Result<Self, XmlParseError> {
let dir = dir.as_ref();
let mut db = Self::new();
// Load regular locations
let locations_path = dir.join("FastTravelLocations/FastTravelLocations.xml");
if locations_path.exists() {
let locations = parse_fast_travel_locations_xml(&locations_path)?;
db.add_locations(locations);
}
// Load canoe locations
let canoe_path = dir.join("FastTravelCanoe/FastTravelCanoe.xml");
if canoe_path.exists() {
let canoe_locations = parse_fast_travel_canoe_xml(&canoe_path)?;
db.add_locations(canoe_locations);
}
// Load portal locations
let portals_path = dir.join("FastTravelPortals/FastTravelPortals.xml");
if portals_path.exists() {
let portals = parse_fast_travel_portals_xml(&portals_path)?;
db.add_locations(portals);
}
Ok(db)
}
/// Load only regular fast travel locations from XML
pub fn load_locations_from_xml<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
let locations = parse_fast_travel_locations_xml(path)?;
let mut db = Self::new();
db.add_locations(locations);
Ok(db)
}
/// Load only canoe fast travel locations from XML
pub fn load_canoe_from_xml<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
let locations = parse_fast_travel_canoe_xml(path)?;
let mut db = Self::new();
db.add_locations(locations);
Ok(db)
}
/// Load only portal fast travel locations from XML
pub fn load_portals_from_xml<P: AsRef<Path>>(path: P) -> Result<Self, XmlParseError> {
let locations = parse_fast_travel_portals_xml(path)?;
let mut db = Self::new();
db.add_locations(locations);
Ok(db)
}
/// Add fast travel locations to the database
pub fn add_locations(&mut self, locations: Vec<FastTravelLocation>) {
for location in locations {
let index = self.locations.len();
// Index by ID
self.locations_by_id.insert(location.id, index);
// Index by name
self.locations_by_name
.entry(location.name.clone())
.or_insert_with(Vec::new)
.push(index);
// Index by type
self.locations_by_type
.entry(location.travel_type)
.or_insert_with(Vec::new)
.push(index);
self.locations.push(location);
}
}
/// Get a fast travel location by ID
pub fn get_by_id(&self, id: i32) -> Option<&FastTravelLocation> {
self.locations_by_id
.get(&id)
.and_then(|&index| self.locations.get(index))
}
/// Get fast travel locations by name (returns all locations with matching name)
pub fn get_by_name(&self, name: &str) -> Vec<&FastTravelLocation> {
self.locations_by_name
.get(name)
.map(|indices| {
indices
.iter()
.filter_map(|&index| self.locations.get(index))
.collect()
})
.unwrap_or_default()
}
/// Get all locations
pub fn all_locations(&self) -> &[FastTravelLocation] {
&self.locations
}
/// Get all locations of a specific type
pub fn get_by_type(&self, travel_type: FastTravelType) -> Vec<&FastTravelLocation> {
self.locations_by_type
.get(&travel_type)
.map(|indices| {
indices
.iter()
.filter_map(|&index| self.locations.get(index))
.collect()
})
.unwrap_or_default()
}
/// Get all regular fast travel locations
pub fn get_locations(&self) -> Vec<&FastTravelLocation> {
self.get_by_type(FastTravelType::Location)
}
/// Get all canoe fast travel locations
pub fn get_canoe_locations(&self) -> Vec<&FastTravelLocation> {
self.get_by_type(FastTravelType::Canoe)
}
/// Get all portal fast travel locations
pub fn get_portals(&self) -> Vec<&FastTravelLocation> {
self.get_by_type(FastTravelType::Portal)
}
/// Get all unlocked locations (regular locations only)
pub fn get_unlocked_locations(&self) -> Vec<&FastTravelLocation> {
self.locations
.iter()
.filter(|loc| loc.unlocked)
.collect()
}
/// Get all locations with requirements
pub fn get_locations_with_requirements(&self) -> Vec<&FastTravelLocation> {
self.locations
.iter()
.filter(|loc| loc.has_requirements())
.collect()
}
/// Get all locations that have connections to other locations
pub fn get_connected_locations(&self) -> Vec<&FastTravelLocation> {
self.locations
.iter()
.filter(|loc| loc.has_connections())
.collect()
}
/// Get locations that are connected to a specific location ID
pub fn get_locations_connected_to(&self, id: i32) -> Vec<&FastTravelLocation> {
self.locations
.iter()
.filter(|loc| loc.get_connections().contains(&id))
.collect()
}
/// Get locations that require a specific quest
pub fn get_locations_requiring_quest(&self, quest_id: &str) -> Vec<&FastTravelLocation> {
self.locations
.iter()
.filter(|loc| loc.requires_quest(quest_id))
.collect()
}
/// Get locations that require a specific trait
pub fn get_locations_requiring_trait(&self, trait_id: i32) -> Vec<&FastTravelLocation> {
self.locations
.iter()
.filter(|loc| loc.requires_trait(trait_id))
.collect()
}
/// Get all unique location names
pub fn get_all_names(&self) -> Vec<String> {
self.locations_by_name.keys().cloned().collect()
}
/// Get count by type
pub fn count_by_type(&self, travel_type: FastTravelType) -> usize {
self.locations_by_type
.get(&travel_type)
.map(|v| v.len())
.unwrap_or(0)
}
/// Get number of locations in database
pub fn len(&self) -> usize {
self.locations.len()
}
/// Check if database is empty
pub fn is_empty(&self) -> bool {
self.locations.is_empty()
}
/// 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()
.map(|location| {
let json =
serde_json::to_string(location).unwrap_or_else(|_| "{}".to_string());
(
location.id,
location.name.clone(),
location.travel_type.to_string(),
json,
)
})
.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 {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fast_travel_database_basic() {
let mut db = FastTravelDatabase::new();
assert!(db.is_empty());
assert_eq!(db.len(), 0);
}
}