From 96b1e172eecd9cede0053a1a4d3c94ca4c5d8e2f Mon Sep 17 00:00:00 2001 From: Connor Date: Fri, 2 Jan 2026 14:00:42 +0000 Subject: [PATCH] custom component parsing --- .claude/settings.local.json | 3 +- Cargo.lock | 26 + Cargo.toml | 54 +-- SUMMARY.md | 444 ------------------ TESTING.md | 245 ---------- cursebreaker-parser-macros/Cargo.toml | 16 + cursebreaker-parser-macros/src/lib.rs | 201 ++++++++ cursebreaker-parser/Cargo.toml | 60 +++ .../examples}/basic_parsing.rs | 0 .../examples/custom_component.rs | 101 ++++ .../examples}/guid_resolution.rs.disabled | 0 .../src}/ecs/builder.rs | 42 +- {src => cursebreaker-parser/src}/ecs/mod.rs | 0 {src => cursebreaker-parser/src}/error.rs | 0 {src => cursebreaker-parser/src}/lib.rs | 11 +- {src => cursebreaker-parser/src}/model/mod.rs | 0 .../src}/parser/meta.rs | 0 .../src}/parser/mod.rs | 0 .../src}/parser/unity_tag.rs | 0 .../src}/parser/yaml.rs | 0 .../src}/project/mod.rs | 0 .../src}/project/query.rs | 0 .../src}/property/mod.rs | 0 .../src}/types/component.rs | 16 + .../src}/types/game_object.rs | 0 {src => cursebreaker-parser/src}/types/ids.rs | 0 {src => cursebreaker-parser/src}/types/mod.rs | 5 +- .../src}/types/prefab_instance.rs | 0 .../src}/types/reference.rs | 0 .../src}/types/transform.rs | 0 .../src}/types/type_registry.rs | 0 .../src}/types/values.rs | 0 .../tests}/integration_tests.rs | 0 cursebreaker-parser/tests/macro_tests.rs | 197 ++++++++ .../tests}/test_guid_resolution.rs.disabled | 0 35 files changed, 672 insertions(+), 749 deletions(-) delete mode 100644 SUMMARY.md delete mode 100644 TESTING.md create mode 100644 cursebreaker-parser-macros/Cargo.toml create mode 100644 cursebreaker-parser-macros/src/lib.rs create mode 100644 cursebreaker-parser/Cargo.toml rename {examples => cursebreaker-parser/examples}/basic_parsing.rs (100%) create mode 100644 cursebreaker-parser/examples/custom_component.rs rename {examples => cursebreaker-parser/examples}/guid_resolution.rs.disabled (100%) rename {src => cursebreaker-parser/src}/ecs/builder.rs (78%) rename {src => cursebreaker-parser/src}/ecs/mod.rs (100%) rename {src => cursebreaker-parser/src}/error.rs (100%) rename {src => cursebreaker-parser/src}/lib.rs (75%) rename {src => cursebreaker-parser/src}/model/mod.rs (100%) rename {src => cursebreaker-parser/src}/parser/meta.rs (100%) rename {src => cursebreaker-parser/src}/parser/mod.rs (100%) rename {src => cursebreaker-parser/src}/parser/unity_tag.rs (100%) rename {src => cursebreaker-parser/src}/parser/yaml.rs (100%) rename {src => cursebreaker-parser/src}/project/mod.rs (100%) rename {src => cursebreaker-parser/src}/project/query.rs (100%) rename {src => cursebreaker-parser/src}/property/mod.rs (100%) rename {src => cursebreaker-parser/src}/types/component.rs (92%) rename {src => cursebreaker-parser/src}/types/game_object.rs (100%) rename {src => cursebreaker-parser/src}/types/ids.rs (100%) rename {src => cursebreaker-parser/src}/types/mod.rs (84%) rename {src => cursebreaker-parser/src}/types/prefab_instance.rs (100%) rename {src => cursebreaker-parser/src}/types/reference.rs (100%) rename {src => cursebreaker-parser/src}/types/transform.rs (100%) rename {src => cursebreaker-parser/src}/types/type_registry.rs (100%) rename {src => cursebreaker-parser/src}/types/values.rs (100%) rename {tests => cursebreaker-parser/tests}/integration_tests.rs (100%) create mode 100644 cursebreaker-parser/tests/macro_tests.rs rename {tests => cursebreaker-parser/tests}/test_guid_resolution.rs.disabled (100%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index af509e5..924182e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -9,7 +9,8 @@ "WebFetch(domain:docs.rs)", "Bash(findstr:*)", "Bash(cargo check:*)", - "Bash(ls:*)" + "Bash(ls:*)", + "Bash(find:*)" ] } } diff --git a/Cargo.lock b/Cargo.lock index 81d52fd..26d0a3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,8 +27,10 @@ checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" name = "cursebreaker-parser" version = "0.1.0" dependencies = [ + "cursebreaker-parser-macros", "glam", "indexmap", + "inventory", "lru", "once_cell", "pretty_assertions", @@ -40,6 +42,15 @@ dependencies = [ "walkdir", ] +[[package]] +name = "cursebreaker-parser-macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "diff" version = "0.1.13" @@ -96,6 +107,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inventory" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e" +dependencies = [ + "rustversion", +] + [[package]] name = "itoa" version = "1.0.17" @@ -186,6 +206,12 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.22" diff --git a/Cargo.toml b/Cargo.toml index 2c3b629..14ba694 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,54 +1,10 @@ -[package] -name = "cursebreaker-parser" +[workspace] +members = ["cursebreaker-parser", "cursebreaker-parser-macros"] +resolver = "2" + +[workspace.package] version = "0.1.0" edition = "2021" authors = ["Your Name "] license = "MIT OR Apache-2.0" -description = "A high-performance Rust library for parsing Unity project files (.unity, .prefab, .asset)" repository = "https://github.com/yourusername/cursebreaker-parser-rust" -keywords = ["unity", "parser", "yaml", "gamedev"] -categories = ["parser-implementations", "game-development"] -rust-version = "1.70" - -[lib] -name = "cursebreaker_parser" -path = "src/lib.rs" - -[dependencies] -# YAML parsing -serde_yaml = "0.9" -serde = { version = "1.0", features = ["derive"] } - -# Error handling -thiserror = "1.0" - -# Ordered maps for properties -indexmap = { version = "2.1", features = ["serde"] } - -# Regex for parsing -regex = "1.10" - -# Math types (Vector2, Vector3, Quaternion, etc.) -glam = { version = "0.29", features = ["serde"] } - -# ECS (Entity Component System) -sparsey = "0.13" - -# LRU cache for reference resolution -lru = "0.12" - -# Directory traversal for loading projects -walkdir = "2.4" - -# Lazy static initialization for type registry -once_cell = "1.19" - -[dev-dependencies] -# Testing utilities -pretty_assertions = "1.4" - -[features] -default = [] - -# Future: parallel processing support -parallel = [] diff --git a/SUMMARY.md b/SUMMARY.md deleted file mode 100644 index 7a71b1c..0000000 --- a/SUMMARY.md +++ /dev/null @@ -1,444 +0,0 @@ -# Cursebreaker Parser - Current State Summary - -**Last Updated:** 2026-01-01 -**Version:** 0.1.0 (Major refactoring in progress) - -## Overview - -This codebase is a Unity file parser that converts Unity YAML files (.unity, .prefab, .asset) into Rust data structures. A major architectural refactoring has been completed to: -1. Parse YAML directly into component types (bypassing intermediate `UnityDocument`) -2. Automatically build Sparsey ECS Worlds for scene files -3. Keep prefabs as raw YAML for efficient cloning and instantiation - -## Current Architecture - -### Data Flow - -``` -Unity File (.unity/.prefab/.asset) - ↓ -Parser detects file type by extension - ↓ -┌─────────────┬──────────────┬──────────────┐ -│ .unity │ .prefab │ .asset │ -│ (Scene) │ (Prefab) │ (Asset) │ -└─────────────┴──────────────┴──────────────┘ - ↓ ↓ ↓ - Parse YAML Parse YAML Parse YAML - ↓ ↓ ↓ - RawDocument RawDocument RawDocument - ↓ ↓ ↓ - Build World Store YAML Store YAML - ↓ ↓ ↓ - UnityScene UnityPrefab UnityAsset - ↓ - Entity + Components -``` - -### Core Types - -#### `UnityFile` (src/model/mod.rs:14-53) -```rust -pub enum UnityFile { - Scene(UnityScene), // .unity files → ECS World - Prefab(UnityPrefab), // .prefab files → Raw YAML - Asset(UnityAsset), // .asset files → Raw YAML -} -``` - -#### `UnityScene` (src/model/mod.rs:60-85) -Contains a fully-parsed Sparsey ECS World: -```rust -pub struct UnityScene { - pub path: PathBuf, - pub world: World, // Sparsey ECS World - pub entity_map: HashMap, // Unity FileID → Entity mapping -} -``` - -#### `UnityPrefab` / `UnityAsset` (src/model/mod.rs:92-150) -Contains raw YAML documents for cloning: -```rust -pub struct UnityPrefab { - pub path: PathBuf, - pub documents: Vec, // Raw YAML + metadata -} -``` - -#### `RawDocument` (src/model/mod.rs:160-194) -Lightweight storage of Unity object metadata + YAML: -```rust -pub struct RawDocument { - pub type_id: u32, // Unity type ID - pub file_id: FileID, // Unity file ID - pub class_name: String, // "GameObject", "Transform", etc. - pub yaml: serde_yaml::Value, // Inner YAML (after "GameObject: {...}" wrapper) -} -``` - -### Component System - -#### `UnityComponent` Trait (src/types/component.rs:18-28) -Components parse directly from YAML: -```rust -pub trait UnityComponent: Sized { - fn parse(yaml: &serde_yaml::Mapping, ctx: &ComponentContext) -> Option; -} -``` - -**Key Change:** Previously used `UnityDocument`, now uses raw `serde_yaml::Mapping` for zero-copy parsing. - -#### `ComponentContext` (src/types/component.rs:8-15) -Provides metadata during parsing: -```rust -pub struct ComponentContext<'a> { - pub type_id: u32, - pub file_id: FileID, - pub class_name: &'a str, -} -``` - -#### YAML Helpers (src/types/component.rs:31-167) -Typed accessors for Unity YAML patterns: -- `get_vector3()` - Parses `{x, y, z}` into `glam::Vec3` -- `get_quaternion()` - Parses `{x, y, z, w}` into `glam::Quat` -- `get_file_ref()` - Parses `{fileID: N}` into `FileRef` -- etc. - -#### Implemented Components -1. **GameObject** (src/types/game_object.rs) - Basic entity data (name, active, layer) -2. **Transform** (src/types/transform.rs) - Position, rotation, scale + hierarchy -3. **RectTransform** (src/types/transform.rs) - UI transform with anchors - -### ECS World Building (src/ecs/builder.rs) - -**3-Pass Approach:** - -**Pass 1: Spawn GameObjects** (lines 32-36) -- Creates entities for all GameObjects -- Maps `FileID → Entity` - -**Pass 2: Attach Components** (lines 38-42) -- Parses components from YAML -- Dispatches to correct parser based on `class_name` -- Attaches to GameObject entities - -**Pass 3: Resolve Hierarchy** (lines 44-46) -- Converts Transform parent/children FileRefs to Entity references - -### Parser Pipeline (src/parser/mod.rs) - -**File Type Detection** (lines 69-76) -```rust -.unity → FileType::Scene → Build ECS World -.prefab → FileType::Prefab → Store Raw YAML -.asset → FileType::Asset → Store Raw YAML -``` - -**YAML Document Parsing** (lines 125-167) -1. Parse Unity tag: `--- !u!1 &12345` -2. Extract YAML after tag line -3. Unwrap class name wrapper: `GameObject: {...}` → `{...}` -4. Store as `RawDocument` - -## ✅ What's Implemented - -### Fully Working -- ✅ File type detection by extension -- ✅ YAML parsing with Unity header validation -- ✅ Direct YAML-to-component parsing (bypasses UnityDocument) -- ✅ Component trait with typed YAML helpers -- ✅ GameObject, Transform, RectTransform parsing -- ✅ Separate code paths for scenes vs prefabs -- ✅ Sparsey World creation with component registration -- ✅ Entity spawning for GameObjects -- ✅ Component Linking (Transform parent and children) with callbacks in case the component hasn't been initialized yet. - -## ❌ What's Not Implemented - -### Critical Missing Features - -#### 1. Prefab Instancing System (MEDIUM PRIORITY) -**Status:** Not started - -**What's Needed:** -Create `src/prefab/mod.rs` with: - -```rust -pub struct PrefabInstance { - documents: Vec, // Cloned YAML -} - -impl UnityPrefab { - /// Clone prefab for instancing - pub fn instantiate(&self) -> PrefabInstance; -} - -impl PrefabInstance { - /// Override YAML values before spawning - pub fn override_value(&mut self, file_id: FileID, path: &str, value: serde_yaml::Value); - - /// Spawn into existing scene world - pub fn spawn_into(self, world: &mut World) -> Result>; -} -``` - -**Usage Example:** -```rust -let prefab = match unity_file { - UnityFile::Prefab(p) => p, - _ => panic!("Not a prefab"), -}; - -let mut instance = prefab.instantiate(); -instance.override_value(file_id, "m_Name", "CustomName".into())?; -instance.override_value(file_id, "m_LocalPosition.x", 100.0.into())?; -let entities = instance.spawn_into(&mut scene.world)?; -``` - -**Implementation Steps:** -1. Create src/prefab/mod.rs -2. Implement YAML cloning (serde_yaml::Value::clone()) -3. Implement YAML path navigation for overrides (e.g., "m_LocalPosition.x") -4. Reuse `build_world_from_documents()` for spawning -5. Add tests with real prefab files - -**Files to Create:** -- src/prefab/mod.rs - -**Files to Modify:** -- src/lib.rs (add `pub mod prefab`) - -#### 4. UnityProject Module Update (MEDIUM PRIORITY) -**Status:** Currently disabled to allow compilation - -**Location:** src/project/mod.rs, src/project/query.rs - -**Problem:** References old `UnityDocument` type that no longer exists. - -**What's Needed:** -- Update `UnityProject` to store `HashMap` instead of files with documents -- Implement queries that work across scenes/prefabs: - - `get_all_scenes() -> Vec<&UnityScene>` - - `get_all_prefabs() -> Vec<&UnityPrefab>` - - `find_by_name()` - search across RawDocuments in prefabs -- Update reference resolution for cross-file references -- GUID → Entity resolution for scene references to prefabs - -**Files to Modify:** -- src/project/mod.rs (lines 9, 36-50) -- src/project/query.rs (entire file) -- src/lib.rs (re-enable module exports) - -**Example Updated API:** -```rust -impl UnityProject { - pub fn load_file(&mut self, path: impl AsRef) -> Result<&UnityFile>; - - pub fn get_scenes(&self) -> Vec<&UnityScene>; - pub fn get_prefabs(&self) -> Vec<&UnityPrefab>; - - pub fn find_prefab_by_name(&self, name: &str) -> Option<&UnityPrefab>; -} -``` - -#### 5. Additional Unity Components (LOW PRIORITY) -**Status:** Only 3 components implemented - -**Currently Missing:** -- Camera -- Light -- MeshRenderer / MeshFilter -- Collider variants (BoxCollider, SphereCollider, etc.) -- Rigidbody -- MonoBehaviour (custom scripts) -- UI components (Image, Text, Button, etc.) - -**Implementation Pattern:** -```rust -// src/types/camera.rs -#[derive(Debug, Clone)] -pub struct Camera { - pub field_of_view: f32, - pub near_clip_plane: f32, - pub far_clip_plane: f32, - // ... other fields -} - -impl UnityComponent for Camera { - fn parse(yaml: &serde_yaml::Mapping, _ctx: &ComponentContext) -> Option { - Some(Self { - field_of_view: yaml_helpers::get_f64(yaml, "m_FieldOfView")? as f32, - near_clip_plane: yaml_helpers::get_f64(yaml, "near clip plane")? as f32, - far_clip_plane: yaml_helpers::get_f64(yaml, "far clip plane")? as f32, - }) - } -} -``` - -**Files to Create:** -- src/types/camera.rs -- src/types/light.rs -- src/types/renderer.rs -- etc. - -**Files to Modify:** -- src/types/mod.rs (add module declarations) -- src/ecs/builder.rs:96-122 (add component dispatch cases) -- Register components in Sparsey World builder (src/ecs/builder.rs:24-28) - -## 🔧 Known Issues - -### 1. Compilation Warnings -None currently! Code compiles cleanly in release mode. - -### 2. Disabled Modules -- `src/project/` - Commented out in src/lib.rs:33 due to UnityDocument references - -### 3. Stubbed Functionality -- Component insertion (src/ecs/builder.rs:141-151) -- Transform hierarchy resolution (src/ecs/builder.rs:155-176) - -## 📋 Recommended Next Steps - -### Phase 1: Complete Sparsey Integration (CRITICAL) -**Time Estimate:** 1-2 hours of research + 2-3 hours implementation - -**Success Criteria:** -- Parse a .unity scene with nested GameObjects -- Verify Transform hierarchy is correctly resolved -- Query entities and access components from World - -### Phase 2: Implement Prefab Instancing (HIGH VALUE) -**Time Estimate:** 3-4 hours - -1. Create `src/prefab/mod.rs` with PrefabInstance API -2. Implement YAML cloning and override logic -3. Implement `spawn_into()` using existing world builder -4. Add tests with real prefab files - -**Success Criteria:** -- Load a prefab -- Override values (name, position, etc.) -- Instantiate into scene multiple times -- Verify entities created correctly - -### Phase 3: Update UnityProject Module (MEDIUM PRIORITY) -**Time Estimate:** 2-3 hours - -1. Update HashMap to store UnityFile enum -2. Implement scene/prefab accessors -3. Update query functions for RawDocument -4. Re-enable module exports - -**Success Criteria:** -- Load multiple scenes and prefabs -- Query across files -- Find prefabs by name - -### Phase 4: Add More Components (ONGOING) -**Time Estimate:** 1-2 hours per component - -Start with most common components: -1. Camera (critical for scene rendering) -2. Light (critical for scene rendering) -3. MeshRenderer + MeshFilter (for 3D objects) - -## 🎯 Performance Characteristics - -### Memory Improvements -- **Before:** YAML → PropertyValue tree → Component (2x allocations) -- **After (Scenes):** YAML → Component (1x allocation, ~40% reduction) -- **After (Prefabs):** YAML → serde_yaml::Value (shared references, minimal overhead) - -### Parsing Speed -- Direct YAML access eliminates PropertyValue conversion -- Prefabs use cheap cloning (Arc-based in serde_yaml) - -## 🧪 Testing Status - -### Unit Tests -- ✅ Parser header validation (src/parser/mod.rs:196-201) -- ✅ YAML content extraction (src/parser/mod.rs:204-209) -- ✅ File type detection (src/parser/mod.rs:212-229) - -### Integration Tests -- ❌ Scene parsing end-to-end -- ❌ Prefab parsing end-to-end -- ❌ Component attachment -- ❌ Transform hierarchy resolution -- ❌ Prefab instantiation - -**Recommendation:** Add integration tests once Sparsey integration is complete. - -## 📝 Code Organization - -``` -src/ -├── lib.rs # Public API + exports -├── error.rs # Error types -├── model/ -│ └── mod.rs # ✅ UnityFile, UnityScene, UnityPrefab, RawDocument -├── parser/ -│ ├── mod.rs # ✅ File type detection + parsing pipeline -│ ├── unity_tag.rs # ✅ Unity tag parsing (!u!N &ID) -│ ├── yaml.rs # ✅ YAML document splitting -│ └── meta.rs # ✅ .meta file parsing -├── types/ -│ ├── mod.rs # ✅ Type exports -│ ├── component.rs # ✅ UnityComponent trait + yaml_helpers -│ ├── game_object.rs # ✅ GameObject component -│ ├── transform.rs # ✅ Transform + RectTransform -│ ├── ids.rs # ✅ FileID, LocalID -│ ├── values.rs # ✅ Vector2/3, Quaternion, Color, etc. -│ ├── reference.rs # ✅ UnityReference enum -│ └── type_registry.rs # ✅ Type ID ↔ Class name mapping -├── ecs/ -│ ├── mod.rs # ✅ Module exports -│ └── builder.rs # ⚠️ 3-pass world building (incomplete) -├── prefab/ # ❌ NOT CREATED YET -│ └── mod.rs # TODO: Prefab instancing -├── project/ # ❌ DISABLED (needs refactoring) -│ ├── mod.rs # ❌ References old UnityDocument -│ └── query.rs # ❌ References old UnityDocument -└── property/ - └── mod.rs # ✅ PropertyValue (kept for helpers) -``` - -## 🔗 External Dependencies - -- **serde_yaml 0.9** - YAML parsing -- **sparsey 0.13** - ECS framework -- **glam 0.29** - Math types (Vec2/3, Quat) -- **indexmap 2.1** - Ordered maps -- **lru 0.12** - LRU cache for references - -## 📚 Useful Documentation - -- **Sparsey Docs:** https://docs.rs/sparsey/0.13.3/ -- **Sparsey GitHub:** https://github.com/LechintanTudor/sparsey -- **Unity YAML Format:** GameObjects use `--- !u!1 &fileID` tags with nested YAML - -## 🤝 Contributing / Next Agent Instructions - -**If you're the next AI agent working on this:** - -1. **Start here:** Read this summary completely -2. **Quick test:** Try `cargo build --release` - should compile cleanly -3. **Focus on:** Sparsey integration (Phase 1) - highest priority -4. **Key files:** - - src/ecs/builder.rs (needs Sparsey API research) - - src/prefab/mod.rs (doesn't exist yet) - - src/project/mod.rs (needs refactoring) - -**Before making changes:** -- Understand the 3-pass world building approach -- Know that dispatcher routes to parsers (no redundant type checks in parsers) -- RawDocument.yaml contains INNER yaml (after class name wrapper is removed) - -**Testing approach:** -- Use files in `data/` directory for real Unity files -- Focus on .unity scenes first, then .prefab files -- Verify entity creation and component attachment - -Good luck! 🚀 diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index fbbb331..0000000 --- a/TESTING.md +++ /dev/null @@ -1,245 +0,0 @@ -# Testing Guide - -This document describes how to test the Cursebreaker Unity Parser against real Unity projects. - -## Integration Tests - -The integration test suite can automatically clone Unity projects from GitHub and parse all their files, providing detailed statistics and error reporting. - -### Requirements - -- **Git**: Required for cloning test projects -- **Internet connection**: For cloning repositories (only needed on first run) -- **Disk space**: ~100-500 MB per project - -### Running Tests - -#### Basic Test (VR Horror Project) - -This test clones and parses the VR Horror Unity project: - -```bash -cargo test test_vr_horror_project -- --nocapture -``` - -Expected output: -``` -============================================================ -Testing: VR_Horror_YouCantRun -============================================================ -Cloning VR_Horror_YouCantRun from https://github.com/Unity3D-Projects/VR_Horror_YouCantRun.git... -Finding Unity files in test_data/VR_Horror_YouCantRun... -Found 150 Unity files -Parsing files... - [1/150] Parsing: SampleScene.unity - [10/150] Parsing: Player.prefab - ... - -============================================================ -Parsing Statistics -============================================================ - Total files found: 150 - Scenes parsed: 15 - Prefabs parsed: 120 - Assets parsed: 15 - Total entities: 450 - Total documents: 1200 - Parse time: 250 ms - - Success rate: 95.00% -============================================================ -``` - -#### Detailed Parsing Test - -This test shows detailed information about parsed files: - -```bash -cargo test test_vr_horror_detailed -- --nocapture -``` - -This will: -- Parse a sample scene file and show entity information -- Parse a sample prefab file and test the instantiation system -- Test the override system -- Display component type distributions - -#### All Projects (Including Ignored Tests) - -```bash -cargo test --test integration_tests -- --nocapture --ignored -``` - -This runs tests for additional projects like PiratePanic (ignored by default because they're large). - -#### Performance Benchmark - -```bash -cargo test benchmark_parsing -- --nocapture --ignored -``` - -This measures parsing performance and provides metrics like: -- Files per second -- KB per second -- Average time per file - -### Available Test Projects - -| Project | Description | Files | Size | -|---------|-------------|-------|------| -| **VR_Horror_YouCantRun** | VR horror game with complex scenes | ~150 | ~50MB | -| **PiratePanic** | Unity Technologies sample project | ~300 | ~200MB | - -### Test Data Location - -Cloned projects are stored in `test_data/` (gitignored): -``` -test_data/ -├── VR_Horror_YouCantRun/ -│ └── Assets/ -│ ├── Scenes/ -│ ├── Prefabs/ -│ └── ... -└── PiratePanic/ - └── Assets/ - └── ... -``` - -Projects are cloned only once and reused for subsequent test runs. Delete `test_data/` to force a fresh clone. - -### Understanding Test Output - -#### Success Rate -- **>95%**: Excellent - parser handles almost all files -- **80-95%**: Good - some edge cases not handled -- **<80%**: Needs investigation - may indicate parser issues - -#### Common Error Types -- **Missing Header**: File doesn't have Unity YAML header -- **Invalid Type Tag**: Unknown Unity type ID -- **YAML Parsing Error**: Malformed YAML structure - -#### Statistics -- **Total entities**: Number of GameObjects in scenes -- **Total documents**: Number of YAML documents in prefabs/assets -- **Parse time**: Total time to parse all files (lower is better) - -### Adding New Test Projects - -To add a new Unity project to test: - -1. Edit `tests/integration_tests.rs` -2. Add a new project configuration: -```rust -const MY_PROJECT: TestProject = TestProject { - name: "MyProject", - repo_url: "https://github.com/user/MyProject.git", - branch: None, // or Some("main") -}; -``` - -3. Add a test function: -```rust -#[test] -#[ignore] // Optional: ignore by default for large projects -fn test_my_project() { - test_project(&TestProject::MY_PROJECT); -} -``` - -4. Run the test: -```bash -cargo test test_my_project -- --nocapture --ignored -``` - -### Continuous Integration - -For CI/CD pipelines: - -```bash -# Quick smoke test (doesn't require git) -cargo test --lib - -# Full integration tests (requires git) -cargo test --test integration_tests -- --nocapture -``` - -To skip integration tests in CI environments without git: -```bash -cargo test --lib --bins -``` - -### Troubleshooting - -#### "Git clone failed" -- Ensure git is installed: `git --version` -- Check internet connection -- Verify repository URL is accessible - -#### "Skipping test: file not found" -- The test project hasn't been cloned yet -- Run the test again with `--nocapture` to see clone progress -- Check `test_data/` directory was created - -#### High error rate -- Check error details in test output -- Some Unity files may use unsupported features -- Error rate <20% is generally acceptable for parsing stress tests - -#### Out of disk space -- Delete `test_data/` to free up space -- Run tests for individual projects instead of all at once - -### Development Workflow - -When adding new parser features: - -1. Run integration tests to establish baseline: -```bash -cargo test test_vr_horror_project -- --nocapture > baseline.txt -``` - -2. Make your changes - -3. Re-run tests and compare: -```bash -cargo test test_vr_horror_project -- --nocapture > after_changes.txt -diff baseline.txt after_changes.txt -``` - -4. Verify: - - Success rate didn't decrease - - No new error types introduced - - Parse time didn't significantly increase - -### Performance Targets - -- **Parse time**: <2ms per file average -- **Memory usage**: <100MB for 1000 files -- **Success rate**: >90% for well-formed Unity projects - -### Example: Testing Prefab Instancing - -The detailed test demonstrates the prefab instancing system: - -```bash -cargo test test_vr_horror_detailed -- --nocapture -``` - -Look for output like: -``` -Testing prefab instantiation: - ✓ Created instance with 45 remapped FileIDs - ✓ Override system working - - Component types: - - GameObject: 1 - - Transform: 1 - - RectTransform: 3 - - Canvas: 1 -``` - -This confirms that: -1. Prefabs can be instantiated -2. FileIDs are properly remapped -3. The override system works -4. All component types are recognized diff --git a/cursebreaker-parser-macros/Cargo.toml b/cursebreaker-parser-macros/Cargo.toml new file mode 100644 index 0000000..d602c0f --- /dev/null +++ b/cursebreaker-parser-macros/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cursebreaker-parser-macros" +version = "0.1.0" +edition = "2021" +authors = ["Your Name "] +license = "MIT OR Apache-2.0" +description = "Procedural macros for cursebreaker-parser" +repository = "https://github.com/yourusername/cursebreaker-parser-rust" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0", features = ["full", "extra-traits"] } +quote = "1.0" +proc-macro2 = "1.0" diff --git a/cursebreaker-parser-macros/src/lib.rs b/cursebreaker-parser-macros/src/lib.rs new file mode 100644 index 0000000..1a7d097 --- /dev/null +++ b/cursebreaker-parser-macros/src/lib.rs @@ -0,0 +1,201 @@ +//! Procedural macros for cursebreaker-parser +//! +//! This crate provides the `#[derive(UnityComponent)]` macro for automatically +//! generating Unity component parsing code. + +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Type}; + +/// Derive macro for automatically implementing UnityComponent trait +/// +/// # Example +/// ```ignore +/// #[derive(UnityComponent)] +/// #[unity_class("PlaySFX")] // Optional, defaults to struct name +/// pub struct PlaySFX { +/// #[unity_field("volume")] +/// volume: f64, +/// +/// #[unity_field("startTime")] +/// start_time: f64, +/// } +/// ``` +#[proc_macro_derive(UnityComponent, attributes(unity_field, unity_class, unity_type))] +pub fn derive_unity_component(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + // Extract struct name + let struct_name = &input.ident; + + // Extract class name (defaults to struct name) + let class_name = extract_class_name(&input.attrs, struct_name); + + // Extract type ID (defaults to 114 for MonoBehaviour) + let type_id = extract_type_id(&input.attrs); + + // Only process structs + let fields = match &input.data { + Data::Struct(data) => match &data.fields { + Fields::Named(fields) => &fields.named, + _ => panic!("UnityComponent can only be derived for structs with named fields"), + }, + _ => panic!("UnityComponent can only be derived for structs"), + }; + + // Extract field mappings + let field_parsers: Vec<_> = fields + .iter() + .map(|field| { + let field_name = field.ident.as_ref().unwrap(); + let field_type = &field.ty; + let unity_field_name = extract_unity_field_name(&field.attrs, field_name); + + // Generate the parsing code for this field + let parser_call = generate_parser_call(field_type, &unity_field_name); + + quote! { + let #field_name = #parser_call; + } + }) + .collect(); + + // Generate field initialization + let field_names: Vec<_> = fields + .iter() + .map(|field| field.ident.as_ref().unwrap()) + .collect(); + + // Generate UnityComponent implementation + let parse_impl = quote! { + impl cursebreaker_parser::UnityComponent for #struct_name { + fn parse( + yaml: &serde_yaml::Mapping, + ctx: &cursebreaker_parser::ComponentContext + ) -> Option { + #(#field_parsers)* + + Some(Self { + #(#field_names,)* + }) + } + } + }; + + // Generate inventory registration + let registration = quote! { + inventory::submit! { + cursebreaker_parser::ComponentRegistration { + type_id: #type_id, + class_name: #class_name, + parser: |yaml, ctx| { + #struct_name::parse(yaml, ctx) + .map(|c| Box::new(c) as Box) + } + } + } + }; + + // Combine everything + let expanded = quote! { + #parse_impl + #registration + }; + + TokenStream::from(expanded) +} + +/// Extract the Unity class name from attributes, or default to struct name +fn extract_class_name(attrs: &[syn::Attribute], struct_name: &syn::Ident) -> String { + for attr in attrs { + if attr.path().is_ident("unity_class") { + if let Ok(Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + })) = attr.parse_args() + { + return lit.value(); + } + } + } + struct_name.to_string() +} + +/// Extract the Unity type ID from attributes, or default to 114 (MonoBehaviour) +fn extract_type_id(attrs: &[syn::Attribute]) -> u32 { + for attr in attrs { + if attr.path().is_ident("unity_type") { + if let Ok(Expr::Lit(ExprLit { + lit: Lit::Int(lit), .. + })) = attr.parse_args() + { + if let Ok(value) = lit.base10_parse::() { + return value; + } + } + } + } + 114 // Default to MonoBehaviour +} + +/// Extract the Unity field name from field attributes +fn extract_unity_field_name(attrs: &[syn::Attribute], field_name: &syn::Ident) -> String { + for attr in attrs { + if attr.path().is_ident("unity_field") { + if let Ok(Expr::Lit(ExprLit { + lit: Lit::Str(lit), .. + })) = attr.parse_args() + { + return lit.value(); + } + } + } + panic!( + "Field '{}' is missing #[unity_field(\"name\")] attribute", + field_name + ); +} + +/// Generate the appropriate yaml_helpers call based on field type +fn generate_parser_call(field_type: &Type, unity_field_name: &str) -> proc_macro2::TokenStream { + let type_str = quote! { #field_type }.to_string(); + let type_str = type_str.replace(" ", ""); // Remove whitespace + + // Determine the appropriate yaml_helpers function + let helper_call = if type_str == "f64" { + quote! { cursebreaker_parser::yaml_helpers::get_f64(yaml, #unity_field_name) } + } else if type_str == "f32" { + quote! { cursebreaker_parser::yaml_helpers::get_f64(yaml, #unity_field_name).map(|v| v as f32) } + } else if type_str == "i64" { + quote! { cursebreaker_parser::yaml_helpers::get_i64(yaml, #unity_field_name) } + } else if type_str == "i32" { + quote! { cursebreaker_parser::yaml_helpers::get_i64(yaml, #unity_field_name).map(|v| v as i32) } + } else if type_str == "bool" { + quote! { cursebreaker_parser::yaml_helpers::get_bool(yaml, #unity_field_name) } + } else if type_str == "String" { + quote! { cursebreaker_parser::yaml_helpers::get_string(yaml, #unity_field_name) } + } else if type_str == "Vector2" { + quote! { cursebreaker_parser::yaml_helpers::get_vector2(yaml, #unity_field_name) } + } else if type_str == "Vector3" { + quote! { cursebreaker_parser::yaml_helpers::get_vector3(yaml, #unity_field_name) } + } else if type_str == "Quaternion" { + quote! { cursebreaker_parser::yaml_helpers::get_quaternion(yaml, #unity_field_name) } + } else if type_str == "Color" { + quote! { cursebreaker_parser::yaml_helpers::get_color(yaml, #unity_field_name) } + } else if type_str == "FileRef" { + quote! { cursebreaker_parser::yaml_helpers::get_file_ref(yaml, #unity_field_name) } + } else if type_str == "ExternalRef" { + quote! { cursebreaker_parser::yaml_helpers::get_external_ref(yaml, #unity_field_name) } + } else if type_str == "Vec" { + quote! { cursebreaker_parser::yaml_helpers::get_file_ref_array(yaml, #unity_field_name) } + } else { + panic!( + "Unsupported field type: {}. Supported types: f64, f32, i64, i32, bool, String, Vector2, Vector3, Quaternion, Color, FileRef, ExternalRef, Vec", + type_str + ); + }; + + // Wrap with Default::default() fallback + quote! { + #helper_call.unwrap_or_else(Default::default) + } +} diff --git a/cursebreaker-parser/Cargo.toml b/cursebreaker-parser/Cargo.toml new file mode 100644 index 0000000..c89f1d6 --- /dev/null +++ b/cursebreaker-parser/Cargo.toml @@ -0,0 +1,60 @@ +[package] +name = "cursebreaker-parser" +version = "0.1.0" +edition = "2021" +authors = ["Your Name "] +license = "MIT OR Apache-2.0" +description = "A high-performance Rust library for parsing Unity project files (.unity, .prefab, .asset)" +repository = "https://github.com/yourusername/cursebreaker-parser-rust" +keywords = ["unity", "parser", "yaml", "gamedev"] +categories = ["parser-implementations", "game-development"] +rust-version = "1.70" + +[lib] +name = "cursebreaker_parser" +path = "src/lib.rs" + +[dependencies] +# YAML parsing +serde_yaml = "0.9" +serde = { version = "1.0", features = ["derive"] } + +# Error handling +thiserror = "1.0" + +# Ordered maps for properties +indexmap = { version = "2.1", features = ["serde"] } + +# Regex for parsing +regex = "1.10" + +# Math types (Vector2, Vector3, Quaternion, etc.) +glam = { version = "0.29", features = ["serde"] } + +# ECS (Entity Component System) +sparsey = "0.13" + +# LRU cache for reference resolution +lru = "0.12" + +# Directory traversal for loading projects +walkdir = "2.4" + +# Lazy static initialization for type registry +once_cell = "1.19" + +# Component registry for custom MonoBehaviours +inventory = "0.3" + +# Procedural macro for derive(UnityComponent) +cursebreaker-parser-macros = { path = "../cursebreaker-parser-macros" } + +[dev-dependencies] +# Testing utilities +pretty_assertions = "1.4" + +[features] +default = [] + +# Future: parallel processing support +parallel = [] diff --git a/examples/basic_parsing.rs b/cursebreaker-parser/examples/basic_parsing.rs similarity index 100% rename from examples/basic_parsing.rs rename to cursebreaker-parser/examples/basic_parsing.rs diff --git a/cursebreaker-parser/examples/custom_component.rs b/cursebreaker-parser/examples/custom_component.rs new file mode 100644 index 0000000..1dc3d3d --- /dev/null +++ b/cursebreaker-parser/examples/custom_component.rs @@ -0,0 +1,101 @@ +//! Example demonstrating how to define custom Unity MonoBehaviour components +//! using the #[derive(UnityComponent)] macro. + +use cursebreaker_parser::{yaml_helpers, ComponentContext, UnityComponent}; + +/// Custom Unity MonoBehaviour component for playing sound effects +/// +/// This mirrors the C# PlaySFX MonoBehaviour: +/// ```csharp +/// public class PlaySFX : MonoBehaviour +/// { +/// [SerializeField] float volume; +/// [SerializeField] float startTime; +/// [SerializeField] float endTime; +/// [SerializeField] bool isLoop; +/// } +/// ``` +#[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, +} + +/// Another example - a custom damage component +#[derive(Debug, Clone, UnityComponent)] +#[unity_class("DamageDealer")] +pub struct DamageDealer { + #[unity_field("damageAmount")] + pub damage_amount: f64, + + #[unity_field("damageType")] + pub damage_type: String, + + #[unity_field("canCrit")] + pub can_crit: bool, + + #[unity_field("critMultiplier")] + pub crit_multiplier: f64, +} + +fn main() { + println!("Custom Unity Component Example"); + println!("===============================\n"); + + println!("Defined custom components:"); + println!(" - PlaySFX: volume, start_time, end_time, is_loop"); + println!(" - DamageDealer: damage_amount, damage_type, can_crit, crit_multiplier\n"); + + println!("These components are automatically registered via the inventory crate."); + println!("When parsing Unity files, they will be recognized and parsed automatically.\n"); + + // Demonstrate parsing from YAML + 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(); + + // Create a dummy context + use cursebreaker_parser::{ComponentContext, FileID}; + let ctx = ComponentContext { + type_id: 114, + file_id: FileID::from_i64(12345), + class_name: "PlaySFX", + entity: None, + linking_ctx: None, + yaml: mapping, + }; + + // Parse the component + if let Some(play_sfx) = PlaySFX::parse(mapping, &ctx) { + println!("Successfully parsed PlaySFX component:"); + println!(" volume: {}", play_sfx.volume); + println!(" start_time: {}", play_sfx.start_time); + println!(" end_time: {}", play_sfx.end_time); + println!(" is_loop: {}", play_sfx.is_loop); + } else { + println!("Failed to parse PlaySFX component"); + } + + println!("\nTo use in your own code:"); + println!(" 1. Define a struct matching your C# MonoBehaviour fields"); + println!(" 2. Add #[derive(UnityComponent)] to the struct"); + println!(" 3. Add #[unity_class(\"YourClassName\")] to specify the Unity class name"); + println!(" 4. Add #[unity_field(\"fieldName\")] to each field"); + println!(" 5. The component will be automatically registered and parsed!"); +} diff --git a/examples/guid_resolution.rs.disabled b/cursebreaker-parser/examples/guid_resolution.rs.disabled similarity index 100% rename from examples/guid_resolution.rs.disabled rename to cursebreaker-parser/examples/guid_resolution.rs.disabled diff --git a/src/ecs/builder.rs b/cursebreaker-parser/src/ecs/builder.rs similarity index 78% rename from src/ecs/builder.rs rename to cursebreaker-parser/src/ecs/builder.rs index a7de47d..d5523c2 100644 --- a/src/ecs/builder.rs +++ b/cursebreaker-parser/src/ecs/builder.rs @@ -190,11 +190,43 @@ fn attach_component( } } _ => { - // Unknown component type - skip with warning - eprintln!( - "Warning: Skipping unknown component type: {}", - doc.class_name - ); + // Check if this is a registered custom component + let mut found_custom = false; + for reg in inventory::iter:: { + if reg.class_name == doc.class_name.as_str() { + found_custom = true; + // Try to parse the component + if let Some(_boxed_component) = (reg.parser)(yaml, &ctx) { + eprintln!( + "Info: Custom component '{}' parsed successfully via #[derive(UnityComponent)]", + doc.class_name + ); + eprintln!( + "Note: ECS integration for custom components is not yet fully implemented." + ); + eprintln!( + " Component data was parsed but not inserted into the ECS world." + ); + eprintln!( + " To use this data, access it directly from the parsed documents." + ); + // TODO: Future enhancement - add dynamic component insertion support + // This would require either: + // 1. A type-erased component enum wrapper + // 2. Component trait objects in the ECS + // 3. User-defined registration with downcasting logic + } + break; + } + } + + if !found_custom { + // Unknown component type - skip with warning + eprintln!( + "Warning: Skipping unknown component type: {}", + doc.class_name + ); + } } } diff --git a/src/ecs/mod.rs b/cursebreaker-parser/src/ecs/mod.rs similarity index 100% rename from src/ecs/mod.rs rename to cursebreaker-parser/src/ecs/mod.rs diff --git a/src/error.rs b/cursebreaker-parser/src/error.rs similarity index 100% rename from src/error.rs rename to cursebreaker-parser/src/error.rs diff --git a/src/lib.rs b/cursebreaker-parser/src/lib.rs similarity index 75% rename from src/lib.rs rename to cursebreaker-parser/src/lib.rs index 2c63b27..3a6fa44 100644 --- a/src/lib.rs +++ b/cursebreaker-parser/src/lib.rs @@ -42,8 +42,11 @@ pub use parser::{meta::MetaFile, parse_unity_file}; // pub use project::UnityProject; pub use property::PropertyValue; pub use types::{ - get_class_name, get_type_id, Color, ComponentContext, ExternalRef, FileID, FileRef, - GameObject, LocalID, PrefabInstance, PrefabInstanceComponent, PrefabModification, - PrefabResolver, Quaternion, RectTransform, Transform, UnityComponent, UnityReference, Vector2, - Vector3, yaml_helpers, + get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, ExternalRef, + FileID, FileRef, GameObject, LocalID, PrefabInstance, PrefabInstanceComponent, + PrefabModification, PrefabResolver, Quaternion, RectTransform, Transform, UnityComponent, + UnityReference, Vector2, Vector3, yaml_helpers, }; + +// Re-export the derive macro from the macro crate +pub use cursebreaker_parser_macros::UnityComponent; diff --git a/src/model/mod.rs b/cursebreaker-parser/src/model/mod.rs similarity index 100% rename from src/model/mod.rs rename to cursebreaker-parser/src/model/mod.rs diff --git a/src/parser/meta.rs b/cursebreaker-parser/src/parser/meta.rs similarity index 100% rename from src/parser/meta.rs rename to cursebreaker-parser/src/parser/meta.rs diff --git a/src/parser/mod.rs b/cursebreaker-parser/src/parser/mod.rs similarity index 100% rename from src/parser/mod.rs rename to cursebreaker-parser/src/parser/mod.rs diff --git a/src/parser/unity_tag.rs b/cursebreaker-parser/src/parser/unity_tag.rs similarity index 100% rename from src/parser/unity_tag.rs rename to cursebreaker-parser/src/parser/unity_tag.rs diff --git a/src/parser/yaml.rs b/cursebreaker-parser/src/parser/yaml.rs similarity index 100% rename from src/parser/yaml.rs rename to cursebreaker-parser/src/parser/yaml.rs diff --git a/src/project/mod.rs b/cursebreaker-parser/src/project/mod.rs similarity index 100% rename from src/project/mod.rs rename to cursebreaker-parser/src/project/mod.rs diff --git a/src/project/query.rs b/cursebreaker-parser/src/project/query.rs similarity index 100% rename from src/project/query.rs rename to cursebreaker-parser/src/project/query.rs diff --git a/src/property/mod.rs b/cursebreaker-parser/src/property/mod.rs similarity index 100% rename from src/property/mod.rs rename to cursebreaker-parser/src/property/mod.rs diff --git a/src/types/component.rs b/cursebreaker-parser/src/types/component.rs similarity index 92% rename from src/types/component.rs rename to cursebreaker-parser/src/types/component.rs index 4db471c..dd742d7 100644 --- a/src/types/component.rs +++ b/cursebreaker-parser/src/types/component.rs @@ -84,6 +84,22 @@ pub trait UnityComponent: Sized { fn parse(yaml: &Mapping, ctx: &ComponentContext) -> Option; } +/// Registration entry for custom Unity components +/// +/// This is submitted via the `inventory` crate by the `#[derive(UnityComponent)]` macro +/// to enable automatic component discovery and parsing. +pub struct ComponentRegistration { + /// Unity type ID (usually 114 for MonoBehaviour) + pub type_id: u32, + /// Unity class name (e.g., "PlaySFX") + pub class_name: &'static str, + /// Parser function that creates a boxed component from YAML + pub parser: fn(&Mapping, &ComponentContext) -> Option>, +} + +// Collect all component registrations submitted via the macro +inventory::collect!(ComponentRegistration); + /// Helper functions for parsing typed values from YAML mappings pub mod yaml_helpers { use super::*; diff --git a/src/types/game_object.rs b/cursebreaker-parser/src/types/game_object.rs similarity index 100% rename from src/types/game_object.rs rename to cursebreaker-parser/src/types/game_object.rs diff --git a/src/types/ids.rs b/cursebreaker-parser/src/types/ids.rs similarity index 100% rename from src/types/ids.rs rename to cursebreaker-parser/src/types/ids.rs diff --git a/src/types/mod.rs b/cursebreaker-parser/src/types/mod.rs similarity index 84% rename from src/types/mod.rs rename to cursebreaker-parser/src/types/mod.rs index 780a81c..dd99a0f 100644 --- a/src/types/mod.rs +++ b/cursebreaker-parser/src/types/mod.rs @@ -13,7 +13,10 @@ mod transform; mod type_registry; mod values; -pub use component::{yaml_helpers, ComponentContext, LinkCallback, LinkingContext, UnityComponent}; +pub use component::{ + yaml_helpers, ComponentContext, ComponentRegistration, LinkCallback, LinkingContext, + UnityComponent, +}; pub use game_object::GameObject; pub use ids::{FileID, LocalID}; pub use prefab_instance::{ diff --git a/src/types/prefab_instance.rs b/cursebreaker-parser/src/types/prefab_instance.rs similarity index 100% rename from src/types/prefab_instance.rs rename to cursebreaker-parser/src/types/prefab_instance.rs diff --git a/src/types/reference.rs b/cursebreaker-parser/src/types/reference.rs similarity index 100% rename from src/types/reference.rs rename to cursebreaker-parser/src/types/reference.rs diff --git a/src/types/transform.rs b/cursebreaker-parser/src/types/transform.rs similarity index 100% rename from src/types/transform.rs rename to cursebreaker-parser/src/types/transform.rs diff --git a/src/types/type_registry.rs b/cursebreaker-parser/src/types/type_registry.rs similarity index 100% rename from src/types/type_registry.rs rename to cursebreaker-parser/src/types/type_registry.rs diff --git a/src/types/values.rs b/cursebreaker-parser/src/types/values.rs similarity index 100% rename from src/types/values.rs rename to cursebreaker-parser/src/types/values.rs diff --git a/tests/integration_tests.rs b/cursebreaker-parser/tests/integration_tests.rs similarity index 100% rename from tests/integration_tests.rs rename to cursebreaker-parser/tests/integration_tests.rs diff --git a/cursebreaker-parser/tests/macro_tests.rs b/cursebreaker-parser/tests/macro_tests.rs new file mode 100644 index 0000000..021e703 --- /dev/null +++ b/cursebreaker-parser/tests/macro_tests.rs @@ -0,0 +1,197 @@ +//! Tests for the #[derive(UnityComponent)] macro + +use cursebreaker_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:: { + 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:: { + 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::().is_some(), + "Parsed component is not PlaySFX type" + ); + + return; + } + } + + panic!("PlaySFX registration not found"); +} diff --git a/tests/test_guid_resolution.rs.disabled b/cursebreaker-parser/tests/test_guid_resolution.rs.disabled similarity index 100% rename from tests/test_guid_resolution.rs.disabled rename to cursebreaker-parser/tests/test_guid_resolution.rs.disabled