452 lines
17 KiB
Rust
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(())
|
|
}
|