images in database
This commit is contained in:
@@ -44,7 +44,8 @@
|
|||||||
"Bash(timeout 10 cargo run:*)",
|
"Bash(timeout 10 cargo run:*)",
|
||||||
"Bash(timeout 60 cargo run:*)",
|
"Bash(timeout 60 cargo run:*)",
|
||||||
"Bash(DATABASE_URL=../cursebreaker.db diesel print-schema:*)",
|
"Bash(DATABASE_URL=../cursebreaker.db diesel print-schema:*)",
|
||||||
"Bash(DATABASE_URL=../cursebreaker.db diesel database:*)"
|
"Bash(DATABASE_URL=../cursebreaker.db diesel database:*)",
|
||||||
|
"Bash(DATABASE_URL=cursebreaker.db CB_ASSETS_PATH=/home/connor/repos/CBAssets cargo run:*)"
|
||||||
],
|
],
|
||||||
"additionalDirectories": [
|
"additionalDirectories": [
|
||||||
"/home/connor/repos/CBAssets/"
|
"/home/connor/repos/CBAssets/"
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,3 +20,4 @@ target/
|
|||||||
# Test data (cloned Unity projects for integration tests)
|
# Test data (cloned Unity projects for integration tests)
|
||||||
test_data/
|
test_data/
|
||||||
cursebreaker.db
|
cursebreaker.db
|
||||||
|
**/cursebreaker.db
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
DROP TABLE IF EXISTS icons;
|
||||||
|
DROP TABLE IF EXISTS achievement_icons;
|
||||||
|
DROP TABLE IF EXISTS general_icons;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- Simple icons table (abilities, buffs, traits, player houses, stat icons)
|
||||||
|
CREATE TABLE IF NOT EXISTS icons (
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
icon BLOB NOT NULL,
|
||||||
|
PRIMARY KEY (category, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Achievement icons table (filtered, no _0 suffix)
|
||||||
|
CREATE TABLE IF NOT EXISTS achievement_icons (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
icon BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- General icons table (multiple sizes)
|
||||||
|
CREATE TABLE IF NOT EXISTS general_icons (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
original_width INTEGER NOT NULL,
|
||||||
|
original_height INTEGER NOT NULL,
|
||||||
|
icon_original BLOB,
|
||||||
|
icon_256 BLOB,
|
||||||
|
icon_64 BLOB,
|
||||||
|
icon_32 BLOB
|
||||||
|
);
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
//! - Storing all tiles in the SQLite database
|
//! - Storing all tiles in the SQLite database
|
||||||
//! - Generating statistics about storage and compression
|
//! - Generating statistics about storage and compression
|
||||||
|
|
||||||
use cursebreaker_parser::MinimapDatabase;
|
use cursebreaker_parser::{MinimapDatabase, IconDatabase};
|
||||||
use log::{info, error, LevelFilter};
|
use log::{info, error, LevelFilter};
|
||||||
use unity_parser::log::DedupLogger;
|
use unity_parser::log::DedupLogger;
|
||||||
use std::env;
|
use std::env;
|
||||||
@@ -26,7 +26,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
// Process minimap tiles
|
// Process minimap tiles
|
||||||
info!("🗺️ Processing minimap tiles...");
|
info!("🗺️ Processing minimap tiles...");
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "cursebreaker.db".to_string());
|
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "cursebreaker.db".to_string());
|
||||||
let minimap_db = MinimapDatabase::new(database_url);
|
let minimap_db = MinimapDatabase::new(database_url.clone());
|
||||||
|
|
||||||
let cb_assets_path = env::var("CB_ASSETS_PATH")
|
let cb_assets_path = env::var("CB_ASSETS_PATH")
|
||||||
.unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
.unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
||||||
@@ -70,6 +70,29 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Process game icons
|
||||||
|
info!("\n=== Processing Game Icons ===");
|
||||||
|
let icon_db = IconDatabase::new(database_url);
|
||||||
|
|
||||||
|
match icon_db.load_all_icons(&cb_assets_path) {
|
||||||
|
Ok(stats) => {
|
||||||
|
info!("\n=== Icon Statistics ===");
|
||||||
|
info!("Ability icons: {}", stats.abilities);
|
||||||
|
info!("Buff icons: {}", stats.buffs);
|
||||||
|
info!("Trait icons: {}", stats.traits);
|
||||||
|
info!("Player house icons: {}", stats.player_houses);
|
||||||
|
info!("Stat icons: {}", stats.stat_icons);
|
||||||
|
info!("Achievement icons: {}", stats.achievement_icons);
|
||||||
|
info!("General icons: {}", stats.general_icons);
|
||||||
|
info!("Total icons: {}", stats.total_icons());
|
||||||
|
info!("Total size: {} KB", stats.total_bytes / 1024);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to process icons: {}", e);
|
||||||
|
return Err(Box::new(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log::logger().flush();
|
log::logger().flush();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
//! - Computing world transforms
|
//! - Computing world transforms
|
||||||
//! - Saving resource locations to the database
|
//! - Saving resource locations to the database
|
||||||
//! - Processing and saving item icons for resources
|
//! - Processing and saving item icons for resources
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//! scene-parser [min_x max_x min_y max_y]
|
||||||
|
//!
|
||||||
|
//! Examples:
|
||||||
|
//! scene-parser # Parse all scenes
|
||||||
|
//! scene-parser 0 10 0 10 # Parse scenes from (0,0) to (10,10)
|
||||||
|
|
||||||
use cursebreaker_parser::{
|
use cursebreaker_parser::{
|
||||||
InteractableResource, InteractableTeleporter, InteractableWorkbench,
|
InteractableResource, InteractableTeleporter, InteractableWorkbench,
|
||||||
@@ -20,6 +27,94 @@ use std::env;
|
|||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sqlite::SqliteConnection;
|
use diesel::sqlite::SqliteConnection;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
/// Bounds for filtering which scene tiles to parse
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Bounds {
|
||||||
|
min_x: i32,
|
||||||
|
max_x: i32,
|
||||||
|
min_y: i32,
|
||||||
|
max_y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bounds {
|
||||||
|
fn contains(&self, x: i32, y: i32) -> bool {
|
||||||
|
x >= self.min_x && x <= self.max_x && y >= self.min_y && y <= self.max_y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse scene filename to extract tile coordinates (e.g., "10_3.unity" -> (10, 3))
|
||||||
|
fn parse_scene_coords(filename: &str) -> Option<(i32, i32)> {
|
||||||
|
let stem = filename.strip_suffix(".unity")?;
|
||||||
|
let parts: Vec<&str> = stem.split('_').collect();
|
||||||
|
if parts.len() == 2 {
|
||||||
|
let x = parts[0].parse().ok()?;
|
||||||
|
let y = parts[1].parse().ok()?;
|
||||||
|
Some((x, y))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all scene files matching the *_*.unity pattern
|
||||||
|
fn find_scene_files(scenes_dir: &Path, bounds: Option<&Bounds>) -> Vec<PathBuf> {
|
||||||
|
let mut scenes = Vec::new();
|
||||||
|
|
||||||
|
if let Ok(entries) = fs::read_dir(scenes_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
|
||||||
|
if filename.ends_with(".unity") {
|
||||||
|
if let Some((x, y)) = parse_scene_coords(filename) {
|
||||||
|
// Check bounds if specified
|
||||||
|
if let Some(b) = bounds {
|
||||||
|
if !b.contains(x, y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scenes.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by coordinates for consistent ordering
|
||||||
|
scenes.sort_by(|a, b| {
|
||||||
|
let a_coords = a.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.and_then(parse_scene_coords)
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
let b_coords = b.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.and_then(parse_scene_coords)
|
||||||
|
.unwrap_or((0, 0));
|
||||||
|
a_coords.cmp(&b_coords)
|
||||||
|
});
|
||||||
|
|
||||||
|
scenes
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse command line arguments for bounds
|
||||||
|
fn parse_bounds_args() -> Option<Bounds> {
|
||||||
|
let args: Vec<String> = env::args().collect();
|
||||||
|
|
||||||
|
if args.len() == 5 {
|
||||||
|
let min_x = args[1].parse().ok()?;
|
||||||
|
let max_x = args[2].parse().ok()?;
|
||||||
|
let min_y = args[3].parse().ok()?;
|
||||||
|
let max_y = args[4].parse().ok()?;
|
||||||
|
Some(Bounds { min_x, max_x, min_y, max_y })
|
||||||
|
} else if args.len() == 1 {
|
||||||
|
None // No bounds specified, parse all
|
||||||
|
} else {
|
||||||
|
eprintln!("Usage: {} [min_x max_x min_y max_y]", args[0]);
|
||||||
|
eprintln!(" No arguments: parse all scenes");
|
||||||
|
eprintln!(" 4 arguments: parse scenes within bounds (inclusive)");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let logger = DedupLogger::new();
|
let logger = DedupLogger::new();
|
||||||
@@ -29,6 +124,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
info!("🎮 Cursebreaker - Scene Parser");
|
info!("🎮 Cursebreaker - Scene Parser");
|
||||||
|
|
||||||
|
// Parse bounds from command line
|
||||||
|
let bounds = parse_bounds_args();
|
||||||
|
if let Some(ref b) = bounds {
|
||||||
|
info!("📐 Bounds: x=[{}, {}], y=[{}, {}]", b.min_x, b.max_x, b.min_y, b.max_y);
|
||||||
|
} else {
|
||||||
|
info!("📐 Bounds: none (parsing all scenes)");
|
||||||
|
}
|
||||||
|
|
||||||
let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
let cb_assets_path = env::var("CB_ASSETS_PATH").unwrap_or_else(|_| "/home/connor/repos/CBAssets".to_string());
|
||||||
|
|
||||||
// Initialize Unity project once - scans entire project for GUID mappings
|
// Initialize Unity project once - scans entire project for GUID mappings
|
||||||
@@ -37,6 +140,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
let project = UnityProject::from_path(project_root)?;
|
let project = UnityProject::from_path(project_root)?;
|
||||||
|
|
||||||
|
// Find all scene files
|
||||||
|
let scenes_dir = project_root.join("_GameAssets/Scenes/Tiles");
|
||||||
|
let scene_files = find_scene_files(&scenes_dir, bounds.as_ref());
|
||||||
|
info!("🔍 Found {} scene files to parse", scene_files.len());
|
||||||
|
|
||||||
|
if scene_files.is_empty() {
|
||||||
|
warn!("No scene files found matching criteria");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
// Create type filter to only parse GameObject, Transform, and InteractableResource MonoBehaviour
|
// Create type filter to only parse GameObject, Transform, and InteractableResource MonoBehaviour
|
||||||
info!("🔍 Setting up type filter:");
|
info!("🔍 Setting up type filter:");
|
||||||
info!(" • Unity types: GameObject, Transform");
|
info!(" • Unity types: GameObject, Transform");
|
||||||
@@ -46,48 +159,121 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
vec!["InteractableResource", "InteractableTeleporter", "InteractableWorkbench", "LootSpawner", "MapIcon", "MapNameChanger"]
|
vec!["InteractableResource", "InteractableTeleporter", "InteractableWorkbench", "LootSpawner", "MapIcon", "MapNameChanger"]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Now parse the scene using the pre-built GUID resolvers with filtering
|
// Setup database connection
|
||||||
let scene_path = "_GameAssets/Scenes/Tiles/10_3.unity";
|
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "cursebreaker.db".to_string());
|
||||||
info!("📁 Parsing scene: {}", scene_path);
|
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||||
|
|
||||||
|
// Clear all tables before processing (they're regenerated each run)
|
||||||
|
{
|
||||||
|
use cursebreaker_parser::schema::{
|
||||||
|
world_resources, world_teleporters, world_workbenches,
|
||||||
|
world_loot, world_map_icons, world_map_name_changers, resource_icons
|
||||||
|
};
|
||||||
|
diesel::delete(world_resources::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_teleporters::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_workbenches::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_loot::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_map_icons::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(world_map_name_changers::table).execute(&mut conn)?;
|
||||||
|
diesel::delete(resource_icons::table).execute(&mut conn)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect unique harvestables across all scenes for icon processing
|
||||||
|
let mut all_unique_harvestables: HashMap<i32, String> = HashMap::new();
|
||||||
|
|
||||||
|
// Track totals
|
||||||
|
let mut total_resources = 0;
|
||||||
|
let mut total_teleporters = 0;
|
||||||
|
let mut total_workbenches = 0;
|
||||||
|
let mut total_loot = 0;
|
||||||
|
let mut total_map_icons = 0;
|
||||||
|
let mut total_map_name_changers = 0;
|
||||||
|
let mut scenes_processed = 0;
|
||||||
|
let mut scenes_failed = 0;
|
||||||
|
|
||||||
|
// Process each scene
|
||||||
|
for (idx, scene_path) in scene_files.iter().enumerate() {
|
||||||
|
let relative_path = scene_path.strip_prefix(project_root)
|
||||||
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|_| scene_path.to_string_lossy().to_string());
|
||||||
|
|
||||||
|
info!("\n📁 [{}/{}] Parsing scene: {}", idx + 1, scene_files.len(), relative_path);
|
||||||
|
log::logger().flush();
|
||||||
|
|
||||||
|
match project.parse_scene_filtered(&relative_path, Some(&type_filter)) {
|
||||||
|
Ok(mut scene) => {
|
||||||
|
info!(" ✓ Parsed ({} entities)", scene.entity_map.len());
|
||||||
|
|
||||||
|
// Post-processing: Compute world transforms
|
||||||
|
unity_parser::compute_world_transforms(&mut scene.world, &scene.entity_map);
|
||||||
|
|
||||||
|
// Save resources
|
||||||
|
let resource_count = save_resources(&mut conn, &scene)?;
|
||||||
|
total_resources += resource_count;
|
||||||
|
|
||||||
|
// Collect unique harvestables for icon processing later
|
||||||
|
scene.world
|
||||||
|
.query_all::<(&InteractableResource, &unity_parser::GameObject)>()
|
||||||
|
.for_each(|(resource, object)| {
|
||||||
|
all_unique_harvestables.entry(resource.type_id as i32)
|
||||||
|
.or_insert_with(|| object.name.to_string());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save other world objects (append mode - tables already cleared)
|
||||||
|
total_teleporters += save_teleporters_append(&mut conn, &scene)?;
|
||||||
|
total_workbenches += save_workbenches_append(&mut conn, &scene)?;
|
||||||
|
total_loot += save_loot_spawners_append(&mut conn, &scene)?;
|
||||||
|
total_map_icons += save_map_icons_append(&mut conn, &scene)?;
|
||||||
|
total_map_name_changers += save_map_name_changers_append(&mut conn, &scene)?;
|
||||||
|
|
||||||
|
scenes_processed += 1;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!(" ✗ Parse error: {}", e);
|
||||||
|
scenes_failed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
log::logger().flush();
|
log::logger().flush();
|
||||||
|
|
||||||
// Parse the scene using the project with type filtering
|
// Process icons for all unique harvestables
|
||||||
match project.parse_scene_filtered(scene_path, Some(&type_filter)) {
|
info!("\n🎨 Processing item icons for {} unique harvestable types...", all_unique_harvestables.len());
|
||||||
Ok(mut scene) => {
|
process_item_icons_from_map(&cb_assets_path, &mut conn, &all_unique_harvestables)?;
|
||||||
info!("✅ Scene parsed successfully!");
|
|
||||||
info!(" Total entities: {}", scene.entity_map.len());
|
|
||||||
|
|
||||||
// Post-processing: Compute world transforms
|
// Print summary
|
||||||
info!("🔄 Computing world transforms...");
|
info!("\n==================================================");
|
||||||
unity_parser::compute_world_transforms(&mut scene.world, &scene.entity_map);
|
info!("📊 SUMMARY");
|
||||||
info!(" ✓ World transforms computed");
|
info!("==================================================");
|
||||||
|
info!(" Scenes processed: {} ({} failed)", scenes_processed, scenes_failed);
|
||||||
|
info!(" Resources: {}", total_resources);
|
||||||
|
info!(" Teleporters: {}", total_teleporters);
|
||||||
|
info!(" Workbenches: {}", total_workbenches);
|
||||||
|
info!(" Loot spawners: {}", total_loot);
|
||||||
|
info!(" Map icons: {}", total_map_icons);
|
||||||
|
info!(" Map name changers:{}", total_map_name_changers);
|
||||||
|
info!("==================================================");
|
||||||
|
|
||||||
// Save resources to database
|
log::logger().flush();
|
||||||
info!("💾 Saving resources to database...");
|
|
||||||
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
|
||||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
|
||||||
|
|
||||||
// Use diesel schema
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save resources from a scene (append mode)
|
||||||
|
fn save_resources(
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
scene: &unity_parser::UnityScene,
|
||||||
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_resources;
|
use cursebreaker_parser::schema::world_resources;
|
||||||
|
|
||||||
// Clear the entire table (it's regenerated each run)
|
let mut count = 0;
|
||||||
diesel::delete(world_resources::table).execute(&mut conn)?;
|
|
||||||
|
|
||||||
let mut resource_count = 0;
|
|
||||||
|
|
||||||
// Insert all resources in a transaction
|
|
||||||
conn.transaction::<_, diesel::result::Error, _>(|conn| {
|
conn.transaction::<_, diesel::result::Error, _>(|conn| {
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&InteractableResource, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&InteractableResource, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(resource, transform, object)| {
|
.for_each(|(resource, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
info!(" 📦 Resource: \"{}\"", object.name);
|
|
||||||
info!(" • typeId: {}", resource.type_id);
|
|
||||||
info!(" • Position: ({:.2}, {:.2})", world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
// Insert: store x and z (as y) coordinates only
|
|
||||||
let _ = diesel::insert_into(world_resources::table)
|
let _ = diesel::insert_into(world_resources::table)
|
||||||
.values((
|
.values((
|
||||||
world_resources::item_id.eq(resource.type_id as i32),
|
world_resources::item_id.eq(resource.type_id as i32),
|
||||||
@@ -96,67 +282,23 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
))
|
))
|
||||||
.execute(conn);
|
.execute(conn);
|
||||||
|
|
||||||
resource_count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
info!("✅ Saved {} resources to database", resource_count);
|
Ok(count)
|
||||||
log::logger().flush();
|
|
||||||
|
|
||||||
// Process and save item icons
|
|
||||||
info!("🎨 Processing item icons...");
|
|
||||||
process_item_icons(&cb_assets_path, &mut conn, &scene)?;
|
|
||||||
|
|
||||||
// Save other world objects
|
|
||||||
info!("🗺️ Saving teleporters...");
|
|
||||||
save_teleporters(&mut conn, &scene)?;
|
|
||||||
|
|
||||||
info!("🔨 Saving workbenches...");
|
|
||||||
save_workbenches(&mut conn, &scene)?;
|
|
||||||
|
|
||||||
info!("💰 Saving loot spawners...");
|
|
||||||
save_loot_spawners(&mut conn, &scene)?;
|
|
||||||
|
|
||||||
info!("📍 Saving map icons...");
|
|
||||||
save_map_icons(&mut conn, &scene)?;
|
|
||||||
|
|
||||||
info!("🏷️ Saving map name changers...");
|
|
||||||
save_map_name_changers(&mut conn, &scene)?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("Parse error: {}", e);
|
|
||||||
return Err(Box::new(e));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log::logger().flush();
|
/// Process item icons from a pre-collected map of harvestables
|
||||||
|
fn process_item_icons_from_map(
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process item icons for all resources in the scene
|
|
||||||
fn process_item_icons(
|
|
||||||
cb_assets_path: &str,
|
cb_assets_path: &str,
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
unique_harvestables: &HashMap<i32, String>,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::{resource_icons, items, harvestables, harvestable_drops};
|
use cursebreaker_parser::schema::{resource_icons, items, harvestables, harvestable_drops};
|
||||||
|
|
||||||
// Collect unique harvestable IDs from resources
|
info!(" Processing {} unique harvestable types", unique_harvestables.len());
|
||||||
let mut unique_harvestables: HashMap<i32, String> = HashMap::new();
|
|
||||||
|
|
||||||
scene.world
|
|
||||||
.query_all::<(&InteractableResource, &unity_parser::GameObject)>()
|
|
||||||
.for_each(|(resource, object)| {
|
|
||||||
unique_harvestables.entry(resource.type_id as i32)
|
|
||||||
.or_insert_with(|| object.name.to_string());
|
|
||||||
});
|
|
||||||
|
|
||||||
info!(" Found {} unique harvestable types", unique_harvestables.len());
|
|
||||||
|
|
||||||
// Clear existing resource icons (regenerated each run)
|
|
||||||
diesel::delete(resource_icons::table).execute(conn)?;
|
|
||||||
|
|
||||||
// Create image processor with white outline
|
// Create image processor with white outline
|
||||||
let processor = ImageProcessor::default();
|
let processor = ImageProcessor::default();
|
||||||
@@ -249,22 +391,19 @@ fn process_item_icons(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save teleporter data to database
|
/// Save teleporter data to database (append mode - doesn't clear table)
|
||||||
fn save_teleporters(
|
fn save_teleporters_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_teleporters;
|
use cursebreaker_parser::schema::world_teleporters;
|
||||||
|
|
||||||
// Clear existing teleporters
|
|
||||||
diesel::delete(world_teleporters::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all teleporters
|
// Query all teleporters
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&InteractableTeleporter, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&InteractableTeleporter, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(teleporter, transform, object)| {
|
.for_each(|(teleporter, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
// Get the tp_transform position if it exists
|
// Get the tp_transform position if it exists
|
||||||
@@ -279,9 +418,6 @@ fn save_teleporters(
|
|||||||
(None, None)
|
(None, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
info!(" 🗺️ Teleporter: \"{}\" at ({:.2}, {:.2}) -> ({:?}, {:?})",
|
|
||||||
object.name, world_pos.x, world_pos.z, tp_x, tp_y);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_teleporters::table)
|
let _ = diesel::insert_into(world_teleporters::table)
|
||||||
.values((
|
.values((
|
||||||
world_teleporters::pos_x.eq(world_pos.x as f32),
|
world_teleporters::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -294,31 +430,24 @@ fn save_teleporters(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} teleporters to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save workbench data to database
|
/// Save workbench data to database (append mode - doesn't clear table)
|
||||||
fn save_workbenches(
|
fn save_workbenches_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_workbenches;
|
use cursebreaker_parser::schema::world_workbenches;
|
||||||
|
|
||||||
// Clear existing workbenches
|
|
||||||
diesel::delete(world_workbenches::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all workbenches
|
// Query all workbenches
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&InteractableWorkbench, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&InteractableWorkbench, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(workbench, transform, object)| {
|
.for_each(|(workbench, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
info!(" 🔨 Workbench: \"{}\" (ID: {}) at ({:.2}, {:.2})",
|
|
||||||
object.name, workbench.workbench_id, world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_workbenches::table)
|
let _ = diesel::insert_into(world_workbenches::table)
|
||||||
.values((
|
.values((
|
||||||
world_workbenches::pos_x.eq(world_pos.x as f32),
|
world_workbenches::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -330,31 +459,24 @@ fn save_workbenches(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} workbenches to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save loot spawner data to database
|
/// Save loot spawner data to database (append mode - doesn't clear table)
|
||||||
fn save_loot_spawners(
|
fn save_loot_spawners_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_loot;
|
use cursebreaker_parser::schema::world_loot;
|
||||||
|
|
||||||
// Clear existing loot spawners
|
|
||||||
diesel::delete(world_loot::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all loot spawners
|
// Query all loot spawners
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&LootSpawner, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&LootSpawner, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(loot, transform, object)| {
|
.for_each(|(loot, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
info!(" 💰 Loot: \"{}\" (Item: {}, Amount: {}, Respawn: {}s) at ({:.2}, {:.2})",
|
|
||||||
object.name, loot.item_id, loot.amount, loot.respawn_time, world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_loot::table)
|
let _ = diesel::insert_into(world_loot::table)
|
||||||
.values((
|
.values((
|
||||||
world_loot::pos_x.eq(world_pos.x as f32),
|
world_loot::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -369,31 +491,24 @@ fn save_loot_spawners(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} loot spawners to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save map icon data to database
|
/// Save map icon data to database (append mode - doesn't clear table)
|
||||||
fn save_map_icons(
|
fn save_map_icons_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_map_icons;
|
use cursebreaker_parser::schema::world_map_icons;
|
||||||
|
|
||||||
// Clear existing map icons
|
|
||||||
diesel::delete(world_map_icons::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all map icons
|
// Query all map icons
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&MapIcon, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&MapIcon, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(icon, transform, object)| {
|
.for_each(|(icon, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
info!(" 📍 MapIcon: \"{}\" (Type: {:?}, Text: \"{}\") at ({:.2}, {:.2})",
|
|
||||||
object.name, icon.icon_type, icon.text, world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_map_icons::table)
|
let _ = diesel::insert_into(world_map_icons::table)
|
||||||
.values((
|
.values((
|
||||||
world_map_icons::pos_x.eq(world_pos.x as f32),
|
world_map_icons::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -410,31 +525,24 @@ fn save_map_icons(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} map icons to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save map name changer data to database
|
/// Save map name changer data to database (append mode - doesn't clear table)
|
||||||
fn save_map_name_changers(
|
fn save_map_name_changers_append(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
scene: &unity_parser::UnityScene,
|
scene: &unity_parser::UnityScene,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<usize, Box<dyn std::error::Error>> {
|
||||||
use cursebreaker_parser::schema::world_map_name_changers;
|
use cursebreaker_parser::schema::world_map_name_changers;
|
||||||
|
|
||||||
// Clear existing map name changers
|
|
||||||
diesel::delete(world_map_name_changers::table).execute(conn)?;
|
|
||||||
|
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Query all map name changers
|
// Query all map name changers
|
||||||
scene.world
|
scene.world
|
||||||
.query_all::<(&MapNameChanger, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
.query_all::<(&MapNameChanger, &unity_parser::WorldTransform, &unity_parser::GameObject)>()
|
||||||
.for_each(|(changer, transform, object)| {
|
.for_each(|(changer, transform, _object)| {
|
||||||
let world_pos = transform.position();
|
let world_pos = transform.position();
|
||||||
|
|
||||||
info!(" 🏷️ MapNameChanger: \"{}\" -> \"{}\" at ({:.2}, {:.2})",
|
|
||||||
object.name, changer.map_name, world_pos.x, world_pos.z);
|
|
||||||
|
|
||||||
let _ = diesel::insert_into(world_map_name_changers::table)
|
let _ = diesel::insert_into(world_map_name_changers::table)
|
||||||
.values((
|
.values((
|
||||||
world_map_name_changers::pos_x.eq(world_pos.x as f32),
|
world_map_name_changers::pos_x.eq(world_pos.x as f32),
|
||||||
@@ -446,6 +554,5 @@ fn save_map_name_changers(
|
|||||||
count += 1;
|
count += 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
info!("✅ Saved {} map name changers to database", count);
|
Ok(count)
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
|
|
||||||
// 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 database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string());
|
let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "cursebreaker.db".to_string());
|
||||||
let mut conn = SqliteConnection::establish(&database_url)?;
|
let mut conn = SqliteConnection::establish(&database_url)?;
|
||||||
|
|
||||||
// Process and save items with icons
|
// Process and save items with icons
|
||||||
|
|||||||
450
cursebreaker-parser/src/databases/icon_database.rs
Normal file
450
cursebreaker-parser/src/databases/icon_database.rs
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
use crate::types::{IconCategory, NewIcon, NewAchievementIcon, NewGeneralIcon};
|
||||||
|
use crate::image_processor::ImageProcessor;
|
||||||
|
use diesel::prelude::*;
|
||||||
|
use diesel::sqlite::SqliteConnection;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::fs;
|
||||||
|
use thiserror::Error;
|
||||||
|
use log::{info, warn};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum IconDatabaseError {
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DatabaseError(#[from] diesel::result::Error),
|
||||||
|
|
||||||
|
#[error("Image load error: {0}")]
|
||||||
|
ImageLoadError(#[from] image::ImageError),
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IoError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("Connection pool error: {0}")]
|
||||||
|
ConnectionError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Statistics for icon loading
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct IconStats {
|
||||||
|
pub abilities: usize,
|
||||||
|
pub buffs: usize,
|
||||||
|
pub traits: usize,
|
||||||
|
pub player_houses: usize,
|
||||||
|
pub stat_icons: usize,
|
||||||
|
pub achievement_icons: usize,
|
||||||
|
pub general_icons: usize,
|
||||||
|
pub total_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconStats {
|
||||||
|
pub fn total_icons(&self) -> usize {
|
||||||
|
self.abilities + self.buffs + self.traits + self.player_houses
|
||||||
|
+ self.stat_icons + self.achievement_icons + self.general_icons
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Database for managing game icons
|
||||||
|
pub struct IconDatabase {
|
||||||
|
database_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconDatabase {
|
||||||
|
/// Create new database connection
|
||||||
|
pub fn new(database_url: String) -> Self {
|
||||||
|
Self { database_url }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establish database connection
|
||||||
|
fn establish_connection(&self) -> Result<SqliteConnection, IconDatabaseError> {
|
||||||
|
SqliteConnection::establish(&self.database_url)
|
||||||
|
.map_err(|e| IconDatabaseError::ConnectionError(e.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load all icons from the CBAssets directory
|
||||||
|
pub fn load_all_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
cb_assets_path: P,
|
||||||
|
) -> Result<IconStats, IconDatabaseError> {
|
||||||
|
let base = cb_assets_path.as_ref();
|
||||||
|
let textures = base.join("Data/Textures");
|
||||||
|
let mut stats = IconStats::default();
|
||||||
|
|
||||||
|
// 1. Simple icons (original size only)
|
||||||
|
info!("Loading ability icons...");
|
||||||
|
stats.abilities = self.load_simple_icons(
|
||||||
|
&textures.join("Abilities"),
|
||||||
|
IconCategory::Ability,
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading buff icons...");
|
||||||
|
stats.buffs = self.load_simple_icons(
|
||||||
|
&textures.join("Buffs"),
|
||||||
|
IconCategory::Buff,
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading trait icons...");
|
||||||
|
stats.traits = self.load_simple_icons(
|
||||||
|
&textures.join("Traits"),
|
||||||
|
IconCategory::Trait,
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading player house icons...");
|
||||||
|
stats.player_houses = self.load_simple_icons(
|
||||||
|
&textures.join("PlayerHouses/Houses"),
|
||||||
|
IconCategory::PlayerHouse,
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
info!("Loading stat icons...");
|
||||||
|
stats.stat_icons = self.load_simple_icons(
|
||||||
|
&textures.join("StatIcons"),
|
||||||
|
IconCategory::StatIcon,
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 2. Achievement icons (filtered, no _0 suffix)
|
||||||
|
info!("Loading achievement icons...");
|
||||||
|
stats.achievement_icons = self.load_achievement_icons(
|
||||||
|
&textures.join("Achievements/Icons"),
|
||||||
|
&mut stats.total_bytes,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// 3. General icons (multi-size)
|
||||||
|
info!("Loading general icons...");
|
||||||
|
stats.general_icons = self.load_general_icons(&textures, &mut stats.total_bytes)?;
|
||||||
|
|
||||||
|
Ok(stats)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load simple icons from a directory (original size only, converted to WebP)
|
||||||
|
fn load_simple_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
category: IconCategory,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::icons;
|
||||||
|
|
||||||
|
let dir_path = dir.as_ref();
|
||||||
|
if !dir_path.exists() {
|
||||||
|
warn!("Directory does not exist: {}", dir_path.display());
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let image_files = self.find_image_files(dir_path)?;
|
||||||
|
|
||||||
|
for path in image_files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and encode as lossless WebP
|
||||||
|
let img = image::open(&path)?;
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
let webp_data = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
|
||||||
|
*total_bytes += webp_data.len();
|
||||||
|
|
||||||
|
let new_icon = NewIcon {
|
||||||
|
category: category.as_str(),
|
||||||
|
name: &name,
|
||||||
|
icon: &webp_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} {} icons", count, category.as_str());
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load achievement icons, filtering out files ending with _0
|
||||||
|
fn load_achievement_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::achievement_icons;
|
||||||
|
|
||||||
|
let dir_path = dir.as_ref();
|
||||||
|
if !dir_path.exists() {
|
||||||
|
warn!("Directory does not exist: {}", dir_path.display());
|
||||||
|
return Ok(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
let image_files = self.find_image_files(dir_path)?;
|
||||||
|
|
||||||
|
for path in image_files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip files ending with _0
|
||||||
|
if name.ends_with("_0") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load and encode as lossless WebP
|
||||||
|
let img = image::open(&path)?;
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
let webp_data = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
|
||||||
|
*total_bytes += webp_data.len();
|
||||||
|
|
||||||
|
let new_icon = NewAchievementIcon {
|
||||||
|
name: &name,
|
||||||
|
icon: &webp_data,
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(achievement_icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(&mut conn)?;
|
||||||
|
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} achievement icons", count);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load general icons with multiple sizes
|
||||||
|
fn load_general_icons<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
textures_dir: P,
|
||||||
|
total_bytes: &mut usize,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
let textures = textures_dir.as_ref();
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
// Collect all general icon paths
|
||||||
|
let mut icon_paths: Vec<(String, PathBuf)> = Vec::new();
|
||||||
|
|
||||||
|
// Directory-based icons
|
||||||
|
let directories = [
|
||||||
|
("Achievements/Trophies", true), // PNG only
|
||||||
|
("BottomRightTabs", false),
|
||||||
|
("MinimapIcons", false),
|
||||||
|
("Notifications", false),
|
||||||
|
("OverheadIcons", false),
|
||||||
|
("Skills", false),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (subdir, png_only) in directories {
|
||||||
|
let dir = textures.join(subdir);
|
||||||
|
if dir.exists() {
|
||||||
|
let files = if png_only {
|
||||||
|
self.find_png_files(&dir)?
|
||||||
|
} else {
|
||||||
|
self.find_image_files(&dir)?
|
||||||
|
};
|
||||||
|
|
||||||
|
for path in files {
|
||||||
|
let name = path.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.map(|s| format!("{}_{}", subdir.replace('/', "_"), s))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !name.is_empty() {
|
||||||
|
icon_paths.push((name, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Individual file icons
|
||||||
|
let individual_files = [
|
||||||
|
("Common/Book.png", "Common_Book"),
|
||||||
|
("Common/Hourglass.png", "Common_Hourglass"),
|
||||||
|
("Common/Mana.png", "Common_Mana"),
|
||||||
|
("Common/QuestCompleteTrophy.png", "Common_QuestCompleteTrophy"),
|
||||||
|
("Common/Tick.png", "Common_Tick"),
|
||||||
|
("Common/TutorialTip.png", "Common_TutorialTip"),
|
||||||
|
("Common/Zoom_Minus.png", "Common_Zoom_Minus"),
|
||||||
|
("Common/Zoom_Plus.png", "Common_Zoom_Plus"),
|
||||||
|
("Inventory/Banknote.png", "Inventory_Banknote"),
|
||||||
|
("Minimap/ShowCoordinates.png", "Minimap_ShowCoordinates"),
|
||||||
|
("SplashScreens/Olipa.png", "SplashScreens_Olipa"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (file, name) in individual_files {
|
||||||
|
let path = textures.join(file);
|
||||||
|
if path.exists() {
|
||||||
|
icon_paths.push((name.to_string(), path));
|
||||||
|
} else {
|
||||||
|
warn!("File not found: {}", path.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all collected icons
|
||||||
|
let mut conn = self.establish_connection()?;
|
||||||
|
|
||||||
|
for (name, path) in icon_paths {
|
||||||
|
if let Ok(bytes) = self.process_general_icon(&path, &name, &mut conn) {
|
||||||
|
*total_bytes += bytes;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!(" Loaded {} general icons", count);
|
||||||
|
Ok(count)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Process a single general icon at multiple sizes
|
||||||
|
fn process_general_icon(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
name: &str,
|
||||||
|
conn: &mut SqliteConnection,
|
||||||
|
) -> Result<usize, IconDatabaseError> {
|
||||||
|
use crate::schema::general_icons;
|
||||||
|
|
||||||
|
// Load image
|
||||||
|
let img = image::open(path)?;
|
||||||
|
let (width, height) = (img.width(), img.height());
|
||||||
|
let rgba = img.to_rgba8();
|
||||||
|
|
||||||
|
let mut total_bytes = 0;
|
||||||
|
|
||||||
|
// Original size (lossless)
|
||||||
|
let icon_original = ImageProcessor::encode_webp_lossless(&rgba)
|
||||||
|
.map_err(|e| IconDatabaseError::IoError(std::io::Error::other(e.to_string())))?;
|
||||||
|
total_bytes += icon_original.len();
|
||||||
|
|
||||||
|
// Generate smaller sizes only if image is large enough (no upscaling)
|
||||||
|
let processor = ImageProcessor::new(90.0);
|
||||||
|
|
||||||
|
let icon_256 = if width >= 256 && height >= 256 {
|
||||||
|
Some(self.resize_and_encode(&img, 256, &processor)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(ref data) = icon_256 {
|
||||||
|
total_bytes += data.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon_64 = if width >= 64 && height >= 64 {
|
||||||
|
Some(self.resize_and_encode(&img, 64, &processor)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(ref data) = icon_64 {
|
||||||
|
total_bytes += data.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let icon_32 = if width >= 32 && height >= 32 {
|
||||||
|
Some(self.resize_and_encode(&img, 32, &processor)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
if let Some(ref data) = icon_32 {
|
||||||
|
total_bytes += data.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_icon = NewGeneralIcon {
|
||||||
|
name,
|
||||||
|
original_width: width as i32,
|
||||||
|
original_height: height as i32,
|
||||||
|
icon_original: Some(&icon_original),
|
||||||
|
icon_256: icon_256.as_deref(),
|
||||||
|
icon_64: icon_64.as_deref(),
|
||||||
|
icon_32: icon_32.as_deref(),
|
||||||
|
};
|
||||||
|
|
||||||
|
diesel::replace_into(general_icons::table)
|
||||||
|
.values(&new_icon)
|
||||||
|
.execute(conn)?;
|
||||||
|
|
||||||
|
Ok(total_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize image and encode to WebP
|
||||||
|
fn resize_and_encode(
|
||||||
|
&self,
|
||||||
|
img: &image::DynamicImage,
|
||||||
|
size: u32,
|
||||||
|
_processor: &ImageProcessor,
|
||||||
|
) -> Result<Vec<u8>, IconDatabaseError> {
|
||||||
|
let resized = img.resize_exact(size, size, image::imageops::FilterType::Lanczos3);
|
||||||
|
let rgba = resized.to_rgba8();
|
||||||
|
|
||||||
|
// Use lossy encoding for smaller sizes
|
||||||
|
let encoder = webp::Encoder::from_rgba(rgba.as_raw(), size, size);
|
||||||
|
let webp_data = encoder.encode(90.0);
|
||||||
|
|
||||||
|
Ok(webp_data.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all image files (PNG, JPG, etc.) in a directory
|
||||||
|
fn find_image_files<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
) -> Result<Vec<PathBuf>, IconDatabaseError> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
|
||||||
|
let ext_lower = ext.to_lowercase();
|
||||||
|
if ext_lower == "png" || ext_lower == "jpg" || ext_lower == "jpeg" {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find only PNG files in a directory
|
||||||
|
fn find_png_files<P: AsRef<Path>>(
|
||||||
|
&self,
|
||||||
|
dir: P,
|
||||||
|
) -> Result<Vec<PathBuf>, IconDatabaseError> {
|
||||||
|
let mut files = Vec::new();
|
||||||
|
|
||||||
|
for entry in fs::read_dir(dir)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
|
||||||
|
let ext_lower = ext.to_lowercase();
|
||||||
|
if ext_lower == "png" {
|
||||||
|
files.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ mod player_house_database;
|
|||||||
mod trait_database;
|
mod trait_database;
|
||||||
mod shop_database;
|
mod shop_database;
|
||||||
mod minimap_database;
|
mod minimap_database;
|
||||||
|
mod icon_database;
|
||||||
|
|
||||||
pub use item_database::ItemDatabase;
|
pub use item_database::ItemDatabase;
|
||||||
pub use npc_database::NpcDatabase;
|
pub use npc_database::NpcDatabase;
|
||||||
@@ -21,3 +22,4 @@ pub use player_house_database::PlayerHouseDatabase;
|
|||||||
pub use trait_database::TraitDatabase;
|
pub use trait_database::TraitDatabase;
|
||||||
pub use shop_database::ShopDatabase;
|
pub use shop_database::ShopDatabase;
|
||||||
pub use minimap_database::{MinimapDatabase, MinimapDatabaseError, StorageStats};
|
pub use minimap_database::{MinimapDatabase, MinimapDatabaseError, StorageStats};
|
||||||
|
pub use icon_database::{IconDatabase, IconDatabaseError, IconStats};
|
||||||
|
|||||||
@@ -132,16 +132,6 @@ impl ImageProcessor {
|
|||||||
Ok(ProcessedImages { images })
|
Ok(ProcessedImages { images })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load PNG and generate 4 WebP sizes specifically for minimap tiles (512x512 source)
|
|
||||||
///
|
|
||||||
/// Convenience method that generates 512, 256, 128, and 64 pixel versions
|
|
||||||
pub fn process_minimap_png<P: AsRef<Path>>(
|
|
||||||
&self,
|
|
||||||
png_path: P,
|
|
||||||
) -> Result<ProcessedImages, ImageProcessingError> {
|
|
||||||
self.process_image(png_path, &[512, 256, 128, 64], Some((512, 512)), None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply outline effect to image based on alpha channel edges
|
/// Apply outline effect to image based on alpha channel edges
|
||||||
fn apply_outline(&self, img: RgbaImage, config: &OutlineConfig) -> RgbaImage {
|
fn apply_outline(&self, img: RgbaImage, config: &OutlineConfig) -> RgbaImage {
|
||||||
let (width, height) = img.dimensions();
|
let (width, height) = img.dimensions();
|
||||||
|
|||||||
@@ -70,6 +70,9 @@ pub use databases::{
|
|||||||
MinimapDatabase,
|
MinimapDatabase,
|
||||||
MinimapDatabaseError,
|
MinimapDatabaseError,
|
||||||
StorageStats,
|
StorageStats,
|
||||||
|
IconDatabase,
|
||||||
|
IconDatabaseError,
|
||||||
|
IconStats,
|
||||||
};
|
};
|
||||||
pub use types::{
|
pub use types::{
|
||||||
// Items
|
// Items
|
||||||
@@ -124,6 +127,14 @@ pub use types::{
|
|||||||
MinimapTile,
|
MinimapTile,
|
||||||
MinimapTileRecord,
|
MinimapTileRecord,
|
||||||
NewMinimapTile,
|
NewMinimapTile,
|
||||||
|
// Icons
|
||||||
|
IconCategory,
|
||||||
|
IconRecord,
|
||||||
|
NewIcon,
|
||||||
|
AchievementIconRecord,
|
||||||
|
NewAchievementIcon,
|
||||||
|
GeneralIconRecord,
|
||||||
|
NewGeneralIcon,
|
||||||
};
|
};
|
||||||
pub use xml_parser::XmlParseError;
|
pub use xml_parser::XmlParseError;
|
||||||
pub use image_processor::{ImageProcessor, ImageProcessingError, ProcessedImages, OutlineConfig};
|
pub use image_processor::{ImageProcessor, ImageProcessingError, ProcessedImages, OutlineConfig};
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
// @generated automatically by Diesel CLI.
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
achievement_icons (name) {
|
||||||
|
name -> Text,
|
||||||
|
icon -> Binary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
crafting_recipe_items (recipe_id, item_id) {
|
crafting_recipe_items (recipe_id, item_id) {
|
||||||
recipe_id -> Integer,
|
recipe_id -> Integer,
|
||||||
@@ -30,6 +37,18 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
general_icons (name) {
|
||||||
|
name -> Text,
|
||||||
|
original_width -> Integer,
|
||||||
|
original_height -> Integer,
|
||||||
|
icon_original -> Nullable<Binary>,
|
||||||
|
icon_256 -> Nullable<Binary>,
|
||||||
|
icon_64 -> Nullable<Binary>,
|
||||||
|
icon_32 -> Nullable<Binary>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
harvestable_drops (id) {
|
harvestable_drops (id) {
|
||||||
id -> Nullable<Integer>,
|
id -> Nullable<Integer>,
|
||||||
@@ -61,6 +80,14 @@ diesel::table! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
icons (category, name) {
|
||||||
|
category -> Text,
|
||||||
|
name -> Text,
|
||||||
|
icon -> Binary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
diesel::table! {
|
diesel::table! {
|
||||||
item_stats (item_id, stat_type) {
|
item_stats (item_id, stat_type) {
|
||||||
item_id -> Integer,
|
item_id -> Integer,
|
||||||
@@ -251,11 +278,14 @@ diesel::joinable!(harvestable_drops -> items (item_id));
|
|||||||
diesel::joinable!(item_stats -> items (item_id));
|
diesel::joinable!(item_stats -> items (item_id));
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
diesel::allow_tables_to_appear_in_same_query!(
|
||||||
|
achievement_icons,
|
||||||
crafting_recipe_items,
|
crafting_recipe_items,
|
||||||
crafting_recipes,
|
crafting_recipes,
|
||||||
fast_travel_locations,
|
fast_travel_locations,
|
||||||
|
general_icons,
|
||||||
harvestable_drops,
|
harvestable_drops,
|
||||||
harvestables,
|
harvestables,
|
||||||
|
icons,
|
||||||
item_stats,
|
item_stats,
|
||||||
items,
|
items,
|
||||||
loot_tables,
|
loot_tables,
|
||||||
|
|||||||
87
cursebreaker-parser/src/types/cursebreaker/icon_models.rs
Normal file
87
cursebreaker-parser/src/types/cursebreaker/icon_models.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use diesel::prelude::*;
|
||||||
|
use crate::schema::{icons, achievement_icons, general_icons};
|
||||||
|
|
||||||
|
/// Icon category for the icons table
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum IconCategory {
|
||||||
|
Ability,
|
||||||
|
Buff,
|
||||||
|
Trait,
|
||||||
|
PlayerHouse,
|
||||||
|
StatIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IconCategory {
|
||||||
|
pub fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
IconCategory::Ability => "ability",
|
||||||
|
IconCategory::Buff => "buff",
|
||||||
|
IconCategory::Trait => "trait",
|
||||||
|
IconCategory::PlayerHouse => "player_house",
|
||||||
|
IconCategory::StatIcon => "stat_icon",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel queryable model for icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct IconRecord {
|
||||||
|
pub category: String,
|
||||||
|
pub name: String,
|
||||||
|
pub icon: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = icons)]
|
||||||
|
pub struct NewIcon<'a> {
|
||||||
|
pub category: &'a str,
|
||||||
|
pub name: &'a str,
|
||||||
|
pub icon: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel queryable model for achievement_icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = achievement_icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct AchievementIconRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub icon: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for achievement_icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = achievement_icons)]
|
||||||
|
pub struct NewAchievementIcon<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub icon: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel queryable model for general_icons table
|
||||||
|
#[derive(Queryable, Selectable, Debug, Clone)]
|
||||||
|
#[diesel(table_name = general_icons)]
|
||||||
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
|
pub struct GeneralIconRecord {
|
||||||
|
pub name: String,
|
||||||
|
pub original_width: i32,
|
||||||
|
pub original_height: i32,
|
||||||
|
pub icon_original: Option<Vec<u8>>,
|
||||||
|
pub icon_256: Option<Vec<u8>>,
|
||||||
|
pub icon_64: Option<Vec<u8>>,
|
||||||
|
pub icon_32: Option<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diesel insertable model for general_icons table
|
||||||
|
#[derive(Insertable, Debug)]
|
||||||
|
#[diesel(table_name = general_icons)]
|
||||||
|
pub struct NewGeneralIcon<'a> {
|
||||||
|
pub name: &'a str,
|
||||||
|
pub original_width: i32,
|
||||||
|
pub original_height: i32,
|
||||||
|
pub icon_original: Option<&'a [u8]>,
|
||||||
|
pub icon_256: Option<&'a [u8]>,
|
||||||
|
pub icon_64: Option<&'a [u8]>,
|
||||||
|
pub icon_32: Option<&'a [u8]>,
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ mod r#trait;
|
|||||||
mod shop;
|
mod shop;
|
||||||
mod minimap_tile;
|
mod minimap_tile;
|
||||||
mod minimap_models;
|
mod minimap_models;
|
||||||
|
mod icon_models;
|
||||||
|
|
||||||
pub use item::{
|
pub use item::{
|
||||||
// Main types
|
// Main types
|
||||||
@@ -44,3 +45,12 @@ pub use r#trait::{Trait, TraitTrainer};
|
|||||||
pub use shop::{Shop, ShopItem};
|
pub use shop::{Shop, ShopItem};
|
||||||
pub use minimap_tile::MinimapTile;
|
pub use minimap_tile::MinimapTile;
|
||||||
pub use minimap_models::{MinimapTileRecord, NewMinimapTile};
|
pub use minimap_models::{MinimapTileRecord, NewMinimapTile};
|
||||||
|
pub use icon_models::{
|
||||||
|
IconCategory,
|
||||||
|
IconRecord,
|
||||||
|
NewIcon,
|
||||||
|
AchievementIconRecord,
|
||||||
|
NewAchievementIcon,
|
||||||
|
GeneralIconRecord,
|
||||||
|
NewGeneralIcon,
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user