working demo part 1
This commit is contained in:
@@ -10,7 +10,8 @@
|
||||
"Bash(findstr:*)",
|
||||
"Bash(cargo check:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(find:*)"
|
||||
"Bash(find:*)",
|
||||
"Bash(grep:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
491
ROADMAP.md
Normal file
491
ROADMAP.md
Normal file
@@ -0,0 +1,491 @@
|
||||
# ROADMAP: GUID Resolution for MonoBehaviour Components
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The `#[derive(UnityComponent)]` macro system is **fully functional** for parsing custom Unity components. However, Unity stores custom MonoBehaviour scripts in scene/prefab files using a generic `MonoBehaviour` class name with a GUID reference to the actual script. To enable automatic discovery and parsing of custom components like `PlaySFX` from real Unity projects, we need to implement **GUID → Class Name resolution**.
|
||||
|
||||
## Current Status ✅
|
||||
|
||||
### What's Working
|
||||
|
||||
1. **Procedural Macro System**
|
||||
- `#[derive(UnityComponent)]` generates parsing code automatically
|
||||
- Supports all Unity primitive types (f64, String, bool, Vector3, etc.)
|
||||
- Generates `UnityComponent::parse()` implementation
|
||||
- Generates `EcsInsertable::insert_into_world()` implementation
|
||||
|
||||
2. **Auto-Registration via Inventory**
|
||||
- Components automatically register themselves at compile time
|
||||
- `build_world_from_documents()` discovers and registers all custom components
|
||||
- No manual modification of builder.rs needed
|
||||
|
||||
3. **Type Filtering System**
|
||||
- `TypeFilter` allows selective parsing for performance
|
||||
- `parse_with_types!` macro for ergonomic type selection
|
||||
- Works with both Unity types and custom types
|
||||
|
||||
4. **ECS Integration**
|
||||
- Custom components insert into Sparsey ECS world
|
||||
- Components can be queried via `world.borrow::<PlaySFX>()`
|
||||
- Full integration with existing Transform, GameObject, etc.
|
||||
|
||||
### What's Missing
|
||||
|
||||
**MonoBehaviour GUID Resolution**: Unity scene/prefab files store custom scripts like this:
|
||||
|
||||
```yaml
|
||||
--- !u!114 &1234567
|
||||
MonoBehaviour: # ⚠️ Generic class name, not "PlaySFX"
|
||||
m_GameObject: {fileID: 890}
|
||||
m_Script: {fileID: 11500000, guid: 091c537484687e9419460cdcd7038234, type: 3} # 👈 Actual type
|
||||
volume: 1.0
|
||||
startTime: 0.0
|
||||
endTime: 5.0
|
||||
isLoop: 0
|
||||
```
|
||||
|
||||
**Problem**: When parsing, we see `class_name = "MonoBehaviour"`, so we can't match it to the `PlaySFX` component registration.
|
||||
|
||||
**Solution**: Resolve the `m_Script` GUID to discover the actual class name `"PlaySFX"`.
|
||||
|
||||
## The GUID Resolution Feature
|
||||
|
||||
### How Unity GUID Resolution Works
|
||||
|
||||
1. **Every asset has a .meta file** alongside it with a unique GUID:
|
||||
```
|
||||
Assets/Scripts/PlaySFX.cs # The actual C# script
|
||||
Assets/Scripts/PlaySFX.cs.meta # Contains GUID
|
||||
```
|
||||
|
||||
2. **The .meta file contains the GUID**:
|
||||
```yaml
|
||||
fileFormatVersion: 2
|
||||
guid: 091c537484687e9419460cdcd7038234 # 👈 This is the GUID
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
```
|
||||
|
||||
3. **MonoBehaviour references the GUID**:
|
||||
```yaml
|
||||
m_Script: {fileID: 11500000, guid: 091c537484687e9419460cdcd7038234, type: 3}
|
||||
```
|
||||
|
||||
4. **We need to map**: `GUID → Script Path → Class Name`
|
||||
- `091c537484687e9419460cdcd7038234` → `Assets/Scripts/PlaySFX.cs` → `"PlaySFX"`
|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Unity Project Parsing │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. Scan for .meta files (*.cs.meta) │
|
||||
│ Build GUID → Asset Path mapping │
|
||||
│ Parse class name from .cs files │
|
||||
│ Result: GuidResolver { guid → class_name } │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 2. Parse Unity files (.unity, .prefab) │
|
||||
│ When encountering MonoBehaviour: │
|
||||
│ - Extract m_Script.guid │
|
||||
│ - Lookup guid in GuidResolver │
|
||||
│ - Get actual class_name (e.g., "PlaySFX") │
|
||||
│ - Match with component registry │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 3. Component Registration (via inventory) │
|
||||
│ Check if class_name matches registered component │
|
||||
│ If match found: │
|
||||
│ - Call component.parse_and_insert() │
|
||||
│ - Insert into ECS world │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: GUID → Asset Path Resolution
|
||||
|
||||
**Goal**: Build a mapping from GUID to file path by scanning .meta files
|
||||
|
||||
**New Module**: `src/parser/guid_resolver.rs`
|
||||
|
||||
```rust
|
||||
/// Resolves Unity GUIDs to asset paths
|
||||
pub struct GuidResolver {
|
||||
/// Map from GUID string to asset file path
|
||||
guid_to_path: HashMap<String, PathBuf>,
|
||||
}
|
||||
|
||||
impl GuidResolver {
|
||||
/// Scan a Unity project directory for .meta files
|
||||
pub fn from_project(project_path: &Path) -> Result<Self> {
|
||||
// 1. Find all *.meta files (recursively)
|
||||
// 2. Parse each .meta file to extract GUID
|
||||
// 3. Derive asset path from .meta path (remove .meta extension)
|
||||
// 4. Build HashMap
|
||||
}
|
||||
|
||||
/// Look up a GUID to get the asset path
|
||||
pub fn resolve(&self, guid: &str) -> Option<&Path> {
|
||||
self.guid_to_path.get(guid).map(|p| p.as_path())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Create/Modify**:
|
||||
- 📄 `src/parser/guid_resolver.rs` - New file
|
||||
- 📄 `src/parser/mod.rs` - Export GuidResolver
|
||||
- 📄 `src/lib.rs` - Re-export GuidResolver
|
||||
|
||||
**Implementation Details**:
|
||||
1. Recursively walk project directory
|
||||
2. Filter for `*.cs.meta` files (MonoBehaviour scripts)
|
||||
3. Parse YAML to extract `guid:` field
|
||||
4. Store `guid → "Assets/Scripts/PlaySFX.cs"` mapping
|
||||
|
||||
### Phase 2: Class Name Extraction from C# Files
|
||||
|
||||
**Goal**: Parse .cs files to extract the class name
|
||||
|
||||
**Approach**: Simple regex/string parsing (no full C# parser needed)
|
||||
|
||||
```rust
|
||||
impl GuidResolver {
|
||||
/// Extract class name from a C# script file
|
||||
fn extract_class_name(cs_path: &Path) -> Result<String> {
|
||||
let content = std::fs::read_to_string(cs_path)?;
|
||||
|
||||
// Look for: "public class PlaySFX" or "class PlaySFX"
|
||||
// Regex: r"(?:public\s+)?class\s+(\w+)"
|
||||
// Return the captured class name
|
||||
}
|
||||
|
||||
/// Resolve GUID to class name
|
||||
pub fn resolve_class_name(&self, guid: &str) -> Option<String> {
|
||||
let path = self.resolve(guid)?;
|
||||
Self::extract_class_name(path).ok()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Modify**:
|
||||
- 📄 `src/parser/guid_resolver.rs` - Add class name extraction
|
||||
- 📄 `Cargo.toml` - Add `regex` dependency (optional, can use manual parsing)
|
||||
|
||||
### Phase 3: MonoBehaviour Parser Enhancement
|
||||
|
||||
**Goal**: Extract m_Script GUID when parsing MonoBehaviour components
|
||||
|
||||
**Current MonoBehaviour Parsing** (`builder.rs` line 206):
|
||||
```rust
|
||||
_ => {
|
||||
// Check if this is a registered custom component
|
||||
for reg in inventory::iter::<crate::types::ComponentRegistration> {
|
||||
if reg.class_name == doc.class_name.as_str() { // ⚠️ Won't match "MonoBehaviour"
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Enhanced MonoBehaviour Parsing**:
|
||||
```rust
|
||||
"MonoBehaviour" => {
|
||||
// Extract m_Script GUID
|
||||
if let Some(script_ref) = yaml_helpers::get_external_ref(yaml, "m_Script") {
|
||||
let guid = script_ref.guid();
|
||||
|
||||
// Resolve GUID to class name (requires access to GuidResolver)
|
||||
if let Some(class_name) = guid_resolver.resolve_class_name(guid) {
|
||||
// Now match against registered components
|
||||
for reg in inventory::iter::<crate::types::ComponentRegistration> {
|
||||
if reg.class_name == class_name.as_str() {
|
||||
if (reg.parse_and_insert)(yaml, &ctx, world, entity) {
|
||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to generic MonoBehaviour warning
|
||||
eprintln!("Warning: Skipping unknown MonoBehaviour");
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Modify**:
|
||||
- 📄 `src/ecs/builder.rs` - Add MonoBehaviour case, pass GuidResolver
|
||||
- 📄 `src/types/references.rs` - Ensure ExternalRef has `guid()` method
|
||||
- 📄 `src/types/component.rs` - Add `get_external_ref` to yaml_helpers (if not exists)
|
||||
|
||||
**Challenge**: How to pass GuidResolver to `build_world_from_documents()`?
|
||||
|
||||
**Option A**: Add parameter (breaking change)
|
||||
```rust
|
||||
pub fn build_world_from_documents(
|
||||
documents: Vec<RawDocument>,
|
||||
guid_resolver: Option<&GuidResolver>, // 👈 New parameter
|
||||
) -> Result<(World, HashMap<FileID, Entity>)>
|
||||
```
|
||||
|
||||
**Option B**: Store in ComponentContext (preferred)
|
||||
```rust
|
||||
pub struct ComponentContext<'a> {
|
||||
pub type_id: u32,
|
||||
pub file_id: FileID,
|
||||
pub class_name: &'a str,
|
||||
pub entity: Option<Entity>,
|
||||
pub linking_ctx: Option<&'a RefCell<LinkingContext>>,
|
||||
pub yaml: &'a Mapping,
|
||||
pub guid_resolver: Option<&'a GuidResolver>, // 👈 New field
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 4: Integration with UnityFile Parsing
|
||||
|
||||
**Goal**: Build GuidResolver when loading a Unity project
|
||||
|
||||
**Current Flow** (`UnityFile::from_path`):
|
||||
```rust
|
||||
pub fn from_path(path: &Path) -> Result<UnityFile> {
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let documents = parse_unity_file(&content)?;
|
||||
|
||||
match file_type {
|
||||
Scene => {
|
||||
let (world, entity_map) = build_world_from_documents(documents)?;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Enhanced Flow**:
|
||||
```rust
|
||||
pub fn from_path(path: &Path) -> Result<UnityFile> {
|
||||
// 1. Detect if this is part of a Unity project
|
||||
let project_root = find_project_root(path)?;
|
||||
|
||||
// 2. Build GUID resolver for the project
|
||||
let guid_resolver = GuidResolver::from_project(&project_root)?;
|
||||
|
||||
// 3. Parse file with GUID resolution
|
||||
let content = std::fs::read_to_string(path)?;
|
||||
let documents = parse_unity_file(&content)?;
|
||||
|
||||
match file_type {
|
||||
Scene => {
|
||||
let (world, entity_map) = build_world_from_documents(
|
||||
documents,
|
||||
Some(&guid_resolver) // 👈 Pass resolver
|
||||
)?;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Files to Modify**:
|
||||
- 📄 `src/model.rs` - Update `UnityFile::from_path()`
|
||||
- 📄 `src/parser/guid_resolver.rs` - Add `find_project_root()` helper
|
||||
|
||||
**Optimization**: Cache GuidResolver per project (expensive to rebuild)
|
||||
```rust
|
||||
// Thread-local cache for performance
|
||||
thread_local! {
|
||||
static GUID_CACHE: RefCell<HashMap<PathBuf, Arc<GuidResolver>>> = RefCell::new(HashMap::new());
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 5: Update Examples and Tests
|
||||
|
||||
**Examples to Update**:
|
||||
- 📄 `examples/find_playsfx.rs` - Should now find PlaySFX components!
|
||||
- 📄 `examples/ecs_integration.rs` - Add example with GUID resolution
|
||||
|
||||
**Tests to Add**:
|
||||
- 📄 `tests/guid_resolution_tests.rs` - New test file
|
||||
- Test GuidResolver can scan project
|
||||
- Test GUID → path resolution
|
||||
- Test class name extraction
|
||||
- Integration test with VR_Horror project
|
||||
|
||||
**Integration Test**:
|
||||
```rust
|
||||
#[test]
|
||||
fn test_guid_resolution_vr_horror() {
|
||||
let project_path = Path::new("test_data/VR_Horror_YouCantRun");
|
||||
|
||||
// Build resolver
|
||||
let resolver = GuidResolver::from_project(project_path).unwrap();
|
||||
|
||||
// Known PlaySFX GUID
|
||||
let playsfx_guid = "091c537484687e9419460cdcd7038234";
|
||||
|
||||
// Should resolve to class name
|
||||
assert_eq!(
|
||||
resolver.resolve_class_name(playsfx_guid),
|
||||
Some("PlaySFX".to_string())
|
||||
);
|
||||
|
||||
// Now parse a scene with PlaySFX
|
||||
let scene_path = project_path.join("Assets/Scenes/TEST/Final_1F/1F.unity");
|
||||
let file = UnityFile::from_path(&scene_path).unwrap();
|
||||
|
||||
if let UnityFile::Scene(scene) = file {
|
||||
// Should find PlaySFX components
|
||||
let playsfx_view = scene.world.borrow::<PlaySFX>();
|
||||
let mut count = 0;
|
||||
for entity in scene.entity_map.values() {
|
||||
if playsfx_view.get(*entity).is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
assert!(count > 0, "Should find at least one PlaySFX component");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## File Checklist
|
||||
|
||||
### New Files to Create
|
||||
- [ ] `src/parser/guid_resolver.rs` - GUID resolution logic
|
||||
- [ ] `tests/guid_resolution_tests.rs` - Unit and integration tests
|
||||
|
||||
### Existing Files to Modify
|
||||
- [ ] `src/parser/mod.rs` - Export GuidResolver
|
||||
- [ ] `src/lib.rs` - Re-export GuidResolver
|
||||
- [ ] `src/ecs/builder.rs` - Add MonoBehaviour GUID resolution
|
||||
- [ ] `src/model.rs` - Update UnityFile::from_path()
|
||||
- [ ] `src/types/component.rs` - Add guid_resolver to ComponentContext
|
||||
- [ ] `src/types/references.rs` - Ensure ExternalRef has guid() method
|
||||
- [ ] `Cargo.toml` - Add regex dependency (optional)
|
||||
- [ ] `examples/find_playsfx.rs` - Update with notes/verification
|
||||
- [ ] `examples/ecs_integration.rs` - Add GUID resolution example
|
||||
|
||||
## Technical Challenges & Solutions
|
||||
|
||||
### Challenge 1: Performance - Scanning Large Projects
|
||||
|
||||
**Problem**: Scanning thousands of .meta files on every parse is slow
|
||||
|
||||
**Solutions**:
|
||||
1. **Lazy Loading**: Only scan when needed (first MonoBehaviour encountered)
|
||||
2. **Caching**: Store GuidResolver per project path in thread-local cache
|
||||
3. **Incremental**: Only scan Assets/ directory, skip Library/Temp
|
||||
4. **Parallel**: Use rayon to scan .meta files in parallel
|
||||
|
||||
**Recommendation**: Start with simple implementation, optimize if needed
|
||||
|
||||
### Challenge 2: Class Name Extraction Complexity
|
||||
|
||||
**Problem**: C# parsing can be complex (namespaces, partial classes, etc.)
|
||||
|
||||
**Solutions**:
|
||||
1. **Simple Regex**: `r"(?:public\s+)?class\s+(\w+)(?:\s*:\s*MonoBehaviour)?"`
|
||||
- Good enough for 95% of cases
|
||||
- Fast and simple
|
||||
2. **Full C# Parser**: Use tree-sitter or similar
|
||||
- Overkill for this use case
|
||||
- Adds heavy dependency
|
||||
|
||||
**Recommendation**: Start with regex, handle edge cases as discovered
|
||||
|
||||
### Challenge 3: Breaking API Changes
|
||||
|
||||
**Problem**: Adding GuidResolver parameter changes public API
|
||||
|
||||
**Solutions**:
|
||||
1. **Option A**: Make it optional with `Option<&GuidResolver>`
|
||||
- Backward compatible
|
||||
- GUIDs won't resolve if not provided
|
||||
2. **Option B**: Add separate method `from_path_with_resolver()`
|
||||
- Keep original API unchanged
|
||||
- Explicit opt-in to GUID resolution
|
||||
3. **Option C**: Detect and auto-create GuidResolver
|
||||
- Best user experience
|
||||
- Always attempt GUID resolution
|
||||
- Fall back gracefully if project not detected
|
||||
|
||||
**Recommendation**: Option C - Auto-detect and resolve
|
||||
|
||||
### Challenge 4: Multiple Scripts Per File
|
||||
|
||||
**Problem**: C# files can have multiple classes
|
||||
|
||||
**Example**:
|
||||
```csharp
|
||||
public class PlaySFX : MonoBehaviour { }
|
||||
public class SFXHelper { } // Same file!
|
||||
```
|
||||
|
||||
**Solution**: The .meta file is associated with the **file**, not the class. Unity creates separate .meta files for each public MonoBehaviour. Non-MonoBehaviour helper classes in the same file aren't referenced by GUID.
|
||||
|
||||
**Recommendation**: Extract first public class that inherits from MonoBehaviour
|
||||
|
||||
## Success Criteria
|
||||
|
||||
When this feature is complete:
|
||||
|
||||
✅ **Functionality**:
|
||||
- [ ] GuidResolver can scan a Unity project and build GUID mappings
|
||||
- [ ] MonoBehaviour components resolve to their actual class names
|
||||
- [ ] Custom components like PlaySFX are discovered and parsed automatically
|
||||
- [ ] `examples/find_playsfx.rs` finds PlaySFX components in VR_Horror project
|
||||
|
||||
✅ **Performance**:
|
||||
- [ ] GUID resolution adds < 500ms overhead for typical projects
|
||||
- [ ] Caching prevents redundant scanning
|
||||
|
||||
✅ **Testing**:
|
||||
- [ ] Unit tests for GUID resolution
|
||||
- [ ] Integration test finds PlaySFX in VR_Horror
|
||||
- [ ] Edge cases handled (missing .meta, invalid GUIDs, etc.)
|
||||
|
||||
✅ **Documentation**:
|
||||
- [ ] GUID resolution documented in README
|
||||
- [ ] Examples show usage with real Unity projects
|
||||
- [ ] API docs explain GuidResolver usage
|
||||
|
||||
## Timeline Estimate
|
||||
|
||||
- **Phase 1** (GUID → Path): 2-4 hours
|
||||
- **Phase 2** (Class Name Extraction): 1-2 hours
|
||||
- **Phase 3** (MonoBehaviour Parser): 2-3 hours
|
||||
- **Phase 4** (Integration): 2-3 hours
|
||||
- **Phase 5** (Tests & Examples): 2-3 hours
|
||||
|
||||
**Total**: 9-15 hours of development time
|
||||
|
||||
## Future Enhancements (Out of Scope)
|
||||
|
||||
These can be added later if needed:
|
||||
|
||||
1. **Prefab GUID Resolution**: Resolve nested prefab references
|
||||
2. **AssetDatabase**: Full asset path resolution (materials, textures, etc.)
|
||||
3. **GUID Cache File**: Persist GUID mappings to disk for instant loading
|
||||
4. **Watch Mode**: Auto-update GUID mappings when .meta files change
|
||||
5. **Cross-Platform Paths**: Handle Windows/Mac/Linux path differences
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- Unity YAML Format: [UnityYAMLParser](https://github.com/HearthSim/UnityYAMLParser)
|
||||
- Unity .meta Files: [Unity Manual - Meta Files](https://docs.unity3d.com/Manual/class-Meta.html)
|
||||
- GUID Format: RFC 4122 compliant UUIDs without hyphens
|
||||
|
||||
---
|
||||
|
||||
**Status**: 📋 Planning Complete - Ready for Implementation
|
||||
|
||||
**Last Updated**: 2026-01-02
|
||||
@@ -101,7 +101,8 @@ pub fn derive_unity_component(input: TokenStream) -> TokenStream {
|
||||
<#struct_name as cursebreaker_parser::EcsInsertable>::parse_and_insert(
|
||||
yaml, ctx, world, entity
|
||||
)
|
||||
}
|
||||
},
|
||||
register: |builder| builder.register::<#struct_name>()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
194
cursebreaker-parser/examples/find_playsfx.rs
Normal file
194
cursebreaker-parser/examples/find_playsfx.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! Demo: Find all PlaySFX components and their locations in VR_Horror_YouCantRun
|
||||
//!
|
||||
//! This example demonstrates:
|
||||
//! 1. Parsing a real Unity project
|
||||
//! 2. Finding custom MonoBehaviour components (PlaySFX)
|
||||
//! 3. Querying the ECS world for components
|
||||
//! 4. Accessing Transform data for component locations
|
||||
|
||||
use cursebreaker_parser::{UnityComponent, UnityFile};
|
||||
use std::path::Path;
|
||||
|
||||
/// PlaySFX component from VR_Horror_YouCantRun
|
||||
///
|
||||
/// C# definition:
|
||||
/// ```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,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🎮 VR Horror - PlaySFX Component Finder");
|
||||
println!("{}", "=".repeat(70));
|
||||
println!();
|
||||
|
||||
let project_path = Path::new("test_data/VR_Horror_YouCantRun");
|
||||
|
||||
// Check if project exists
|
||||
if !project_path.exists() {
|
||||
eprintln!("❌ Error: VR_Horror_YouCantRun project not found at {}", project_path.display());
|
||||
eprintln!(" Run the integration tests first to download it:");
|
||||
eprintln!(" cargo test test_vr_horror_project");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("📁 Scanning project: {}", project_path.display());
|
||||
println!();
|
||||
|
||||
// Find all Unity scene files
|
||||
let scene_files = find_unity_files(project_path, "unity");
|
||||
|
||||
println!("📄 Found {} scene file(s)", scene_files.len());
|
||||
println!();
|
||||
|
||||
let mut total_playsfx = 0;
|
||||
|
||||
// Parse each scene
|
||||
for scene_path in scene_files {
|
||||
println!("🔍 Parsing: {}", scene_path.file_name().unwrap().to_string_lossy());
|
||||
|
||||
match UnityFile::from_path(&scene_path) {
|
||||
Ok(UnityFile::Scene(scene)) => {
|
||||
// Get views for all component types we need
|
||||
let playsfx_view = scene.world.borrow::<PlaySFX>();
|
||||
let transform_view = scene.world.borrow::<cursebreaker_parser::Transform>();
|
||||
let rect_transform_view = scene.world.borrow::<cursebreaker_parser::RectTransform>();
|
||||
let gameobject_view = scene.world.borrow::<cursebreaker_parser::GameObject>();
|
||||
|
||||
// Find all entities that have PlaySFX
|
||||
let mut found_count = 0;
|
||||
let mut found_entities = Vec::new();
|
||||
|
||||
// Iterate through all entities in the entity_map
|
||||
for entity in scene.entity_map.values() {
|
||||
if let Some(playsfx) = playsfx_view.get(*entity) {
|
||||
found_entities.push((*entity, playsfx.clone()));
|
||||
found_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if found_count > 0 {
|
||||
println!(" ✅ Found {} PlaySFX component(s)", found_count);
|
||||
total_playsfx += found_count;
|
||||
|
||||
// Process each found PlaySFX component
|
||||
for (entity, playsfx) in found_entities {
|
||||
let transform = transform_view.get(entity);
|
||||
let rect_transform = rect_transform_view.get(entity);
|
||||
let game_object = gameobject_view.get(entity);
|
||||
|
||||
let name = game_object
|
||||
.and_then(|go| go.name())
|
||||
.unwrap_or("(unnamed)");
|
||||
|
||||
println!();
|
||||
println!(" 🔊 PlaySFX on GameObject: \"{}\"", name);
|
||||
println!(" Entity: {:?}", entity);
|
||||
println!(" Properties:");
|
||||
println!(" • volume: {}", playsfx.volume);
|
||||
println!(" • startTime: {}", playsfx.start_time);
|
||||
println!(" • endTime: {}", playsfx.end_time);
|
||||
println!(" • isLoop: {}", playsfx.is_loop);
|
||||
|
||||
// Print position if available
|
||||
if let Some(transform) = transform {
|
||||
if let Some(pos) = transform.local_position() {
|
||||
println!(" Transform:");
|
||||
println!(" • Position: ({:.2}, {:.2}, {:.2})",
|
||||
pos.x, pos.y, pos.z);
|
||||
}
|
||||
if let Some(rot) = transform.local_rotation() {
|
||||
println!(" • Rotation: ({:.2}, {:.2}, {:.2}, {:.2})",
|
||||
rot.x, rot.y, rot.z, rot.w);
|
||||
}
|
||||
if let Some(scale) = transform.local_scale() {
|
||||
println!(" • Scale: ({:.2}, {:.2}, {:.2})",
|
||||
scale.x, scale.y, scale.z);
|
||||
}
|
||||
} else if let Some(rect_transform) = rect_transform {
|
||||
let transform = rect_transform.transform();
|
||||
if let Some(pos) = transform.local_position() {
|
||||
println!(" RectTransform (UI):");
|
||||
println!(" • Position: ({:.2}, {:.2}, {:.2})",
|
||||
pos.x, pos.y, pos.z);
|
||||
}
|
||||
} else {
|
||||
println!(" ⚠️ No Transform found");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(" ⊘ No PlaySFX components found");
|
||||
}
|
||||
|
||||
println!();
|
||||
}
|
||||
Ok(_) => {
|
||||
println!(" ⊘ Skipped (not a scene file)");
|
||||
println!();
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ❌ Parse error: {}", e);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}", "=".repeat(70));
|
||||
println!("📊 Summary:");
|
||||
println!(" Total PlaySFX components found: {}", total_playsfx);
|
||||
println!("{}", "=".repeat(70));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find all Unity files with a specific extension in a directory
|
||||
fn find_unity_files(dir: &Path, extension: &str) -> Vec<std::path::PathBuf> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
fn visit_dir(dir: &Path, extension: &str, files: &mut Vec<std::path::PathBuf>) {
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
// Skip Library, Temp, Builds, and .git directories
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name == "Library" || name == "Temp" || name == "Builds" || name == ".git" {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
visit_dir(&path, extension, files);
|
||||
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
|
||||
if ext == extension {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
visit_dir(dir, extension, &mut files);
|
||||
files.sort();
|
||||
files
|
||||
}
|
||||
@@ -25,13 +25,20 @@ use std::collections::HashMap;
|
||||
pub fn build_world_from_documents(
|
||||
documents: Vec<RawDocument>,
|
||||
) -> Result<(World, HashMap<FileID, Entity>)> {
|
||||
// Create World with registered component types
|
||||
let mut world = World::builder()
|
||||
// Create World builder with registered component types
|
||||
let mut builder = World::builder();
|
||||
builder
|
||||
.register::<GameObject>()
|
||||
.register::<Transform>()
|
||||
.register::<RectTransform>()
|
||||
.register::<PrefabInstanceComponent>()
|
||||
.build();
|
||||
.register::<PrefabInstanceComponent>();
|
||||
|
||||
// Register all custom components from inventory
|
||||
for reg in inventory::iter::<crate::types::ComponentRegistration> {
|
||||
(reg.register)(&mut builder);
|
||||
}
|
||||
|
||||
let mut world = builder.build();
|
||||
|
||||
let linking_ctx = RefCell::new(LinkingContext::new());
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::types::*;
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use sparsey::world::WorldBuilder;
|
||||
use sparsey::Entity;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -118,6 +119,8 @@ pub struct ComponentRegistration {
|
||||
pub class_name: &'static str,
|
||||
/// Parser function that parses and inserts the component into the ECS world
|
||||
pub parse_and_insert: fn(&Mapping, &ComponentContext, &mut sparsey::World, Entity) -> bool,
|
||||
/// Function to register this component type with a WorldBuilder
|
||||
pub register: for<'a> fn(&'a mut WorldBuilder) -> &'a mut WorldBuilder,
|
||||
}
|
||||
|
||||
// Collect all component registrations submitted via the macro
|
||||
|
||||
Reference in New Issue
Block a user