From e53deb57bb04f77acc51dabf1b1cbcddc9e4aa48 Mon Sep 17 00:00:00 2001 From: Connor Date: Mon, 12 Jan 2026 06:06:44 +0000 Subject: [PATCH] resource icons --- .claude/settings.local.json | 3 +- .../down.sql | 2 + .../up.sql | 8 ++ cursebreaker-parser/src/bin/scene-parser.rs | 100 +++++++++++++++++- .../src/bin/verify-resource-icons.rs | 40 +++++++ cursebreaker-parser/src/bin/xml-parser.rs | 14 +-- cursebreaker-parser/src/schema.rs | 9 ++ 7 files changed, 166 insertions(+), 10 deletions(-) create mode 100644 cursebreaker-parser/migrations/2026-01-12-044259-0000_create_resource_icons/down.sql create mode 100644 cursebreaker-parser/migrations/2026-01-12-044259-0000_create_resource_icons/up.sql create mode 100644 cursebreaker-parser/src/bin/verify-resource-icons.rs diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 89de8ba..fd49278 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -38,7 +38,8 @@ "Bash(DATABASE_URL=cursebreaker.db diesel migration:*)", "Bash(DATABASE_URL=../cursebreaker-parser/cursebreaker.db cargo run:*)", "Bash(identify:*)", - "Bash(diesel migration revert:*)" + "Bash(diesel migration revert:*)", + "Bash(xargs:*)" ], "additionalDirectories": [ "/home/connor/repos/CBAssets/" diff --git a/cursebreaker-parser/migrations/2026-01-12-044259-0000_create_resource_icons/down.sql b/cursebreaker-parser/migrations/2026-01-12-044259-0000_create_resource_icons/down.sql new file mode 100644 index 0000000..b903498 --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-12-044259-0000_create_resource_icons/down.sql @@ -0,0 +1,2 @@ +-- Drop resource_icons table +DROP TABLE resource_icons; diff --git a/cursebreaker-parser/migrations/2026-01-12-044259-0000_create_resource_icons/up.sql b/cursebreaker-parser/migrations/2026-01-12-044259-0000_create_resource_icons/up.sql new file mode 100644 index 0000000..b6f6952 --- /dev/null +++ b/cursebreaker-parser/migrations/2026-01-12-044259-0000_create_resource_icons/up.sql @@ -0,0 +1,8 @@ +-- Create resource_icons table to store processed item icons for world resources +CREATE TABLE resource_icons ( + item_id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + icon_64 BLOB NOT NULL +); + +CREATE INDEX idx_resource_icons_name ON resource_icons(name); diff --git a/cursebreaker-parser/src/bin/scene-parser.rs b/cursebreaker-parser/src/bin/scene-parser.rs index 17ca768..18622ce 100644 --- a/cursebreaker-parser/src/bin/scene-parser.rs +++ b/cursebreaker-parser/src/bin/scene-parser.rs @@ -6,15 +6,17 @@ //! - Extracting Interactable_Resource components only //! - Computing world transforms //! - Saving resource locations to the database +//! - Processing and saving item icons for resources -use cursebreaker_parser::InteractableResource; +use cursebreaker_parser::{InteractableResource, ImageProcessor, OutlineConfig}; use unity_parser::{UnityProject, TypeFilter}; -use std::path::Path; +use std::path::{Path, PathBuf}; use unity_parser::log::DedupLogger; use log::{info, error, warn, LevelFilter}; use std::env; use diesel::prelude::*; use diesel::sqlite::SqliteConnection; +use std::collections::HashMap; fn main() -> Result<(), Box> { let logger = DedupLogger::new(); @@ -98,6 +100,10 @@ fn main() -> Result<(), Box> { info!("✅ Saved {} resources to database", resource_count); log::logger().flush(); + + // Process and save item icons + info!("🎨 Processing item icons..."); + process_item_icons(&cb_assets_path, &mut conn, &scene)?; } Err(e) => { error!("Parse error: {}", e); @@ -109,3 +115,93 @@ fn main() -> Result<(), Box> { Ok(()) } + +/// Process item icons for all resources in the scene +fn process_item_icons( + cb_assets_path: &str, + conn: &mut SqliteConnection, + scene: &unity_parser::UnityScene, +) -> Result<(), Box> { + use cursebreaker_parser::schema::{resource_icons, items}; + + // Collect unique item IDs from resources + let mut unique_items: HashMap = HashMap::new(); + + scene.world + .query_all::<(&InteractableResource, &unity_parser::GameObject)>() + .for_each(|(resource, object)| { + unique_items.entry(resource.type_id as i32) + .or_insert_with(|| object.name.to_string()); + }); + + info!(" Found {} unique resource types", unique_items.len()); + + // Clear existing resource icons (regenerated each run) + diesel::delete(resource_icons::table).execute(conn)?; + + // Create image processor with white outline + let processor = ImageProcessor::default(); + let outline_config = OutlineConfig::white(1); + + let mut processed_count = 0; + let mut failed_count = 0; + + // Process each unique item + for (item_id, default_name) in unique_items.iter() { + // Try to get the actual item name from the items table + let item_name: String = items::table + .filter(items::id.eq(item_id)) + .select(items::name) + .first(conn) + .unwrap_or_else(|_| default_name.clone()); + + // Construct icon path + let icon_path = PathBuf::from(cb_assets_path) + .join("Data/Textures/ItemIcons") + .join(format!("{}.png", item_id)); + + if !icon_path.exists() { + warn!(" âš ī¸ Icon not found for item {} ({}): {}", item_id, item_name, icon_path.display()); + failed_count += 1; + continue; + } + + // Process the icon: resize to 64px with white outline + match processor.process_image(&icon_path, &[64], None, Some(&outline_config)) { + Ok(processed) => { + if let Some(icon_data) = processed.get(64) { + // Insert into database + match diesel::insert_into(resource_icons::table) + .values(( + resource_icons::item_id.eq(item_id), + resource_icons::name.eq(&item_name), + resource_icons::icon_64.eq(icon_data.as_slice()), + )) + .execute(conn) + { + Ok(_) => { + info!(" ✓ Processed icon for item {} ({}): {} bytes", + item_id, item_name, icon_data.len()); + processed_count += 1; + } + Err(e) => { + warn!(" âš ī¸ Failed to insert icon for item {} ({}): {}", + item_id, item_name, e); + failed_count += 1; + } + } + } + } + Err(e) => { + warn!(" âš ī¸ Failed to process icon for item {} ({}): {}", + item_id, item_name, e); + failed_count += 1; + } + } + } + + info!("✅ Processed {} item icons ({} succeeded, {} failed)", + unique_items.len(), processed_count, failed_count); + + Ok(()) +} diff --git a/cursebreaker-parser/src/bin/verify-resource-icons.rs b/cursebreaker-parser/src/bin/verify-resource-icons.rs new file mode 100644 index 0000000..555d4e8 --- /dev/null +++ b/cursebreaker-parser/src/bin/verify-resource-icons.rs @@ -0,0 +1,40 @@ +//! Verify resource_icons table + +use diesel::prelude::*; +use diesel::sqlite::SqliteConnection; +use std::env; + +fn main() -> Result<(), Box> { + use cursebreaker_parser::schema::resource_icons; + + let database_url = env::var("DATABASE_URL").unwrap_or_else(|_| "../cursebreaker.db".to_string()); + let mut conn = SqliteConnection::establish(&database_url)?; + + // Count total icons + let count: i64 = resource_icons::table.count().get_result(&mut conn)?; + println!("✅ Database contains {} resource icons\n", count); + + // Get all icons and show details + #[derive(Queryable, Debug)] + struct ResourceIcon { + item_id: i32, + name: String, + icon_64: Vec, + } + + let icons: Vec = resource_icons::table + .load(&mut conn)?; + + if icons.is_empty() { + println!("â„šī¸ No resource icons found in database."); + println!(" This is expected if no item icons were available during scene parsing."); + } else { + println!("Resource icons:"); + for icon in icons { + println!(" â€ĸ Item {}: {} ({} bytes)", + icon.item_id, icon.name, icon.icon_64.len()); + } + } + + Ok(()) +} diff --git a/cursebreaker-parser/src/bin/xml-parser.rs b/cursebreaker-parser/src/bin/xml-parser.rs index 121dcf0..77ee15c 100644 --- a/cursebreaker-parser/src/bin/xml-parser.rs +++ b/cursebreaker-parser/src/bin/xml-parser.rs @@ -36,9 +36,9 @@ fn main() -> Result<(), Box> { // let quest_db = QuestDatabase::load_from_xml(quests_path)?; // info!("✅ Loaded {} quests", quest_db.len()); - // let harvestables_path = format!("{}/Data/XMLs/Harvestables/HarvestableInfo.xml", cb_assets_path); - // let harvestable_db = HarvestableDatabase::load_from_xml(harvestables_path)?; - // info!("✅ Loaded {} harvestables", harvestable_db.len()); + let harvestables_path = format!("{}/Data/XMLs/Harvestables/HarvestableInfo.xml", cb_assets_path); + let harvestable_db = HarvestableDatabase::load_from_xml(harvestables_path)?; + info!("✅ Loaded {} harvestables", harvestable_db.len()); // let loot_path = format!("{}/Data/XMLs/Loot/Loot.xml", cb_assets_path); // let loot_db = LootDatabase::load_from_xml(loot_path)?; @@ -91,10 +91,10 @@ fn main() -> Result<(), Box> { // 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 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), diff --git a/cursebreaker-parser/src/schema.rs b/cursebreaker-parser/src/schema.rs index 8e43bc5..b540eb4 100644 --- a/cursebreaker-parser/src/schema.rs +++ b/cursebreaker-parser/src/schema.rs @@ -135,6 +135,14 @@ diesel::table! { } } +diesel::table! { + resource_icons (item_id) { + item_id -> Integer, + name -> Text, + icon_64 -> Binary, + } +} + diesel::table! { shops (id) { id -> Nullable, @@ -181,6 +189,7 @@ diesel::allow_tables_to_appear_in_same_query!( npcs, player_houses, quests, + resource_icons, shops, traits, world_resources,