//! Scene Parser - Parses Unity scenes and extracts game objects //! //! This binary handles: //! - Initializing the Unity project //! - Parsing Unity scenes with type filtering //! - 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, InteractableTeleporter, InteractableWorkbench, LootSpawner, MapIcon, MapNameChanger, ImageProcessor, OutlineConfig }; use unity_parser::{UnityProject, TypeFilter}; 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(); log::set_boxed_logger(Box::new(logger)) .map(|()| log::set_max_level(LevelFilter::Trace)) .unwrap(); info!("šŸŽ® Cursebreaker - Scene Parser"); 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 let project_root = Path::new(&cb_assets_path); info!("\nšŸ“¦ Initializing Unity project from: {}", project_root.display()); let project = UnityProject::from_path(project_root)?; // Create type filter to only parse GameObject, Transform, and InteractableResource MonoBehaviour info!("šŸ” Setting up type filter:"); info!(" • Unity types: GameObject, Transform"); info!(" • Custom MonoBehaviours: InteractableResource"); let type_filter = TypeFilter::new( vec!["GameObject", "Transform", "PrefabInstance"], vec!["InteractableResource", "InteractableTeleporter", "InteractableWorkbench", "LootSpawner", "MapIcon", "MapNameChanger"] ); // Now parse the scene using the pre-built GUID resolvers with filtering let scene_path = "_GameAssets/Scenes/Tiles/10_3.unity"; info!("šŸ“ Parsing scene: {}", scene_path); log::logger().flush(); // Parse the scene using the project with type filtering match project.parse_scene_filtered(scene_path, Some(&type_filter)) { Ok(mut scene) => { info!("āœ… Scene parsed successfully!"); info!(" Total entities: {}", scene.entity_map.len()); // Post-processing: Compute world transforms info!("šŸ”„ Computing world transforms..."); unity_parser::compute_world_transforms(&mut scene.world, &scene.entity_map); info!(" āœ“ World transforms computed"); // Save resources to database 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 use cursebreaker_parser::schema::world_resources; // Clear the entire table (it's regenerated each run) 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| { scene.world .query_all::<(&InteractableResource, &unity_parser::WorldTransform, &unity_parser::GameObject)>() .for_each(|(resource, transform, object)| { 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) .values(( world_resources::item_id.eq(resource.type_id as i32), world_resources::pos_x.eq(world_pos.x as f32), world_resources::pos_y.eq(world_pos.z as f32), )) .execute(conn); resource_count += 1; }); Ok(()) })?; 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)?; // 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(); 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, harvestables, harvestable_drops}; // Collect unique harvestable IDs from resources let mut unique_harvestables: HashMap = 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 let processor = ImageProcessor::default(); let outline_config = OutlineConfig::white(4); let mut processed_count = 0; let mut failed_count = 0; // Process each unique harvestable for (harvestable_id, default_name) in unique_harvestables.iter() { // Get the harvestable name let harvestable_name: String = harvestables::table .filter(harvestables::id.eq(harvestable_id)) .select(harvestables::name) .first(conn) .unwrap_or_else(|_| default_name.clone()); // Get the first item drop for this harvestable let item_id_result: Result = harvestable_drops::table .filter(harvestable_drops::harvestable_id.eq(harvestable_id)) .select(harvestable_drops::item_id) .order(harvestable_drops::id.asc()) .first(conn); let item_id = match item_id_result { Ok(id) => id, Err(_) => { warn!(" āš ļø No drops found for harvestable {} ({})", harvestable_id, harvestable_name); failed_count += 1; continue; } }; // Get the item name let item_name: String = items::table .filter(items::id.eq(&item_id)) .select(items::name) .first(conn) .unwrap_or_else(|_| format!("Item {}", item_id)); // Construct icon path using the item_id from the drop 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 harvestable {} ({}) -> item {} ({}): {}", harvestable_id, harvestable_name, 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 using harvestable_id as the key match diesel::insert_into(resource_icons::table) .values(( resource_icons::item_id.eq(harvestable_id), resource_icons::name.eq(&harvestable_name), resource_icons::icon_64.eq(icon_data.as_slice()), )) .execute(conn) { Ok(_) => { info!(" āœ“ Harvestable {} ({}) -> Item {} ({}): {} bytes", harvestable_id, harvestable_name, item_id, item_name, icon_data.len()); processed_count += 1; } Err(e) => { warn!(" āš ļø Failed to insert icon for harvestable {} ({}): {}", harvestable_id, harvestable_name, e); failed_count += 1; } } } } Err(e) => { warn!(" āš ļø Failed to process icon for harvestable {} ({}) -> item {} ({}): {}", harvestable_id, harvestable_name, item_id, item_name, e); failed_count += 1; } } } info!("āœ… Processed {} harvestable icons ({} succeeded, {} failed)", unique_harvestables.len(), processed_count, failed_count); Ok(()) } /// Save teleporter data to database fn save_teleporters( conn: &mut SqliteConnection, scene: &unity_parser::UnityScene, ) -> Result<(), Box> { use cursebreaker_parser::schema::world_teleporters; // Clear existing teleporters diesel::delete(world_teleporters::table).execute(conn)?; let mut count = 0; // Query all teleporters scene.world .query_all::<(&InteractableTeleporter, &unity_parser::WorldTransform, &unity_parser::GameObject)>() .for_each(|(teleporter, transform, object)| { let world_pos = transform.position(); // Get the tp_transform position if it exists let (tp_x, tp_y) = if let Some(tp_entity) = teleporter.tp_transform { if let Some(tp_transform) = scene.world.borrow::().get(tp_entity) { let tp_pos = tp_transform.position(); (Some(tp_pos.x as f32), Some(tp_pos.z as f32)) } else { (None, None) } } else { (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) .values(( world_teleporters::pos_x.eq(world_pos.x as f32), world_teleporters::pos_y.eq(world_pos.z as f32), world_teleporters::tp_x.eq(tp_x), world_teleporters::tp_y.eq(tp_y), )) .execute(conn); count += 1; }); info!("āœ… Saved {} teleporters to database", count); Ok(()) } /// Save workbench data to database fn save_workbenches( conn: &mut SqliteConnection, scene: &unity_parser::UnityScene, ) -> Result<(), Box> { use cursebreaker_parser::schema::world_workbenches; // Clear existing workbenches diesel::delete(world_workbenches::table).execute(conn)?; let mut count = 0; // Query all workbenches scene.world .query_all::<(&InteractableWorkbench, &unity_parser::WorldTransform, &unity_parser::GameObject)>() .for_each(|(workbench, transform, object)| { 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) .values(( world_workbenches::pos_x.eq(world_pos.x as f32), world_workbenches::pos_y.eq(world_pos.z as f32), world_workbenches::workbench_id.eq(workbench.workbench_id as i32), )) .execute(conn); count += 1; }); info!("āœ… Saved {} workbenches to database", count); Ok(()) } /// Save loot spawner data to database fn save_loot_spawners( conn: &mut SqliteConnection, scene: &unity_parser::UnityScene, ) -> Result<(), Box> { use cursebreaker_parser::schema::world_loot; // Clear existing loot spawners diesel::delete(world_loot::table).execute(conn)?; let mut count = 0; // Query all loot spawners scene.world .query_all::<(&LootSpawner, &unity_parser::WorldTransform, &unity_parser::GameObject)>() .for_each(|(loot, transform, object)| { 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) .values(( world_loot::pos_x.eq(world_pos.x as f32), world_loot::pos_y.eq(world_pos.z as f32), world_loot::item_id.eq(loot.item_id as i32), world_loot::amount.eq(loot.amount as i32), world_loot::respawn_time.eq(loot.respawn_time as i32), world_loot::visibility_checks.eq(&loot.visibility_checks), )) .execute(conn); count += 1; }); info!("āœ… Saved {} loot spawners to database", count); Ok(()) } /// Save map icon data to database fn save_map_icons( conn: &mut SqliteConnection, scene: &unity_parser::UnityScene, ) -> Result<(), Box> { use cursebreaker_parser::schema::world_map_icons; // Clear existing map icons diesel::delete(world_map_icons::table).execute(conn)?; let mut count = 0; // Query all map icons scene.world .query_all::<(&MapIcon, &unity_parser::WorldTransform, &unity_parser::GameObject)>() .for_each(|(icon, transform, object)| { 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) .values(( world_map_icons::pos_x.eq(world_pos.x as f32), world_map_icons::pos_y.eq(world_pos.z as f32), world_map_icons::icon_type.eq(icon.icon_type as i32), world_map_icons::icon_size.eq(icon.icon_size as i32), world_map_icons::icon.eq(&icon.icon), world_map_icons::text.eq(&icon.text), world_map_icons::font_size.eq(icon.font_size as i32), world_map_icons::hover_text.eq(&icon.hover_text), )) .execute(conn); count += 1; }); info!("āœ… Saved {} map icons to database", count); Ok(()) } /// Save map name changer data to database fn save_map_name_changers( conn: &mut SqliteConnection, scene: &unity_parser::UnityScene, ) -> Result<(), Box> { 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; // Query all map name changers scene.world .query_all::<(&MapNameChanger, &unity_parser::WorldTransform, &unity_parser::GameObject)>() .for_each(|(changer, transform, object)| { 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) .values(( world_map_name_changers::pos_x.eq(world_pos.x as f32), world_map_name_changers::pos_y.eq(world_pos.z as f32), world_map_name_changers::map_name.eq(&changer.map_name), )) .execute(conn); count += 1; }); info!("āœ… Saved {} map name changers to database", count); Ok(()) }