Files
cursebreaker-parser-rust/cursebreaker-parser/src/bin/scene-parser.rs
2026-01-16 09:33:30 +00:00

452 lines
17 KiB
Rust

//! 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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
use cursebreaker_parser::schema::{resource_icons, items, harvestables, harvestable_drops};
// Collect unique harvestable IDs from resources
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
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<i32, _> = 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<dyn std::error::Error>> {
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::<unity_parser::WorldTransform>().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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}