custom component parsing
This commit is contained in:
@@ -9,7 +9,8 @@
|
|||||||
"WebFetch(domain:docs.rs)",
|
"WebFetch(domain:docs.rs)",
|
||||||
"Bash(findstr:*)",
|
"Bash(findstr:*)",
|
||||||
"Bash(cargo check:*)",
|
"Bash(cargo check:*)",
|
||||||
"Bash(ls:*)"
|
"Bash(ls:*)",
|
||||||
|
"Bash(find:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
Cargo.lock
generated
26
Cargo.lock
generated
@@ -27,8 +27,10 @@ checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c"
|
|||||||
name = "cursebreaker-parser"
|
name = "cursebreaker-parser"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cursebreaker-parser-macros",
|
||||||
"glam",
|
"glam",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
"inventory",
|
||||||
"lru",
|
"lru",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
@@ -40,6 +42,15 @@ dependencies = [
|
|||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cursebreaker-parser-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.13"
|
version = "0.1.13"
|
||||||
@@ -96,6 +107,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "inventory"
|
||||||
|
version = "0.3.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.17"
|
version = "1.0.17"
|
||||||
@@ -186,6 +206,12 @@ version = "2.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustversion"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
|
|||||||
54
Cargo.toml
54
Cargo.toml
@@ -1,54 +1,10 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "cursebreaker-parser"
|
members = ["cursebreaker-parser", "cursebreaker-parser-macros"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Your Name <your.email@example.com>"]
|
authors = ["Your Name <your.email@example.com>"]
|
||||||
license = "MIT OR Apache-2.0"
|
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"
|
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 = []
|
|
||||||
|
|||||||
444
SUMMARY.md
444
SUMMARY.md
@@ -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<FileID, Entity>, // 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<RawDocument>, // 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<Self>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**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<RawDocument>, // 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<HashMap<FileID, Entity>>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**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<PathBuf, UnityFile>` 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<Path>) -> 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<Self> {
|
|
||||||
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! 🚀
|
|
||||||
245
TESTING.md
245
TESTING.md
@@ -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
|
|
||||||
16
cursebreaker-parser-macros/Cargo.toml
Normal file
16
cursebreaker-parser-macros/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "cursebreaker-parser-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Your Name <your.email@example.com>"]
|
||||||
|
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"
|
||||||
201
cursebreaker-parser-macros/src/lib.rs
Normal file
201
cursebreaker-parser-macros/src/lib.rs
Normal file
@@ -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<Self> {
|
||||||
|
#(#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<dyn std::any::Any>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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::<u32>() {
|
||||||
|
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<FileRef>" {
|
||||||
|
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<FileRef>",
|
||||||
|
type_str
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wrap with Default::default() fallback
|
||||||
|
quote! {
|
||||||
|
#helper_call.unwrap_or_else(Default::default)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
cursebreaker-parser/Cargo.toml
Normal file
60
cursebreaker-parser/Cargo.toml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
[package]
|
||||||
|
name = "cursebreaker-parser"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
authors = ["Your Name <your.email@example.com>"]
|
||||||
|
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 = []
|
||||||
101
cursebreaker-parser/examples/custom_component.rs
Normal file
101
cursebreaker-parser/examples/custom_component.rs
Normal file
@@ -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!");
|
||||||
|
}
|
||||||
@@ -190,6 +190,37 @@ fn attach_component(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
// Check if this is a registered custom component
|
||||||
|
let mut found_custom = false;
|
||||||
|
for reg in inventory::iter::<crate::types::ComponentRegistration> {
|
||||||
|
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
|
// Unknown component type - skip with warning
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Warning: Skipping unknown component type: {}",
|
"Warning: Skipping unknown component type: {}",
|
||||||
@@ -197,6 +228,7 @@ fn attach_component(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -42,8 +42,11 @@ pub use parser::{meta::MetaFile, parse_unity_file};
|
|||||||
// pub use project::UnityProject;
|
// pub use project::UnityProject;
|
||||||
pub use property::PropertyValue;
|
pub use property::PropertyValue;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
get_class_name, get_type_id, Color, ComponentContext, ExternalRef, FileID, FileRef,
|
get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, ExternalRef,
|
||||||
GameObject, LocalID, PrefabInstance, PrefabInstanceComponent, PrefabModification,
|
FileID, FileRef, GameObject, LocalID, PrefabInstance, PrefabInstanceComponent,
|
||||||
PrefabResolver, Quaternion, RectTransform, Transform, UnityComponent, UnityReference, Vector2,
|
PrefabModification, PrefabResolver, Quaternion, RectTransform, Transform, UnityComponent,
|
||||||
Vector3, yaml_helpers,
|
UnityReference, Vector2, Vector3, yaml_helpers,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Re-export the derive macro from the macro crate
|
||||||
|
pub use cursebreaker_parser_macros::UnityComponent;
|
||||||
@@ -84,6 +84,22 @@ pub trait UnityComponent: Sized {
|
|||||||
fn parse(yaml: &Mapping, ctx: &ComponentContext) -> Option<Self>;
|
fn parse(yaml: &Mapping, ctx: &ComponentContext) -> Option<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<Box<dyn std::any::Any>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all component registrations submitted via the macro
|
||||||
|
inventory::collect!(ComponentRegistration);
|
||||||
|
|
||||||
/// Helper functions for parsing typed values from YAML mappings
|
/// Helper functions for parsing typed values from YAML mappings
|
||||||
pub mod yaml_helpers {
|
pub mod yaml_helpers {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -13,7 +13,10 @@ mod transform;
|
|||||||
mod type_registry;
|
mod type_registry;
|
||||||
mod values;
|
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 game_object::GameObject;
|
||||||
pub use ids::{FileID, LocalID};
|
pub use ids::{FileID, LocalID};
|
||||||
pub use prefab_instance::{
|
pub use prefab_instance::{
|
||||||
197
cursebreaker-parser/tests/macro_tests.rs
Normal file
197
cursebreaker-parser/tests/macro_tests.rs
Normal file
@@ -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::<cursebreaker_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::<cursebreaker_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