de-duplicate logger
This commit is contained in:
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -28,6 +28,7 @@ name = "cursebreaker-parser"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"inventory",
|
||||
"log",
|
||||
"serde_yaml",
|
||||
"sparsey",
|
||||
"unity-parser",
|
||||
@@ -104,6 +105,12 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.12.5"
|
||||
@@ -313,6 +320,7 @@ dependencies = [
|
||||
"glam",
|
||||
"indexmap",
|
||||
"inventory",
|
||||
"log",
|
||||
"lru",
|
||||
"once_cell",
|
||||
"pretty_assertions",
|
||||
|
||||
@@ -8,3 +8,4 @@ unity-parser = { path = "../unity-parser" }
|
||||
serde_yaml = "0.9"
|
||||
inventory = "0.3"
|
||||
sparsey = "0.13"
|
||||
log = { version = "0.4", features = ["std"] }
|
||||
|
||||
@@ -13,29 +13,34 @@ use unity_parser::UnityFile;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use unity_parser::log::DedupLogger;
|
||||
use log::{info, warn, error, LevelFilter};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🎮 Cursebreaker - Resource Parser");
|
||||
println!("{}", "=".repeat(70));
|
||||
println!();
|
||||
|
||||
let logger = DedupLogger::new();
|
||||
log::set_boxed_logger(Box::new(logger))
|
||||
.map(|()| log::set_max_level(LevelFilter::Trace))
|
||||
.unwrap();
|
||||
log::set_max_level(LevelFilter::Warn);
|
||||
|
||||
info!("🎮 Cursebreaker - Resource Parser");
|
||||
|
||||
let scene_path = Path::new("/home/connor/repos/CBAssets/_GameAssets/Scenes/Tiles/10_3.unity");
|
||||
|
||||
// Check if scene exists
|
||||
if !scene_path.exists() {
|
||||
eprintln!("❌ Error: Scene not found at {}", scene_path.display());
|
||||
error!("Scene not found at {}", scene_path.display());
|
||||
return Err("Scene file not found".into());
|
||||
}
|
||||
|
||||
println!("📁 Parsing scene: {}", scene_path.display());
|
||||
println!();
|
||||
info!("📁 Parsing scene: {}", scene_path.display());
|
||||
|
||||
// Parse the scene
|
||||
match UnityFile::from_path(&scene_path) {
|
||||
Ok(UnityFile::Scene(scene)) => {
|
||||
println!("✅ Scene parsed successfully!");
|
||||
println!(" Total entities: {}", scene.entity_map.len());
|
||||
println!();
|
||||
info!("✅ Scene parsed successfully!");
|
||||
info!(" Total entities: {}", scene.entity_map.len());
|
||||
|
||||
// Get views for component types we need
|
||||
let resource_view = scene.world.borrow::<InteractableResource>();
|
||||
@@ -62,21 +67,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
println!("🔍 Found {} Interactable_Resource component(s)", found_resources.len());
|
||||
println!();
|
||||
info!("🔍 Found {} Interactable_Resource component(s)", found_resources.len());
|
||||
|
||||
if !found_resources.is_empty() {
|
||||
// Display resources in console
|
||||
for (name, resource, position) in &found_resources {
|
||||
println!(" 📦 Resource: \"{}\"", name);
|
||||
println!(" • typeId: {}", resource.type_id);
|
||||
println!(" • maxHealth: {}", resource.max_health);
|
||||
info!(" 📦 Resource: \"{}\"", name);
|
||||
info!(" • typeId: {}", resource.type_id);
|
||||
info!(" • maxHealth: {}", resource.max_health);
|
||||
if let Some((x, y, z)) = position {
|
||||
println!(" • Position: ({:.2}, {:.2}, {:.2})", x, y, z);
|
||||
info!(" • Position: ({:.2}, {:.2}, {:.2})", x, y, z);
|
||||
} else {
|
||||
println!(" • Position: (no transform)");
|
||||
info!(" • Position: (no transform)");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// Write to output file
|
||||
@@ -106,23 +109,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
writeln!(output_file, "{}", "=".repeat(70))?;
|
||||
writeln!(output_file, "End of resource data")?;
|
||||
|
||||
println!("📝 Resource data written to: {}", output_path);
|
||||
println!();
|
||||
info!("📝 Resource data written to: {}", output_path);
|
||||
}
|
||||
|
||||
println!("{}", "=".repeat(70));
|
||||
println!("✅ Parsing complete!");
|
||||
println!("{}", "=".repeat(70));
|
||||
info!("✅ Parsing complete!");
|
||||
}
|
||||
Ok(_) => {
|
||||
eprintln!("❌ Error: File is not a scene");
|
||||
error!("File is not a scene");
|
||||
return Err("Not a Unity scene file".into());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("❌ Parse error: {}", e);
|
||||
error!("Parse error: {}", e);
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
}
|
||||
|
||||
log::logger().flush();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@ Total resources found: 2
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Resource: HarvestableSpawner_11Redberries
|
||||
TypeID: 11
|
||||
MaxHealth: 0
|
||||
Position: (1769.135864, 32.664658, 150.395081)
|
||||
|
||||
Resource: HarvestableSpawner_38Dandelions
|
||||
TypeID: 38
|
||||
MaxHealth: 0
|
||||
Position: (1746.709717, 44.599632, 299.696503)
|
||||
|
||||
Resource: HarvestableSpawner_2Copper Ore
|
||||
TypeID: 2
|
||||
MaxHealth: 0
|
||||
Position: (1788.727173, 40.725288, 172.017670)
|
||||
|
||||
======================================================================
|
||||
End of resource data
|
||||
|
||||
@@ -52,6 +52,9 @@ smallvec = "1.13"
|
||||
# Procedural macro for derive(UnityComponent)
|
||||
unity-parser-macros = { path = "../unity-parser-macros" }
|
||||
|
||||
# Logging
|
||||
log = "0.4"
|
||||
|
||||
[dev-dependencies]
|
||||
# Testing utilities
|
||||
pretty_assertions = "1.4"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! ECS world building from Unity documents
|
||||
|
||||
use log::{info, warn};
|
||||
|
||||
use crate::model::RawDocument;
|
||||
use crate::parser::{GuidResolver, PrefabGuidResolver};
|
||||
use crate::types::{
|
||||
@@ -104,7 +106,7 @@ pub fn build_world_from_documents(
|
||||
.collect();
|
||||
drop(prefab_view); // Release the borrow
|
||||
|
||||
eprintln!("Pass 2.5: Found {} prefab instance(s) to resolve", prefab_entities.len());
|
||||
info!("Pass 2.5: Found {} prefab instance(s) to resolve", prefab_entities.len());
|
||||
|
||||
for (entity, component) in prefab_entities {
|
||||
match prefab_resolver.instantiate_from_component(
|
||||
@@ -114,12 +116,12 @@ pub fn build_world_from_documents(
|
||||
linking_ctx.borrow_mut().entity_map_mut(),
|
||||
) {
|
||||
Ok(spawned) => {
|
||||
eprintln!(" ✅ Spawned {} entities from prefab GUID: {}",
|
||||
info!("Spawned {} entities from prefab GUID: {}",
|
||||
spawned.len(), component.prefab_ref.guid);
|
||||
}
|
||||
Err(e) => {
|
||||
// Soft failure - warn but continue
|
||||
eprintln!(" ⚠️ Warning: Failed to instantiate prefab: {}", e);
|
||||
warn!("Failed to instantiate prefab: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +231,7 @@ pub fn build_world_from_documents_into(
|
||||
drop(prefab_view); // Release the borrow
|
||||
|
||||
if !prefab_entities.is_empty() {
|
||||
eprintln!("Pass 2.5 (nested): Found {} prefab instance(s) to resolve", prefab_entities.len());
|
||||
info!("Pass 2.5 (nested): Found {} prefab instance(s) to resolve", prefab_entities.len());
|
||||
}
|
||||
|
||||
for (entity, component) in prefab_entities {
|
||||
@@ -240,11 +242,11 @@ pub fn build_world_from_documents_into(
|
||||
linking_ctx.borrow_mut().entity_map_mut(),
|
||||
) {
|
||||
Ok(spawned) => {
|
||||
eprintln!(" ✅ Spawned {} entities from nested prefab", spawned.len());
|
||||
info!("Spawned {} entities from nested prefab", spawned.len());
|
||||
spawned_entities.extend(spawned);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ⚠️ Warning: Failed to instantiate nested prefab: {}", e);
|
||||
warn!("Failed to instantiate nested prefab: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,8 +315,8 @@ fn attach_component(
|
||||
})?,
|
||||
None => {
|
||||
// Some components might not have m_GameObject (e.g., standalone assets)
|
||||
eprintln!(
|
||||
"Warning: Component {} has no m_GameObject reference",
|
||||
warn!(
|
||||
"Component {} has no m_GameObject reference",
|
||||
doc.class_name
|
||||
);
|
||||
return Ok(());
|
||||
@@ -384,25 +386,25 @@ fn attach_component(
|
||||
|
||||
if !found_custom {
|
||||
// GUID resolved but no registered component found
|
||||
eprintln!(
|
||||
"Warning: Skipping MonoBehaviour '{}' (no registered parser)",
|
||||
warn!(
|
||||
"Skipping MonoBehaviour '{}' (no registered parser)",
|
||||
class_name
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// GUID not found in resolver
|
||||
eprintln!(
|
||||
"Warning: Could not resolve MonoBehaviour GUID: {}",
|
||||
warn!(
|
||||
"Could not resolve MonoBehaviour GUID: {}",
|
||||
script_ref.guid
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// No m_Script reference found
|
||||
eprintln!("Warning: MonoBehaviour missing m_Script reference");
|
||||
warn!("MonoBehaviour missing m_Script reference");
|
||||
}
|
||||
} else {
|
||||
// No GUID resolver available
|
||||
eprintln!("Warning: Skipping MonoBehaviour (no GUID resolver available)");
|
||||
warn!("Skipping MonoBehaviour (no GUID resolver available)");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -422,8 +424,8 @@ fn attach_component(
|
||||
|
||||
if !found_custom {
|
||||
// Unknown component type - skip with warning
|
||||
eprintln!(
|
||||
"Warning: Skipping unknown component type: {}",
|
||||
warn!(
|
||||
"Skipping unknown component type: {}",
|
||||
doc.class_name
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
//! let file = UnityFile::from_path("Scene.unity")?;
|
||||
//! match file {
|
||||
//! UnityFile::Scene(scene) => {
|
||||
//! println!("Scene with {} entities", scene.entity_map.len());
|
||||
//! info!("Scene with {} entities", scene.entity_map.len());
|
||||
//! // Access scene.world for ECS queries
|
||||
//! }
|
||||
//! UnityFile::Prefab(prefab) => {
|
||||
//! println!("Prefab with {} documents", prefab.documents.len());
|
||||
//! info!("Prefab with {} documents", prefab.documents.len());
|
||||
//! }
|
||||
//! UnityFile::Asset(asset) => {
|
||||
//! println!("Asset with {} documents", asset.documents.len());
|
||||
//! info!("Asset with {} documents", asset.documents.len());
|
||||
//! }
|
||||
//! }
|
||||
//! # Ok::<(), unity_parser::Error>(())
|
||||
@@ -27,6 +27,7 @@
|
||||
// Public modules
|
||||
pub mod ecs;
|
||||
pub mod error;
|
||||
pub mod log;
|
||||
pub mod macros;
|
||||
pub mod model;
|
||||
pub mod parser;
|
||||
|
||||
107
unity-parser/src/log/dedup_logger.rs
Normal file
107
unity-parser/src/log/dedup_logger.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use log::{Level, Log, Metadata, Record};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// Entry storing deduplication information for a log message
|
||||
#[derive(Debug, Clone)]
|
||||
struct LogEntry {
|
||||
count: usize,
|
||||
last_logged: SystemTime,
|
||||
level: Level,
|
||||
}
|
||||
|
||||
/// A logger that deduplicates messages and batches output until flush is called
|
||||
pub struct DedupLogger {
|
||||
entries: Mutex<HashMap<String, LogEntry>>,
|
||||
}
|
||||
|
||||
impl DedupLogger {
|
||||
/// Create a new DedupLogger
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Flush all accumulated log messages to stdout, sorted by level then timestamp
|
||||
pub fn flush(&self) {
|
||||
let mut entries = self.entries.lock().unwrap();
|
||||
|
||||
if entries.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert HashMap to Vec for sorting
|
||||
let mut messages: Vec<(String, LogEntry)> = entries
|
||||
.drain()
|
||||
.collect();
|
||||
|
||||
// Sort by level (descending: Trace > Debug > Info > Warn > Error)
|
||||
// Then by timestamp (ascending: oldest first)
|
||||
messages.sort_by(|a, b| {
|
||||
let level_cmp = b.1.level.cmp(&a.1.level);
|
||||
if level_cmp == std::cmp::Ordering::Equal {
|
||||
a.1.last_logged.cmp(&b.1.last_logged)
|
||||
} else {
|
||||
level_cmp
|
||||
}
|
||||
});
|
||||
|
||||
// Print all messages
|
||||
for (message, entry) in messages {
|
||||
if entry.count == 1 {
|
||||
println!("[{}] {}", entry.level, message);
|
||||
} else {
|
||||
println!("[{}] {} (x{})", entry.level, message, entry.count);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DedupLogger {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Log for DedupLogger {
|
||||
fn enabled(&self, _metadata: &Metadata) -> bool {
|
||||
// Accept all log levels
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record) {
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = format!("{}", record.args());
|
||||
let level = record.level();
|
||||
let now = SystemTime::now();
|
||||
|
||||
let mut entries = self.entries.lock().unwrap();
|
||||
|
||||
entries
|
||||
.entry(message)
|
||||
.and_modify(|entry| {
|
||||
entry.count += 1;
|
||||
entry.last_logged = now;
|
||||
// Update to highest severity level if it changed
|
||||
if level < entry.level {
|
||||
entry.level = level;
|
||||
}
|
||||
})
|
||||
.or_insert(LogEntry {
|
||||
count: 1,
|
||||
last_logged: now,
|
||||
level,
|
||||
});
|
||||
}
|
||||
|
||||
fn flush(&self) {
|
||||
// The flush method in Log trait is called by the log crate
|
||||
// We delegate to our custom flush implementation
|
||||
self.flush();
|
||||
}
|
||||
}
|
||||
3
unity-parser/src/log/mod.rs
Normal file
3
unity-parser/src/log/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod dedup_logger;
|
||||
|
||||
pub use dedup_logger::DedupLogger;
|
||||
@@ -24,11 +24,13 @@
|
||||
//! // Resolve a GUID to class name
|
||||
//! let guid = "091c537484687e9419460cdcd7038234";
|
||||
//! if let Some(class_name) = resolver.resolve_class_name(guid) {
|
||||
//! println!("GUID {} → {}", guid, class_name);
|
||||
//! info!("GUID {} → {}", guid, class_name);
|
||||
//! }
|
||||
//! # Ok::<(), unity_parser::Error>(())
|
||||
//! ```
|
||||
|
||||
use log::warn;
|
||||
|
||||
use crate::parser::meta::MetaFile;
|
||||
use crate::types::Guid;
|
||||
use crate::{Error, Result};
|
||||
@@ -111,7 +113,7 @@ impl GuidResolver {
|
||||
let meta = match MetaFile::from_path(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to parse {}: {}", path.display(), e);
|
||||
warn!("Failed to parse {}: {}", path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -132,8 +134,8 @@ impl GuidResolver {
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Failed to extract class name from {}: {}",
|
||||
warn!(
|
||||
"Failed to extract class name from {}: {}",
|
||||
cs_path.display(),
|
||||
e
|
||||
);
|
||||
@@ -145,8 +147,8 @@ impl GuidResolver {
|
||||
let guid = match Guid::from_hex(meta.guid()) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Invalid GUID in {}: {}",
|
||||
warn!(
|
||||
"Invalid GUID in {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
@@ -181,13 +183,13 @@ impl GuidResolver {
|
||||
/// # let resolver = GuidResolver::from_project(Path::new("."))?;
|
||||
/// // Resolve by string
|
||||
/// if let Some(class_name) = resolver.resolve_class_name("091c537484687e9419460cdcd7038234") {
|
||||
/// println!("Found class: {}", class_name);
|
||||
/// info!("Found class: {}", class_name);
|
||||
/// }
|
||||
///
|
||||
/// // Resolve by Guid
|
||||
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234")?;
|
||||
/// if let Some(class_name) = resolver.resolve_class_name(&guid) {
|
||||
/// println!("Found class: {}", class_name);
|
||||
/// info!("Found class: {}", class_name);
|
||||
/// }
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
|
||||
@@ -32,7 +32,7 @@ impl MetaFile {
|
||||
/// use unity_parser::parser::meta::MetaFile;
|
||||
///
|
||||
/// let meta = MetaFile::from_path("Assets/Prefabs/Player.prefab.meta")?;
|
||||
/// println!("GUID: {}", meta.guid);
|
||||
/// info!("GUID: {}", meta.guid);
|
||||
/// # Ok::<(), unity_parser::Error>(())
|
||||
/// ```
|
||||
pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
|
||||
|
||||
@@ -12,6 +12,8 @@ pub use prefab_guid_resolver::PrefabGuidResolver;
|
||||
pub use unity_tag::{parse_unity_tag, UnityTag};
|
||||
pub use yaml::split_yaml_documents;
|
||||
|
||||
use log::{info, warn};
|
||||
|
||||
use crate::model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
||||
use crate::types::{FileID, Guid, TypeFilter};
|
||||
use crate::{Error, Result};
|
||||
@@ -148,16 +150,16 @@ fn parse_scene(path: &Path, content: &str, type_filter: Option<&TypeFilter>) ->
|
||||
// Try to find Unity project root and build both GUID resolvers
|
||||
let (guid_resolver, prefab_guid_resolver) = match find_project_root(path) {
|
||||
Ok(project_root) => {
|
||||
eprintln!("📦 Found Unity project root: {}", project_root.display());
|
||||
info!("📦 Found Unity project root: {}", project_root.display());
|
||||
|
||||
// Build script GUID resolver
|
||||
let guid_res = match GuidResolver::from_project(&project_root) {
|
||||
Ok(resolver) => {
|
||||
eprintln!(" ✅ Script GUID resolver built ({} mappings)", resolver.len());
|
||||
info!("Script GUID resolver built ({} mappings)", resolver.len());
|
||||
Some(resolver)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ⚠️ Warning: Failed to build script GUID resolver: {}", e);
|
||||
warn!("Failed to build script GUID resolver: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
@@ -165,11 +167,11 @@ fn parse_scene(path: &Path, content: &str, type_filter: Option<&TypeFilter>) ->
|
||||
// Build prefab GUID resolver
|
||||
let prefab_res = match PrefabGuidResolver::from_project(&project_root) {
|
||||
Ok(resolver) => {
|
||||
eprintln!(" ✅ Prefab GUID resolver built ({} mappings)", resolver.len());
|
||||
info!("Prefab GUID resolver built ({} mappings)", resolver.len());
|
||||
Some(resolver)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" ⚠️ Warning: Failed to build prefab GUID resolver: {}", e);
|
||||
warn!("Failed to build prefab GUID resolver: {}", e);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
//! # Ok::<(), unity_parser::Error>(())
|
||||
//! ```
|
||||
|
||||
use log::warn;
|
||||
|
||||
use crate::parser::meta::MetaFile;
|
||||
use crate::types::Guid;
|
||||
use crate::{Error, Result};
|
||||
@@ -107,7 +109,7 @@ impl PrefabGuidResolver {
|
||||
let meta = match MetaFile::from_path(path) {
|
||||
Ok(meta) => meta,
|
||||
Err(e) => {
|
||||
eprintln!("Warning: Failed to parse {}: {}", path.display(), e);
|
||||
warn!("Failed to parse {}: {}", path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -124,8 +126,8 @@ impl PrefabGuidResolver {
|
||||
let guid = match Guid::from_hex(meta.guid()) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Warning: Invalid GUID in {}: {}",
|
||||
warn!(
|
||||
"Invalid GUID in {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Transform and RectTransform component wrappers
|
||||
|
||||
use log::warn;
|
||||
|
||||
use crate::types::{yaml_helpers, ComponentContext, Quaternion, UnityComponent, Vector2, Vector3};
|
||||
use sparsey::Entity;
|
||||
|
||||
@@ -104,10 +106,7 @@ impl UnityComponent for Transform {
|
||||
if let Some(child_entity) = entity_map.get(child_file_id).copied() {
|
||||
all_children.push(child_entity);
|
||||
} else {
|
||||
eprintln!(
|
||||
"Warning: Could not resolve child Transform: {}",
|
||||
child_file_id
|
||||
);
|
||||
warn!("Could not resolve child Transform");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,10 +263,7 @@ impl UnityComponent for RectTransform {
|
||||
if let Some(child_entity) = entity_map.get(child_file_id).copied() {
|
||||
all_children.push(child_entity);
|
||||
} else {
|
||||
eprintln!(
|
||||
"Warning: Could not resolve child RectTransform: {}",
|
||||
child_file_id
|
||||
);
|
||||
warn!("Could not resolve child RectTransform");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,554 +0,0 @@
|
||||
//! Integration tests for parsing real Unity projects
|
||||
|
||||
use unity_parser::{GuidResolver, UnityFile};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::time::Instant;
|
||||
|
||||
/// Test project configuration
|
||||
struct TestProject {
|
||||
name: &'static str,
|
||||
repo_url: &'static str,
|
||||
branch: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl TestProject {
|
||||
const VR_HORROR: TestProject = TestProject {
|
||||
name: "VR_Horror_YouCantRun",
|
||||
repo_url: "https://github.com/Unity3D-Projects/VR_Horror_YouCantRun.git",
|
||||
branch: None,
|
||||
};
|
||||
|
||||
const PIRATE_PANIC: TestProject = TestProject {
|
||||
name: "PiratePanic",
|
||||
repo_url: "https://github.com/Unity-Technologies/PiratePanic.git",
|
||||
branch: None,
|
||||
};
|
||||
}
|
||||
|
||||
/// Statistics gathered during parsing
|
||||
#[derive(Debug, Default)]
|
||||
struct ParsingStats {
|
||||
total_files: usize,
|
||||
scenes: usize,
|
||||
prefabs: usize,
|
||||
assets: usize,
|
||||
errors: Vec<(PathBuf, String)>,
|
||||
total_entities: usize,
|
||||
total_documents: usize,
|
||||
parse_time_ms: u128,
|
||||
}
|
||||
|
||||
impl ParsingStats {
|
||||
fn print_summary(&self) {
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!("Parsing Statistics");
|
||||
println!("{}", "=".repeat(60));
|
||||
println!(" Total files found: {}", self.total_files);
|
||||
println!(" Scenes parsed: {}", self.scenes);
|
||||
println!(" Prefabs parsed: {}", self.prefabs);
|
||||
println!(" Assets parsed: {}", self.assets);
|
||||
println!(" Total entities: {}", self.total_entities);
|
||||
println!(" Total documents: {}", self.total_documents);
|
||||
println!(" Parse time: {} ms", self.parse_time_ms);
|
||||
|
||||
if !self.errors.is_empty() {
|
||||
println!("\n Errors encountered: {}", self.errors.len());
|
||||
println!("\n Error details:");
|
||||
for (path, error) in self.errors.iter().take(10) {
|
||||
println!(" - {}", path.display());
|
||||
println!(" Error: {}", error);
|
||||
}
|
||||
if self.errors.len() > 10 {
|
||||
println!(" ... and {} more errors", self.errors.len() - 10);
|
||||
}
|
||||
}
|
||||
|
||||
let success_rate = if self.total_files > 0 {
|
||||
((self.total_files - self.errors.len()) as f64 / self.total_files as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
println!("\n Success rate: {:.2}%", success_rate);
|
||||
println!("{}", "=".repeat(60));
|
||||
}
|
||||
}
|
||||
|
||||
/// Clone a git repository for testing
|
||||
fn clone_test_project(project: &TestProject) -> std::io::Result<PathBuf> {
|
||||
let test_data_dir = PathBuf::from("test_data");
|
||||
std::fs::create_dir_all(&test_data_dir)?;
|
||||
|
||||
let project_path = test_data_dir.join(project.name);
|
||||
|
||||
// Skip if already cloned
|
||||
if project_path.exists() {
|
||||
println!("Project already cloned at: {}", project_path.display());
|
||||
return Ok(project_path);
|
||||
}
|
||||
|
||||
println!("Cloning {} from {}...", project.name, project.repo_url);
|
||||
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.arg("clone");
|
||||
|
||||
if let Some(branch) = project.branch {
|
||||
cmd.arg("--branch").arg(branch);
|
||||
}
|
||||
|
||||
cmd.arg("--depth").arg("1"); // Shallow clone for speed
|
||||
cmd.arg(project.repo_url);
|
||||
cmd.arg(&project_path);
|
||||
|
||||
let output = cmd.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
eprintln!("Git clone failed: {}", String::from_utf8_lossy(&output.stderr));
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Git clone failed",
|
||||
));
|
||||
}
|
||||
|
||||
println!("Successfully cloned to: {}", project_path.display());
|
||||
Ok(project_path)
|
||||
}
|
||||
|
||||
/// Recursively find all Unity files in a directory
|
||||
fn find_unity_files(dir: &Path) -> Vec<PathBuf> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
if !dir.exists() || !dir.is_dir() {
|
||||
return files;
|
||||
}
|
||||
|
||||
fn visit_dir(dir: &Path, files: &mut Vec<PathBuf>) {
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Skip common Unity directories that don't contain source assets
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name == "Library" || name == "Temp" || name == "Builds" || name == ".git" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
visit_dir(&path, files);
|
||||
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||
if ext == "unity" || ext == "prefab" || ext == "asset" {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit_dir(dir, &mut files);
|
||||
files
|
||||
}
|
||||
|
||||
/// Parse all Unity files in a project and collect statistics
|
||||
fn parse_project(project_path: &Path) -> ParsingStats {
|
||||
let mut stats = ParsingStats::default();
|
||||
|
||||
println!("\nFinding Unity files in {}...", project_path.display());
|
||||
let files = find_unity_files(project_path);
|
||||
stats.total_files = files.len();
|
||||
|
||||
println!("Found {} Unity files", files.len());
|
||||
println!("\nParsing files...");
|
||||
|
||||
let start_time = Instant::now();
|
||||
|
||||
for (i, file_path) in files.iter().enumerate() {
|
||||
// Print progress
|
||||
if (i + 1) % 10 == 0 || i == 0 {
|
||||
println!(
|
||||
" [{}/{}] Parsing: {}",
|
||||
i + 1,
|
||||
files.len(),
|
||||
file_path.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
}
|
||||
|
||||
match UnityFile::from_path(file_path) {
|
||||
Ok(unity_file) => match unity_file {
|
||||
UnityFile::Scene(scene) => {
|
||||
stats.scenes += 1;
|
||||
stats.total_entities += scene.entity_map.len();
|
||||
}
|
||||
UnityFile::Prefab(prefab) => {
|
||||
stats.prefabs += 1;
|
||||
stats.total_documents += prefab.documents.len();
|
||||
}
|
||||
UnityFile::Asset(asset) => {
|
||||
stats.assets += 1;
|
||||
stats.total_documents += asset.documents.len();
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
stats.errors.push((file_path.clone(), e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stats.parse_time_ms = start_time.elapsed().as_millis();
|
||||
stats
|
||||
}
|
||||
|
||||
/// Test parsing a specific project
|
||||
fn test_project(project: &TestProject) {
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!("Testing: {}", project.name);
|
||||
println!("{}", "=".repeat(60));
|
||||
|
||||
// Clone the project
|
||||
let project_path = match clone_test_project(project) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to clone project: {}", e);
|
||||
eprintln!("Skipping project test (git may not be available)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Parse all files
|
||||
let stats = parse_project(&project_path);
|
||||
|
||||
// Print summary
|
||||
stats.print_summary();
|
||||
|
||||
// Assert basic expectations
|
||||
assert!(
|
||||
stats.total_files > 0,
|
||||
"Should find at least some Unity files"
|
||||
);
|
||||
|
||||
// Allow some errors but not too many
|
||||
let error_rate = if stats.total_files > 0 {
|
||||
(stats.errors.len() as f64 / stats.total_files as f64) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if error_rate > 50.0 {
|
||||
panic!(
|
||||
"Error rate too high: {:.2}% ({}/{})",
|
||||
error_rate, stats.errors.len(), stats.total_files
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Test detailed parsing of specific file types
|
||||
fn test_detailed_parsing(project_path: &Path) {
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!("Detailed Parsing Tests");
|
||||
println!("{}", "=".repeat(60));
|
||||
|
||||
let files = find_unity_files(project_path);
|
||||
|
||||
// Test scene parsing
|
||||
if let Some(scene_file) = files.iter().find(|f| {
|
||||
f.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map_or(false, |e| e == "unity")
|
||||
}) {
|
||||
println!(
|
||||
"\nTesting scene parsing: {}",
|
||||
scene_file.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
match UnityFile::from_path(scene_file) {
|
||||
Ok(UnityFile::Scene(scene)) => {
|
||||
println!(" ✓ Successfully parsed scene");
|
||||
println!(" - Entities: {}", scene.entity_map.len());
|
||||
println!(" - Path: {}", scene.path.display());
|
||||
|
||||
// Try to access entities
|
||||
for (file_id, entity) in scene.entity_map.iter().take(3) {
|
||||
println!(" - FileID {} -> Entity {:?}", file_id, entity);
|
||||
}
|
||||
}
|
||||
Ok(_) => println!(" ✗ File was not parsed as scene"),
|
||||
Err(e) => println!(" ✗ Parse error: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
// Test prefab parsing and instancing
|
||||
if let Some(prefab_file) = files.iter().find(|f| {
|
||||
f.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map_or(false, |e| e == "prefab")
|
||||
}) {
|
||||
println!(
|
||||
"\nTesting prefab parsing: {}",
|
||||
prefab_file.file_name().unwrap().to_string_lossy()
|
||||
);
|
||||
match UnityFile::from_path(prefab_file) {
|
||||
Ok(UnityFile::Prefab(prefab)) => {
|
||||
println!(" ✓ Successfully parsed prefab");
|
||||
println!(" - Documents: {}", prefab.documents.len());
|
||||
println!(" - Path: {}", prefab.path.display());
|
||||
|
||||
// Test instantiation
|
||||
println!("\n Testing prefab instantiation:");
|
||||
let instance = prefab.instantiate();
|
||||
println!(
|
||||
" ✓ Created instance with {} remapped FileIDs",
|
||||
instance.file_id_map().len()
|
||||
);
|
||||
|
||||
// Test override system
|
||||
if let Some(first_doc) = prefab.documents.first() {
|
||||
let mut instance2 = prefab.instantiate();
|
||||
let result = instance2.override_value(
|
||||
first_doc.file_id,
|
||||
"m_Name",
|
||||
serde_yaml::Value::String("TestName".to_string()),
|
||||
);
|
||||
if result.is_ok() {
|
||||
println!(" ✓ Override system working");
|
||||
} else {
|
||||
println!(" - Override test: {}", result.unwrap_err());
|
||||
}
|
||||
}
|
||||
|
||||
// List document types
|
||||
let mut type_counts = std::collections::HashMap::new();
|
||||
for doc in &prefab.documents {
|
||||
*type_counts.entry(&doc.class_name).or_insert(0) += 1;
|
||||
}
|
||||
println!(" - Component types:");
|
||||
for (class_name, count) in type_counts.iter() {
|
||||
println!(" - {}: {}", class_name, count);
|
||||
}
|
||||
}
|
||||
Ok(_) => println!(" ✗ File was not parsed as prefab"),
|
||||
Err(e) => println!(" ✗ Parse error: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vr_horror_project() {
|
||||
test_project(&TestProject::VR_HORROR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Ignore by default, run with --ignored to test
|
||||
fn test_pirate_panic_project() {
|
||||
test_project(&TestProject::PIRATE_PANIC);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vr_horror_detailed() {
|
||||
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to clone project: {}", e);
|
||||
eprintln!("Skipping detailed test (git may not be available)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
test_detailed_parsing(&project_path);
|
||||
}
|
||||
|
||||
/// Benchmark parsing performance
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn benchmark_parsing() {
|
||||
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
eprintln!("Skipping benchmark (git not available)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!("Parsing Performance Benchmark");
|
||||
println!("{}", "=".repeat(60));
|
||||
|
||||
let files = find_unity_files(&project_path);
|
||||
let total_size: u64 = files
|
||||
.iter()
|
||||
.filter_map(|f| std::fs::metadata(f).ok())
|
||||
.map(|m| m.len())
|
||||
.sum();
|
||||
|
||||
println!("Total files: {}", files.len());
|
||||
println!("Total size: {} KB", total_size / 1024);
|
||||
|
||||
let start = Instant::now();
|
||||
let stats = parse_project(&project_path);
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
println!("\nParsing completed in {:?}", elapsed);
|
||||
println!(
|
||||
"Average time per file: {:.2} ms",
|
||||
elapsed.as_millis() as f64 / files.len() as f64
|
||||
);
|
||||
println!(
|
||||
"Throughput: {:.2} files/sec",
|
||||
files.len() as f64 / elapsed.as_secs_f64()
|
||||
);
|
||||
println!(
|
||||
"Throughput: {:.2} KB/sec",
|
||||
(total_size / 1024) as f64 / elapsed.as_secs_f64()
|
||||
);
|
||||
|
||||
stats.print_summary();
|
||||
}
|
||||
|
||||
/// Test GUID resolution for MonoBehaviour scripts
|
||||
#[test]
|
||||
fn test_guid_resolution() {
|
||||
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to clone project: {}", e);
|
||||
eprintln!("Skipping GUID resolution test (git may not be available)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!("Testing GUID Resolution");
|
||||
println!("{}", "=".repeat(60));
|
||||
|
||||
// Build GUID resolver from project
|
||||
println!("\nBuilding GuidResolver from project...");
|
||||
let resolver = GuidResolver::from_project(&project_path)
|
||||
.expect("Should successfully build GuidResolver");
|
||||
|
||||
println!(" ✓ Found {} GUID mappings", resolver.len());
|
||||
assert!(
|
||||
!resolver.is_empty(),
|
||||
"Should find at least some C# scripts with GUIDs"
|
||||
);
|
||||
|
||||
// Test the known PlaySFX GUID from the roadmap
|
||||
let playsfx_guid_str = "091c537484687e9419460cdcd7038234";
|
||||
println!("\nTesting known GUID resolution:");
|
||||
println!(" GUID: {}", playsfx_guid_str);
|
||||
|
||||
// Test resolution by string
|
||||
match resolver.resolve_class_name(playsfx_guid_str) {
|
||||
Some(class_name) => {
|
||||
println!(" ✓ Resolved by string to: {}", class_name);
|
||||
assert_eq!(
|
||||
class_name, "PlaySFX",
|
||||
"PlaySFX GUID should resolve to 'PlaySFX' class name"
|
||||
);
|
||||
}
|
||||
None => {
|
||||
panic!("Failed to resolve PlaySFX GUID. Available GUIDs: {:?}",
|
||||
resolver.guids().take(5).collect::<Vec<_>>());
|
||||
}
|
||||
}
|
||||
|
||||
// Test resolution by Guid type
|
||||
use unity_parser::Guid;
|
||||
let playsfx_guid = Guid::from_hex(playsfx_guid_str).unwrap();
|
||||
match resolver.resolve_class_name(&playsfx_guid) {
|
||||
Some(class_name) => {
|
||||
println!(" ✓ Resolved by Guid type to: {}", class_name);
|
||||
assert_eq!(class_name, "PlaySFX");
|
||||
}
|
||||
None => panic!("Failed to resolve PlaySFX GUID by Guid type"),
|
||||
}
|
||||
|
||||
// Show sample of resolved GUIDs
|
||||
println!("\nSample of resolved GUIDs:");
|
||||
for guid in resolver.guids().take(5) {
|
||||
if let Some(class_name) = resolver.resolve_class_name(&guid) {
|
||||
println!(" {} → {}", guid, class_name);
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrate memory efficiency
|
||||
println!("\nMemory efficiency:");
|
||||
println!(" Each Guid: {} bytes (u128)", std::mem::size_of::<Guid>());
|
||||
println!(" Each String GUID: ~{} bytes (heap allocated)", 32 + std::mem::size_of::<String>());
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
}
|
||||
|
||||
/// Test parsing PlaySFX components from actual scene file
|
||||
#[test]
|
||||
fn test_playsfx_parsing() {
|
||||
use unity_parser::UnityComponent;
|
||||
|
||||
/// PlaySFX component from VR_Horror_YouCantRun
|
||||
#[derive(Debug, Clone, UnityComponent)]
|
||||
#[unity_class("PlaySFX")]
|
||||
pub struct PlaySFX {
|
||||
#[unity_field("volume")]
|
||||
pub volume: f64,
|
||||
|
||||
#[unity_field("startTime")]
|
||||
pub start_time: f64,
|
||||
|
||||
#[unity_field("endTime")]
|
||||
pub end_time: f64,
|
||||
|
||||
#[unity_field("isLoop")]
|
||||
pub is_loop: bool,
|
||||
}
|
||||
|
||||
let project_path = match clone_test_project(&TestProject::VR_HORROR) {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to clone project: {}", e);
|
||||
eprintln!("Skipping PlaySFX parsing test (git may not be available)");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
println!("Testing PlaySFX Component Parsing");
|
||||
println!("{}", "=".repeat(60));
|
||||
|
||||
// Parse the 1F.unity scene that contains PlaySFX components
|
||||
let scene_path = project_path.join("Assets/Scenes/TEST/Final_1F/1F.unity");
|
||||
|
||||
if !scene_path.exists() {
|
||||
eprintln!("Scene file not found: {}", scene_path.display());
|
||||
return;
|
||||
}
|
||||
|
||||
println!("\n Parsing scene: {}", scene_path.display());
|
||||
|
||||
match unity_parser::UnityFile::from_path(&scene_path) {
|
||||
Ok(unity_parser::UnityFile::Scene(scene)) => {
|
||||
println!(" ✓ Scene parsed successfully");
|
||||
println!(" - Total entities: {}", scene.entity_map.len());
|
||||
|
||||
// Try to get PlaySFX components
|
||||
let playsfx_view = scene.world.borrow::<PlaySFX>();
|
||||
let mut found_count = 0;
|
||||
|
||||
for entity in scene.entity_map.values() {
|
||||
if playsfx_view.get(*entity).is_some() {
|
||||
found_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
println!(" ✓ Found {} PlaySFX component(s)", found_count);
|
||||
|
||||
assert!(
|
||||
found_count > 0,
|
||||
"Should find at least one PlaySFX component in 1F.unity"
|
||||
);
|
||||
}
|
||||
Ok(_) => {
|
||||
panic!("File was not parsed as a scene");
|
||||
}
|
||||
Err(e) => {
|
||||
panic!("Failed to parse scene: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n{}", "=".repeat(60));
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
//! Tests for the #[derive(UnityComponent)] macro
|
||||
|
||||
use unity_parser::{ComponentContext, FileID, UnityComponent};
|
||||
|
||||
/// Test component matching the PlaySFX script from VR_Horror_YouCantRun
|
||||
#[derive(Debug, Clone, UnityComponent)]
|
||||
#[unity_class("PlaySFX")]
|
||||
struct PlaySFX {
|
||||
#[unity_field("volume")]
|
||||
volume: f64,
|
||||
|
||||
#[unity_field("startTime")]
|
||||
start_time: f64,
|
||||
|
||||
#[unity_field("endTime")]
|
||||
end_time: f64,
|
||||
|
||||
#[unity_field("isLoop")]
|
||||
is_loop: bool,
|
||||
}
|
||||
|
||||
/// Test component with different field types
|
||||
#[derive(Debug, Clone, UnityComponent)]
|
||||
#[unity_class("TestComponent")]
|
||||
struct TestComponent {
|
||||
#[unity_field("floatValue")]
|
||||
float_value: f32,
|
||||
|
||||
#[unity_field("intValue")]
|
||||
int_value: i32,
|
||||
|
||||
#[unity_field("stringValue")]
|
||||
string_value: String,
|
||||
|
||||
#[unity_field("boolValue")]
|
||||
bool_value: bool,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_play_sfx_parsing() {
|
||||
let yaml_str = r#"
|
||||
volume: 0.75
|
||||
startTime: 1.5
|
||||
endTime: 3.0
|
||||
isLoop: 1
|
||||
"#;
|
||||
|
||||
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
|
||||
let mapping = yaml.as_mapping().unwrap();
|
||||
|
||||
let ctx = ComponentContext {
|
||||
type_id: 114,
|
||||
file_id: FileID::from_i64(12345),
|
||||
class_name: "PlaySFX",
|
||||
entity: None,
|
||||
linking_ctx: None,
|
||||
yaml: mapping,
|
||||
};
|
||||
|
||||
let result = PlaySFX::parse(mapping, &ctx);
|
||||
assert!(result.is_some(), "Failed to parse PlaySFX component");
|
||||
|
||||
let component = result.unwrap();
|
||||
assert_eq!(component.volume, 0.75);
|
||||
assert_eq!(component.start_time, 1.5);
|
||||
assert_eq!(component.end_time, 3.0);
|
||||
assert_eq!(component.is_loop, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_play_sfx_default_values() {
|
||||
// Test with missing fields (should use Default::default())
|
||||
let yaml_str = r#"
|
||||
volume: 0.5
|
||||
"#;
|
||||
|
||||
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
|
||||
let mapping = yaml.as_mapping().unwrap();
|
||||
|
||||
let ctx = ComponentContext {
|
||||
type_id: 114,
|
||||
file_id: FileID::from_i64(12345),
|
||||
class_name: "PlaySFX",
|
||||
entity: None,
|
||||
linking_ctx: None,
|
||||
yaml: mapping,
|
||||
};
|
||||
|
||||
let result = PlaySFX::parse(mapping, &ctx);
|
||||
assert!(result.is_some(), "Failed to parse PlaySFX component with defaults");
|
||||
|
||||
let component = result.unwrap();
|
||||
assert_eq!(component.volume, 0.5);
|
||||
assert_eq!(component.start_time, 0.0); // Default for f64
|
||||
assert_eq!(component.end_time, 0.0); // Default for f64
|
||||
assert_eq!(component.is_loop, false); // Default for bool
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_test_component_parsing() {
|
||||
let yaml_str = r#"
|
||||
floatValue: 3.14
|
||||
intValue: 42
|
||||
stringValue: "Hello, Unity!"
|
||||
boolValue: 1
|
||||
"#;
|
||||
|
||||
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
|
||||
let mapping = yaml.as_mapping().unwrap();
|
||||
|
||||
let ctx = ComponentContext {
|
||||
type_id: 114,
|
||||
file_id: FileID::from_i64(67890),
|
||||
class_name: "TestComponent",
|
||||
entity: None,
|
||||
linking_ctx: None,
|
||||
yaml: mapping,
|
||||
};
|
||||
|
||||
let result = TestComponent::parse(mapping, &ctx);
|
||||
assert!(result.is_some(), "Failed to parse TestComponent");
|
||||
|
||||
let component = result.unwrap();
|
||||
assert!((component.float_value - 3.14_f32).abs() < 0.001);
|
||||
assert_eq!(component.int_value, 42);
|
||||
assert_eq!(component.string_value, "Hello, Unity!");
|
||||
assert_eq!(component.bool_value, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_registration() {
|
||||
// Verify that components are registered in the inventory
|
||||
let mut found_play_sfx = false;
|
||||
let mut found_test_component = false;
|
||||
|
||||
for reg in inventory::iter::<unity_parser::ComponentRegistration> {
|
||||
if reg.class_name == "PlaySFX" {
|
||||
found_play_sfx = true;
|
||||
assert_eq!(reg.type_id, 114);
|
||||
}
|
||||
if reg.class_name == "TestComponent" {
|
||||
found_test_component = true;
|
||||
assert_eq!(reg.type_id, 114);
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
found_play_sfx,
|
||||
"PlaySFX component was not registered in inventory"
|
||||
);
|
||||
assert!(
|
||||
found_test_component,
|
||||
"TestComponent was not registered in inventory"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_component_registration_parser() {
|
||||
// Test that the registered parser function works
|
||||
let yaml_str = r#"
|
||||
volume: 0.8
|
||||
startTime: 2.0
|
||||
endTime: 4.0
|
||||
isLoop: 0
|
||||
"#;
|
||||
|
||||
let yaml: serde_yaml::Value = serde_yaml::from_str(yaml_str).unwrap();
|
||||
let mapping = yaml.as_mapping().unwrap();
|
||||
|
||||
let ctx = ComponentContext {
|
||||
type_id: 114,
|
||||
file_id: FileID::from_i64(11111),
|
||||
class_name: "PlaySFX",
|
||||
entity: None,
|
||||
linking_ctx: None,
|
||||
yaml: mapping,
|
||||
};
|
||||
|
||||
// Find the PlaySFX registration and call its parser
|
||||
for reg in inventory::iter::<unity_parser::ComponentRegistration> {
|
||||
if reg.class_name == "PlaySFX" {
|
||||
let result = (reg.parser)(mapping, &ctx);
|
||||
assert!(result.is_some(), "Registered parser failed to parse");
|
||||
|
||||
// Downcast to verify it's the right type
|
||||
let boxed = result.unwrap();
|
||||
assert!(
|
||||
boxed.downcast_ref::<PlaySFX>().is_some(),
|
||||
"Parsed component is not PlaySFX type"
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
panic!("PlaySFX registration not found");
|
||||
}
|
||||
Reference in New Issue
Block a user