Files
cursebreaker-parser-rust/cursebreaker-parser/src/databases/minimap_database.rs
2026-01-10 07:44:26 +00:00

291 lines
8.9 KiB
Rust

use crate::types::{MinimapTileRecord, NewMinimapTile};
use crate::image_processor::{ImageProcessor, ImageProcessingError};
use diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use std::path::{Path, PathBuf};
use std::fs;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MinimapDatabaseError {
#[error("Database error: {0}")]
DatabaseError(#[from] diesel::result::Error),
#[error("Image processing error: {0}")]
ImageError(#[from] ImageProcessingError),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("Invalid filename format: {0}")]
InvalidFilename(String),
#[error("Connection pool error: {0}")]
ConnectionError(String),
}
/// Database for managing minimap tiles with actual SQLite storage
pub struct MinimapDatabase {
database_url: String,
image_processor: ImageProcessor,
}
impl MinimapDatabase {
/// Create new database connection
pub fn new(database_url: String) -> Self {
Self {
database_url,
image_processor: ImageProcessor::default(),
}
}
/// Create with custom WebP quality
pub fn with_quality(database_url: String, quality: f32) -> Self {
Self {
database_url,
image_processor: ImageProcessor::new(quality),
}
}
/// Establish database connection
fn establish_connection(&self) -> Result<SqliteConnection, MinimapDatabaseError> {
SqliteConnection::establish(&self.database_url)
.map_err(|e| MinimapDatabaseError::ConnectionError(e.to_string()))
}
/// Load all PNG files from directory and process them into database
pub fn load_from_directory<P: AsRef<Path>>(
&self,
minimap_dir: P,
) -> Result<usize, MinimapDatabaseError> {
use crate::schema::minimap_tiles;
let mut conn = self.establish_connection()?;
let mut count = 0;
// Find all PNG files
let png_files = self.find_minimap_pngs(&minimap_dir)?;
for png_path in png_files {
// Parse coordinates from filename
let (x, y) = self.parse_coordinates(&png_path)?;
// Process image
let processed = self.image_processor.process_minimap_png(&png_path)?;
// Get original file size
let original_size = fs::metadata(&png_path)?.len() as i32;
// Extract WebP blobs for each size
let webp_512 = processed.get(512).expect("512px resolution missing");
let webp_256 = processed.get(256).expect("256px resolution missing");
let webp_128 = processed.get(128).expect("128px resolution missing");
let webp_64 = processed.get(64).expect("64px resolution missing");
// Create insertable record
let new_tile = NewMinimapTile {
x,
y,
original_width: 512,
original_height: 512,
original_file_size: Some(original_size),
webp_512,
webp_256,
webp_128,
webp_64,
webp_512_size: webp_512.len() as i32,
webp_256_size: webp_256.len() as i32,
webp_128_size: webp_128.len() as i32,
webp_64_size: webp_64.len() as i32,
source_path: png_path.to_str().unwrap_or(""),
};
// Insert into database
diesel::insert_into(minimap_tiles::table)
.values(&new_tile)
.execute(&mut conn)?;
count += 1;
}
Ok(count)
}
/// Find all minimap PNG files in directory
fn find_minimap_pngs<P: AsRef<Path>>(
&self,
dir: P,
) -> Result<Vec<PathBuf>, MinimapDatabaseError> {
let mut png_files = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("png") {
// Check if filename matches x_y.png pattern
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
if stem.contains('_') && stem.chars().all(|c| c.is_numeric() || c == '_' || c == '-') {
png_files.push(path);
}
}
}
}
Ok(png_files)
}
/// Parse x,y coordinates from filename (e.g., "0_0.png" -> (0, 0))
fn parse_coordinates<P: AsRef<Path>>(
&self,
path: P,
) -> Result<(i32, i32), MinimapDatabaseError> {
let filename = path
.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| {
MinimapDatabaseError::InvalidFilename(path.as_ref().display().to_string())
})?;
let parts: Vec<&str> = filename.split('_').collect();
if parts.len() != 2 {
return Err(MinimapDatabaseError::InvalidFilename(
filename.to_string(),
));
}
let x = parts[0].parse::<i32>().map_err(|_| {
MinimapDatabaseError::InvalidFilename(filename.to_string())
})?;
let y = parts[1].parse::<i32>().map_err(|_| {
MinimapDatabaseError::InvalidFilename(filename.to_string())
})?;
Ok((x, y))
}
/// Get tile by coordinates
pub fn get_tile(
&self,
x: i32,
y: i32,
) -> Result<Option<MinimapTileRecord>, MinimapDatabaseError> {
use crate::schema::minimap_tiles::dsl;
let mut conn = self.establish_connection()?;
let tile = dsl::minimap_tiles
.filter(dsl::x.eq(x))
.filter(dsl::y.eq(y))
.first::<MinimapTileRecord>(&mut conn)
.optional()?;
Ok(tile)
}
/// Get tile WebP blob at specific size
pub fn get_tile_webp(
&self,
x: i32,
y: i32,
size: u32,
) -> Result<Option<Vec<u8>>, MinimapDatabaseError> {
let tile = self.get_tile(x, y)?;
Ok(tile.map(|t| match size {
512 => t.webp_512,
256 => t.webp_256,
128 => t.webp_128,
64 => t.webp_64,
_ => t.webp_512, // Default to 512
}))
}
/// Get all tiles
pub fn get_all_tiles(&self) -> Result<Vec<MinimapTileRecord>, MinimapDatabaseError> {
use crate::schema::minimap_tiles::dsl::*;
let mut conn = self.establish_connection()?;
let tiles = minimap_tiles.load::<MinimapTileRecord>(&mut conn)?;
Ok(tiles)
}
/// Get map bounds (min/max x and y)
pub fn get_map_bounds(
&self,
) -> Result<((i32, i32), (i32, i32)), MinimapDatabaseError> {
use crate::schema::minimap_tiles::dsl::*;
use diesel::dsl::{max, min};
let mut conn = self.establish_connection()?;
let (min_x, max_x): (Option<i32>, Option<i32>) =
minimap_tiles.select((min(x), max(x))).first(&mut conn)?;
let (min_y, max_y): (Option<i32>, Option<i32>) =
minimap_tiles.select((min(y), max(y))).first(&mut conn)?;
Ok((
(min_x.unwrap_or(0), min_y.unwrap_or(0)),
(max_x.unwrap_or(0), max_y.unwrap_or(0)),
))
}
/// Get count of processed tiles
pub fn count(&self) -> Result<i64, MinimapDatabaseError> {
use crate::schema::minimap_tiles::dsl::*;
use diesel::dsl::count_star;
let mut conn = self.establish_connection()?;
let total = minimap_tiles.select(count_star()).first(&mut conn)?;
Ok(total)
}
/// Get total storage size statistics
pub fn get_storage_stats(&self) -> Result<StorageStats, MinimapDatabaseError> {
let mut conn = self.establish_connection()?;
use crate::schema::minimap_tiles::dsl::*;
let tiles = minimap_tiles.load::<MinimapTileRecord>(&mut conn)?;
let mut stats = StorageStats::default();
for tile in tiles {
stats.total_original_size += tile.original_file_size.unwrap_or(0) as i64;
stats.total_webp_512 += tile.webp_512_size as i64;
stats.total_webp_256 += tile.webp_256_size as i64;
stats.total_webp_128 += tile.webp_128_size as i64;
stats.total_webp_64 += tile.webp_64_size as i64;
stats.tile_count += 1;
}
Ok(stats)
}
}
#[derive(Debug, Default)]
pub struct StorageStats {
pub tile_count: i64,
pub total_original_size: i64,
pub total_webp_512: i64,
pub total_webp_256: i64,
pub total_webp_128: i64,
pub total_webp_64: i64,
}
impl StorageStats {
pub fn total_webp_size(&self) -> i64 {
self.total_webp_512 + self.total_webp_256 + self.total_webp_128 + self.total_webp_64
}
pub fn compression_ratio(&self) -> f64 {
if self.total_original_size == 0 {
return 0.0;
}
(self.total_webp_size() as f64 / self.total_original_size as f64) * 100.0
}
}