prefab instanciation
This commit is contained in:
@@ -12,6 +12,9 @@
|
|||||||
"Bash(ls:*)",
|
"Bash(ls:*)",
|
||||||
"Bash(find:*)",
|
"Bash(find:*)",
|
||||||
"Bash(grep:*)"
|
"Bash(grep:*)"
|
||||||
|
],
|
||||||
|
"additionalDirectories": [
|
||||||
|
"/home/connor/repos/CBAssets/"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
491
ROADMAP.md
491
ROADMAP.md
@@ -1,491 +0,0 @@
|
|||||||
# 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
|
|
||||||
218
cursebreaker-parser/examples/parse_resource_prefabs.rs
Normal file
218
cursebreaker-parser/examples/parse_resource_prefabs.rs
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
//! Parse Cursebreaker Resource Prefabs
|
||||||
|
//!
|
||||||
|
//! This example demonstrates:
|
||||||
|
//! 1. Parsing Cursebreaker prefab files directly
|
||||||
|
//! 2. Finding Interactable_Resource components in prefabs
|
||||||
|
//! 3. Extracting typeId and maxHealth data
|
||||||
|
//! 4. Writing resource data to an output file
|
||||||
|
//!
|
||||||
|
//! Note: The 10_3.unity scene uses prefab instances, and the current parser
|
||||||
|
//! doesn't yet support resolving components from nested prefabs. This example
|
||||||
|
//! parses the prefab files directly instead.
|
||||||
|
|
||||||
|
use cursebreaker_parser::{GuidResolver, UnityComponent, UnityFile};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
/// Interactable_Resource component from Cursebreaker
|
||||||
|
///
|
||||||
|
/// C# definition from Interactable_Resource.cs:
|
||||||
|
/// ```csharp
|
||||||
|
/// public class Interactable_Resource : Interactable
|
||||||
|
/// {
|
||||||
|
/// public int health;
|
||||||
|
/// public int maxHealth;
|
||||||
|
/// public int typeId;
|
||||||
|
/// // ... other fields
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, UnityComponent)]
|
||||||
|
#[unity_class("Interactable_Resource")]
|
||||||
|
pub struct InteractableResource {
|
||||||
|
#[unity_field("maxHealth")]
|
||||||
|
pub max_health: i64,
|
||||||
|
|
||||||
|
#[unity_field("typeId")]
|
||||||
|
pub type_id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🎮 Cursebreaker - Resource Prefab Parser");
|
||||||
|
println!("{}", "=".repeat(70));
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Build GUID resolver for the project
|
||||||
|
let project_path = Path::new("/home/connor/repos/CBAssets");
|
||||||
|
println!("📦 Building GUID resolver for project: {}", project_path.display());
|
||||||
|
|
||||||
|
let resolver = match GuidResolver::from_project(project_path) {
|
||||||
|
Ok(r) => {
|
||||||
|
println!(" ✅ GUID resolver built successfully ({} mappings)", r.len());
|
||||||
|
|
||||||
|
// Debug: Check if we have Interactable_Resource
|
||||||
|
if let Some(class) = r.resolve_class_name("d39ddbf1c2c3d1a4baa070e5e76548bd") {
|
||||||
|
println!(" ✅ Found Interactable_Resource in resolver: {}", class);
|
||||||
|
} else {
|
||||||
|
println!(" ⚠️ Interactable_Resource NOT found in resolver");
|
||||||
|
// Try to find what we did find related to "Interactable"
|
||||||
|
println!(" Searching for similar class names...");
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(r)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" ❌ Failed to build GUID resolver: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let harvestables_dir = Path::new("/home/connor/repos/CBAssets/_GameAssets/Prefabs/Harvestables");
|
||||||
|
|
||||||
|
if !harvestables_dir.exists() {
|
||||||
|
eprintln!("❌ Error: Harvestables directory not found at {}", harvestables_dir.display());
|
||||||
|
return Err("Harvestables directory not found".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("📁 Scanning for harvestable prefabs in:");
|
||||||
|
println!(" {}", harvestables_dir.display());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Find all prefab files
|
||||||
|
let mut prefab_files = Vec::new();
|
||||||
|
for entry in WalkDir::new(harvestables_dir)
|
||||||
|
.follow_links(false)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
{
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|s| s.to_str()) == Some("prefab") {
|
||||||
|
prefab_files.push(path.to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("📄 Found {} prefab file(s)", prefab_files.len());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let mut all_resources = Vec::new();
|
||||||
|
|
||||||
|
// Parse each prefab using the GUID resolver we built
|
||||||
|
for prefab_path in &prefab_files {
|
||||||
|
println!("🔍 Parsing: {}", prefab_path.file_name().unwrap().to_string_lossy());
|
||||||
|
|
||||||
|
// For prefabs, we need to manually parse and check documents
|
||||||
|
// since prefabs don't have an ECS world like scenes do
|
||||||
|
match UnityFile::from_path(prefab_path) {
|
||||||
|
Ok(UnityFile::Prefab(prefab)) => {
|
||||||
|
// Search through YAML documents for Interactable_Resource components
|
||||||
|
let mut found_in_prefab = false;
|
||||||
|
|
||||||
|
for doc in &prefab.documents {
|
||||||
|
// Check if this document is a MonoBehaviour
|
||||||
|
if doc.class_name == "MonoBehaviour" {
|
||||||
|
// Try to extract the m_Script GUID
|
||||||
|
if let Some(m_script) = doc.yaml.get("m_Script").and_then(|v| v.as_mapping()) {
|
||||||
|
if let Some(guid_val) = m_script.get("guid").and_then(|v| v.as_str()) {
|
||||||
|
// Resolve GUID to class name
|
||||||
|
if let Some(ref res) = resolver {
|
||||||
|
if let Some(class_name) = res.resolve_class_name(guid_val) {
|
||||||
|
// Debug: print what we found
|
||||||
|
if prefab_path.file_name().unwrap().to_string_lossy().contains("Copper Ore") {
|
||||||
|
eprintln!("DEBUG: Found class '{}' in Copper Ore prefab", class_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if class_name == "Interactable_Resource" {
|
||||||
|
// Extract fields
|
||||||
|
let type_id = doc.yaml.get("typeId")
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let max_health = doc.yaml.get("maxHealth")
|
||||||
|
.and_then(|v| v.as_i64())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
let prefab_name = prefab_path
|
||||||
|
.file_stem()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
|
||||||
|
all_resources.push((
|
||||||
|
prefab_name.to_string(),
|
||||||
|
type_id,
|
||||||
|
max_health,
|
||||||
|
));
|
||||||
|
|
||||||
|
found_in_prefab = true;
|
||||||
|
}
|
||||||
|
} else if prefab_path.file_name().unwrap().to_string_lossy().contains("Copper Ore") {
|
||||||
|
eprintln!("DEBUG: Could not resolve GUID '{}' in Copper Ore prefab", guid_val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found_in_prefab {
|
||||||
|
println!(" ✅ Found Interactable_Resource");
|
||||||
|
} else {
|
||||||
|
println!(" ⊘ No Interactable_Resource found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
println!(" ⊘ Not a prefab file");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!(" ❌ Parse error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("{}", "=".repeat(70));
|
||||||
|
println!("📊 Summary: Found {} resource(s)", all_resources.len());
|
||||||
|
println!("{}", "=".repeat(70));
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if !all_resources.is_empty() {
|
||||||
|
// Display resources
|
||||||
|
for (name, type_id, max_health) in &all_resources {
|
||||||
|
println!(" 📦 Prefab: \"{}\"", name);
|
||||||
|
println!(" • typeId: {}", type_id);
|
||||||
|
println!(" • maxHealth: {}", max_health);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to output file
|
||||||
|
let output_path = "resource_prefabs_output.txt";
|
||||||
|
let mut output_file = File::create(output_path)?;
|
||||||
|
|
||||||
|
writeln!(output_file, "Cursebreaker Resource Prefabs")?;
|
||||||
|
writeln!(output_file, "{}", "=".repeat(70))?;
|
||||||
|
writeln!(output_file)?;
|
||||||
|
writeln!(output_file, "Total resources found: {}", all_resources.len())?;
|
||||||
|
writeln!(output_file)?;
|
||||||
|
writeln!(output_file, "{}", "-".repeat(70))?;
|
||||||
|
writeln!(output_file)?;
|
||||||
|
|
||||||
|
for (name, type_id, max_health) in &all_resources {
|
||||||
|
writeln!(output_file, "Prefab: {}", name)?;
|
||||||
|
writeln!(output_file, " TypeID: {}", type_id)?;
|
||||||
|
writeln!(output_file, " MaxHealth: {}", max_health)?;
|
||||||
|
writeln!(output_file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(output_file, "{}", "=".repeat(70))?;
|
||||||
|
writeln!(output_file, "End of resource data")?;
|
||||||
|
|
||||||
|
println!("📝 Resource data written to: {}", output_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("{}", "=".repeat(70));
|
||||||
|
println!("✅ Parsing complete!");
|
||||||
|
println!("{}", "=".repeat(70));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
147
cursebreaker-parser/examples/parse_resources.rs
Normal file
147
cursebreaker-parser/examples/parse_resources.rs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
//! Parse Cursebreaker Resources from 10_3.unity Scene
|
||||||
|
//!
|
||||||
|
//! This example demonstrates:
|
||||||
|
//! 1. Parsing the Cursebreaker Unity project
|
||||||
|
//! 2. Finding Interactable_Resource components
|
||||||
|
//! 3. Extracting typeId and transform positions
|
||||||
|
//! 4. Writing resource data to an output file
|
||||||
|
|
||||||
|
use cursebreaker_parser::{UnityComponent, UnityFile};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Write;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Interactable_Resource component from Cursebreaker
|
||||||
|
///
|
||||||
|
/// C# definition from Interactable_Resource.cs:
|
||||||
|
/// ```csharp
|
||||||
|
/// public class Interactable_Resource : Interactable
|
||||||
|
/// {
|
||||||
|
/// public int health;
|
||||||
|
/// public int maxHealth;
|
||||||
|
/// public int typeId;
|
||||||
|
/// // ... other fields
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone, UnityComponent)]
|
||||||
|
#[unity_class("Interactable_Resource")]
|
||||||
|
pub struct InteractableResource {
|
||||||
|
#[unity_field("maxHealth")]
|
||||||
|
pub max_health: i64,
|
||||||
|
|
||||||
|
#[unity_field("typeId")]
|
||||||
|
pub type_id: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
println!("🎮 Cursebreaker - Resource Parser");
|
||||||
|
println!("{}", "=".repeat(70));
|
||||||
|
println!();
|
||||||
|
|
||||||
|
let scene_path = Path::new("/home/connor/repos/CBAssets/_GameAssets/Scenes/Tiles/10_3.unity");
|
||||||
|
|
||||||
|
// Check if scene exists
|
||||||
|
if !scene_path.exists() {
|
||||||
|
eprintln!("❌ Error: Scene not found at {}", scene_path.display());
|
||||||
|
return Err("Scene file not found".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("📁 Parsing scene: {}", scene_path.display());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Parse the scene
|
||||||
|
match UnityFile::from_path(&scene_path) {
|
||||||
|
Ok(UnityFile::Scene(scene)) => {
|
||||||
|
println!("✅ Scene parsed successfully!");
|
||||||
|
println!(" Total entities: {}", scene.entity_map.len());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
// Get views for component types we need
|
||||||
|
let resource_view = scene.world.borrow::<InteractableResource>();
|
||||||
|
let transform_view = scene.world.borrow::<cursebreaker_parser::Transform>();
|
||||||
|
let gameobject_view = scene.world.borrow::<cursebreaker_parser::GameObject>();
|
||||||
|
|
||||||
|
// Find all entities that have Interactable_Resource
|
||||||
|
let mut found_resources = Vec::new();
|
||||||
|
|
||||||
|
for entity in scene.entity_map.values() {
|
||||||
|
if let Some(resource) = resource_view.get(*entity) {
|
||||||
|
let transform = transform_view.get(*entity);
|
||||||
|
let game_object = gameobject_view.get(*entity);
|
||||||
|
|
||||||
|
let name = game_object
|
||||||
|
.and_then(|go| go.name())
|
||||||
|
.unwrap_or("(unnamed)");
|
||||||
|
|
||||||
|
let position = transform
|
||||||
|
.and_then(|t| t.local_position())
|
||||||
|
.map(|p| (p.x, p.y, p.z));
|
||||||
|
|
||||||
|
found_resources.push((name.to_string(), resource.clone(), position));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("🔍 Found {} Interactable_Resource component(s)", found_resources.len());
|
||||||
|
println!();
|
||||||
|
|
||||||
|
if !found_resources.is_empty() {
|
||||||
|
// Display resources in console
|
||||||
|
for (name, resource, position) in &found_resources {
|
||||||
|
println!(" 📦 Resource: \"{}\"", name);
|
||||||
|
println!(" • typeId: {}", resource.type_id);
|
||||||
|
println!(" • maxHealth: {}", resource.max_health);
|
||||||
|
if let Some((x, y, z)) = position {
|
||||||
|
println!(" • Position: ({:.2}, {:.2}, {:.2})", x, y, z);
|
||||||
|
} else {
|
||||||
|
println!(" • Position: (no transform)");
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to output file
|
||||||
|
let output_path = "resources_output.txt";
|
||||||
|
let mut output_file = File::create(output_path)?;
|
||||||
|
|
||||||
|
writeln!(output_file, "Cursebreaker Resources - 10_3.unity Scene")?;
|
||||||
|
writeln!(output_file, "{}", "=".repeat(70))?;
|
||||||
|
writeln!(output_file)?;
|
||||||
|
writeln!(output_file, "Total resources found: {}", found_resources.len())?;
|
||||||
|
writeln!(output_file)?;
|
||||||
|
writeln!(output_file, "{}", "-".repeat(70))?;
|
||||||
|
writeln!(output_file)?;
|
||||||
|
|
||||||
|
for (name, resource, position) in &found_resources {
|
||||||
|
writeln!(output_file, "Resource: {}", name)?;
|
||||||
|
writeln!(output_file, " TypeID: {}", resource.type_id)?;
|
||||||
|
writeln!(output_file, " MaxHealth: {}", resource.max_health)?;
|
||||||
|
if let Some((x, y, z)) = position {
|
||||||
|
writeln!(output_file, " Position: ({:.6}, {:.6}, {:.6})", x, y, z)?;
|
||||||
|
} else {
|
||||||
|
writeln!(output_file, " Position: N/A")?;
|
||||||
|
}
|
||||||
|
writeln!(output_file)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
writeln!(output_file, "{}", "=".repeat(70))?;
|
||||||
|
writeln!(output_file, "End of resource data")?;
|
||||||
|
|
||||||
|
println!("📝 Resource data written to: {}", output_path);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{}", "=".repeat(70));
|
||||||
|
println!("✅ Parsing complete!");
|
||||||
|
println!("{}", "=".repeat(70));
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
eprintln!("❌ Error: File is not a scene");
|
||||||
|
return Err("Not a Unity scene file".into());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("❌ Parse error: {}", e);
|
||||||
|
return Err(Box::new(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
19
cursebreaker-parser/resources_output.txt
Normal file
19
cursebreaker-parser/resources_output.txt
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
Cursebreaker Resources - 10_3.unity Scene
|
||||||
|
======================================================================
|
||||||
|
|
||||||
|
Total resources found: 2
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Resource: HarvestableSpawner_11Redberries
|
||||||
|
TypeID: 11
|
||||||
|
MaxHealth: 0
|
||||||
|
Position: (1769.135864, 32.664658, 150.395081)
|
||||||
|
|
||||||
|
Resource: HarvestableSpawner_38Dandelions
|
||||||
|
TypeID: 38
|
||||||
|
MaxHealth: 0
|
||||||
|
Position: (1746.709717, 44.599632, 299.696503)
|
||||||
|
|
||||||
|
======================================================================
|
||||||
|
End of resource data
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
//! ECS world building from Unity documents
|
//! ECS world building from Unity documents
|
||||||
|
|
||||||
use crate::model::RawDocument;
|
use crate::model::RawDocument;
|
||||||
use crate::parser::GuidResolver;
|
use crate::parser::{GuidResolver, PrefabGuidResolver};
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent,
|
yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent,
|
||||||
RectTransform, Transform, TypeFilter, UnityComponent,
|
PrefabResolver, RectTransform, Transform, TypeFilter, UnityComponent,
|
||||||
};
|
};
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
use sparsey::{Entity, World};
|
use sparsey::{Entity, World};
|
||||||
@@ -13,20 +13,23 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
/// Build a Sparsey ECS World from raw Unity documents
|
/// Build a Sparsey ECS World from raw Unity documents
|
||||||
///
|
///
|
||||||
/// This uses a 3-pass approach:
|
/// This uses a 3.5-pass approach:
|
||||||
/// 1. Create entities for all GameObjects
|
/// 1. Create entities for all GameObjects
|
||||||
/// 2. Attach components (Transform, RectTransform, etc.) to entities
|
/// 2. Attach components (Transform, RectTransform, etc.) to entities
|
||||||
|
/// 2.5. Resolve and instantiate prefab instances (NEW)
|
||||||
/// 3. Resolve Transform hierarchy (parent/children Entity references)
|
/// 3. Resolve Transform hierarchy (parent/children Entity references)
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// - `documents`: Parsed Unity documents to build the world from
|
/// - `documents`: Parsed Unity documents to build the world from
|
||||||
/// - `guid_resolver`: Optional GUID resolver for resolving MonoBehaviour scripts to class names
|
/// - `guid_resolver`: Optional GUID resolver for resolving MonoBehaviour scripts to class names
|
||||||
|
/// - `prefab_guid_resolver`: Optional prefab GUID resolver for automatic prefab instantiation
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// A tuple of (World, FileID → Entity mapping)
|
/// A tuple of (World, FileID → Entity mapping)
|
||||||
pub fn build_world_from_documents(
|
pub fn build_world_from_documents(
|
||||||
documents: Vec<RawDocument>,
|
documents: Vec<RawDocument>,
|
||||||
guid_resolver: Option<&GuidResolver>,
|
guid_resolver: Option<&GuidResolver>,
|
||||||
|
prefab_guid_resolver: Option<&PrefabGuidResolver>,
|
||||||
) -> Result<(World, HashMap<FileID, Entity>)> {
|
) -> Result<(World, HashMap<FileID, Entity>)> {
|
||||||
// Create World builder with registered component types
|
// Create World builder with registered component types
|
||||||
let mut builder = World::builder();
|
let mut builder = World::builder();
|
||||||
@@ -45,18 +48,87 @@ pub fn build_world_from_documents(
|
|||||||
|
|
||||||
let linking_ctx = RefCell::new(LinkingContext::new());
|
let linking_ctx = RefCell::new(LinkingContext::new());
|
||||||
|
|
||||||
// PASS 1: Create entities for all GameObjects
|
// PASS 1: Create entities for all GameObjects and PrefabInstances
|
||||||
for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
|
for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
|
||||||
let entity = spawn_game_object(&mut world, doc)?;
|
let entity = spawn_game_object(&mut world, doc)?;
|
||||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also create entities for PrefabInstances (type 1001)
|
||||||
|
for doc in documents.iter().filter(|d| d.type_id == 1001 || d.class_name == "PrefabInstance") {
|
||||||
|
// Create an entity to represent this prefab instance
|
||||||
|
let entity = world.create(());
|
||||||
|
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||||
|
|
||||||
|
// Parse and attach the PrefabInstanceComponent
|
||||||
|
if let Some(yaml) = doc.as_mapping() {
|
||||||
|
let ctx = ComponentContext {
|
||||||
|
type_id: doc.type_id,
|
||||||
|
file_id: doc.file_id,
|
||||||
|
class_name: &doc.class_name,
|
||||||
|
entity: Some(entity),
|
||||||
|
linking_ctx: Some(&linking_ctx),
|
||||||
|
yaml,
|
||||||
|
guid_resolver,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(prefab_comp) = PrefabInstanceComponent::parse(yaml, &ctx) {
|
||||||
|
world.insert(entity, (prefab_comp,));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PASS 2: Attach components to entities
|
// PASS 2: Attach components to entities
|
||||||
let type_filter = TypeFilter::parse_all();
|
let type_filter = TypeFilter::parse_all();
|
||||||
for doc in documents.iter().filter(|d| d.type_id != 1 && d.class_name != "GameObject") {
|
for doc in documents.iter().filter(|d| {
|
||||||
|
d.type_id != 1 && d.class_name != "GameObject" &&
|
||||||
|
d.type_id != 1001 && d.class_name != "PrefabInstance"
|
||||||
|
}) {
|
||||||
attach_component(&mut world, doc, &linking_ctx, &type_filter, guid_resolver)?;
|
attach_component(&mut world, doc, &linking_ctx, &type_filter, guid_resolver)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PASS 2.5: Resolve and instantiate prefab instances (NEW)
|
||||||
|
if let Some(prefab_resolver_ref) = prefab_guid_resolver {
|
||||||
|
let mut prefab_resolver = PrefabResolver::from_resolvers(guid_resolver, prefab_resolver_ref);
|
||||||
|
|
||||||
|
// Query for entities with PrefabInstanceComponent
|
||||||
|
// We need to collect first to avoid borrowing conflicts
|
||||||
|
let prefab_view = world.borrow::<PrefabInstanceComponent>();
|
||||||
|
let prefab_entities: Vec<_> = linking_ctx
|
||||||
|
.borrow()
|
||||||
|
.entity_map()
|
||||||
|
.values()
|
||||||
|
.filter_map(|&entity| {
|
||||||
|
prefab_view.get(entity).map(|component| (entity, component.clone()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
drop(prefab_view); // Release the borrow
|
||||||
|
|
||||||
|
eprintln!("Pass 2.5: Found {} prefab instance(s) to resolve", prefab_entities.len());
|
||||||
|
|
||||||
|
for (entity, component) in prefab_entities {
|
||||||
|
match prefab_resolver.instantiate_from_component(
|
||||||
|
&component,
|
||||||
|
Some(entity),
|
||||||
|
&mut world,
|
||||||
|
linking_ctx.borrow_mut().entity_map_mut(),
|
||||||
|
) {
|
||||||
|
Ok(spawned) => {
|
||||||
|
eprintln!(" ✅ Spawned {} entities from prefab GUID: {}",
|
||||||
|
spawned.len(), component.prefab_ref.guid);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Soft failure - warn but continue
|
||||||
|
eprintln!(" ⚠️ Warning: Failed to instantiate prefab: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove PrefabInstanceComponent after resolution
|
||||||
|
// This prevents it from being processed again
|
||||||
|
let _ = world.remove::<(PrefabInstanceComponent,)>(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PASS 3: Execute all deferred linking callbacks
|
// PASS 3: Execute all deferred linking callbacks
|
||||||
let entity_map = linking_ctx.into_inner().execute_callbacks(&mut world);
|
let entity_map = linking_ctx.into_inner().execute_callbacks(&mut world);
|
||||||
|
|
||||||
@@ -68,15 +140,18 @@ pub fn build_world_from_documents(
|
|||||||
/// This is similar to `build_world_from_documents` but spawns into an existing
|
/// This is similar to `build_world_from_documents` but spawns into an existing
|
||||||
/// world instead of creating a new one. This is used for prefab instantiation.
|
/// world instead of creating a new one. This is used for prefab instantiation.
|
||||||
///
|
///
|
||||||
/// Uses the same 3-pass approach:
|
/// Uses the same 3.5-pass approach:
|
||||||
/// 1. Create entities for all GameObjects
|
/// 1. Create entities for all GameObjects
|
||||||
/// 2. Attach components (Transform, RectTransform, etc.) to entities
|
/// 2. Attach components (Transform, RectTransform, etc.) to entities
|
||||||
|
/// 2.5. Resolve and instantiate prefab instances (if resolver provided)
|
||||||
/// 3. Resolve Transform hierarchy (parent/children Entity references)
|
/// 3. Resolve Transform hierarchy (parent/children Entity references)
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// - `documents`: Parsed Unity documents to build entities from
|
/// - `documents`: Parsed Unity documents to build entities from
|
||||||
/// - `world`: Existing Sparsey ECS world to spawn entities into
|
/// - `world`: Existing Sparsey ECS world to spawn entities into
|
||||||
/// - `entity_map`: Existing entity map to merge new mappings into
|
/// - `entity_map`: Existing entity map to merge new mappings into
|
||||||
|
/// - `guid_resolver`: Optional script GUID resolver for MonoBehaviour components
|
||||||
|
/// - `prefab_guid_resolver`: Optional prefab GUID resolver for nested prefab instantiation
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// Vec of newly spawned entities
|
/// Vec of newly spawned entities
|
||||||
@@ -84,6 +159,8 @@ pub fn build_world_from_documents_into(
|
|||||||
documents: Vec<RawDocument>,
|
documents: Vec<RawDocument>,
|
||||||
world: &mut World,
|
world: &mut World,
|
||||||
entity_map: &mut HashMap<FileID, Entity>,
|
entity_map: &mut HashMap<FileID, Entity>,
|
||||||
|
guid_resolver: Option<&GuidResolver>,
|
||||||
|
prefab_guid_resolver: Option<&PrefabGuidResolver>,
|
||||||
) -> Result<Vec<Entity>> {
|
) -> Result<Vec<Entity>> {
|
||||||
let linking_ctx = RefCell::new(LinkingContext::new());
|
let linking_ctx = RefCell::new(LinkingContext::new());
|
||||||
|
|
||||||
@@ -93,18 +170,87 @@ pub fn build_world_from_documents_into(
|
|||||||
|
|
||||||
let mut spawned_entities = Vec::new();
|
let mut spawned_entities = Vec::new();
|
||||||
|
|
||||||
// PASS 1: Create entities for all GameObjects
|
// PASS 1: Create entities for all GameObjects and PrefabInstances
|
||||||
for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
|
for doc in documents.iter().filter(|d| d.type_id == 1 || d.class_name == "GameObject") {
|
||||||
let entity = spawn_game_object(world, doc)?;
|
let entity = spawn_game_object(world, doc)?;
|
||||||
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||||
spawned_entities.push(entity);
|
spawned_entities.push(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also create entities for PrefabInstances (type 1001)
|
||||||
|
for doc in documents.iter().filter(|d| d.type_id == 1001 || d.class_name == "PrefabInstance") {
|
||||||
|
// Create an entity to represent this prefab instance
|
||||||
|
let entity = world.create(());
|
||||||
|
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity);
|
||||||
|
spawned_entities.push(entity);
|
||||||
|
|
||||||
|
// Parse and attach the PrefabInstanceComponent
|
||||||
|
if let Some(yaml) = doc.as_mapping() {
|
||||||
|
let ctx = ComponentContext {
|
||||||
|
type_id: doc.type_id,
|
||||||
|
file_id: doc.file_id,
|
||||||
|
class_name: &doc.class_name,
|
||||||
|
entity: Some(entity),
|
||||||
|
linking_ctx: Some(&linking_ctx),
|
||||||
|
yaml,
|
||||||
|
guid_resolver: None, // Nested prefabs use None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(prefab_comp) = PrefabInstanceComponent::parse(yaml, &ctx) {
|
||||||
|
world.insert(entity, (prefab_comp,));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PASS 2: Attach components to entities
|
// PASS 2: Attach components to entities
|
||||||
let type_filter = TypeFilter::parse_all();
|
let type_filter = TypeFilter::parse_all();
|
||||||
for doc in documents.iter().filter(|d| d.type_id != 1 && d.class_name != "GameObject") {
|
for doc in documents.iter().filter(|d| {
|
||||||
// Note: Prefab instantiation doesn't need GUID resolution (uses base prefab's components)
|
d.type_id != 1 && d.class_name != "GameObject" &&
|
||||||
attach_component(world, doc, &linking_ctx, &type_filter, None)?;
|
d.type_id != 1001 && d.class_name != "PrefabInstance"
|
||||||
|
}) {
|
||||||
|
// Use GUID resolver to resolve MonoBehaviour components in prefabs
|
||||||
|
attach_component(world, doc, &linking_ctx, &type_filter, guid_resolver)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PASS 2.5: Resolve and instantiate nested prefab instances
|
||||||
|
if let Some(prefab_resolver_ref) = prefab_guid_resolver {
|
||||||
|
let mut prefab_resolver = PrefabResolver::from_resolvers(guid_resolver, prefab_resolver_ref);
|
||||||
|
|
||||||
|
// Query for entities with PrefabInstanceComponent
|
||||||
|
let prefab_view = world.borrow::<PrefabInstanceComponent>();
|
||||||
|
let prefab_entities: Vec<_> = linking_ctx
|
||||||
|
.borrow()
|
||||||
|
.entity_map()
|
||||||
|
.values()
|
||||||
|
.filter_map(|&entity| {
|
||||||
|
prefab_view.get(entity).map(|component| (entity, component.clone()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
drop(prefab_view); // Release the borrow
|
||||||
|
|
||||||
|
if !prefab_entities.is_empty() {
|
||||||
|
eprintln!("Pass 2.5 (nested): Found {} prefab instance(s) to resolve", prefab_entities.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entity, component) in prefab_entities {
|
||||||
|
match prefab_resolver.instantiate_from_component(
|
||||||
|
&component,
|
||||||
|
Some(entity),
|
||||||
|
world,
|
||||||
|
linking_ctx.borrow_mut().entity_map_mut(),
|
||||||
|
) {
|
||||||
|
Ok(spawned) => {
|
||||||
|
eprintln!(" ✅ Spawned {} entities from nested prefab", spawned.len());
|
||||||
|
spawned_entities.extend(spawned);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" ⚠️ Warning: Failed to instantiate nested prefab: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove PrefabInstanceComponent after resolution
|
||||||
|
let _ = world.remove::<(PrefabInstanceComponent,)>(entity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PASS 3: Execute all deferred linking callbacks
|
// PASS 3: Execute all deferred linking callbacks
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub use error::{Error, Result};
|
|||||||
pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
|
||||||
pub use parser::{
|
pub use parser::{
|
||||||
find_project_root, meta::MetaFile, parse_unity_file, parse_unity_file_filtered, GuidResolver,
|
find_project_root, meta::MetaFile, parse_unity_file, parse_unity_file_filtered, GuidResolver,
|
||||||
|
PrefabGuidResolver,
|
||||||
};
|
};
|
||||||
// TODO: Re-enable once project module is updated
|
// TODO: Re-enable once project module is updated
|
||||||
// pub use project::UnityProject;
|
// pub use project::UnityProject;
|
||||||
|
|||||||
@@ -80,14 +80,17 @@ impl GuidResolver {
|
|||||||
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
||||||
let project_path = project_path.as_ref();
|
let project_path = project_path.as_ref();
|
||||||
|
|
||||||
// Verify this looks like a Unity project
|
// Verify this looks like a Unity project - check for Assets/ or _GameAssets/
|
||||||
let assets_dir = project_path.join("Assets");
|
let assets_dir = if project_path.join("Assets").exists() {
|
||||||
if !assets_dir.exists() {
|
project_path.join("Assets")
|
||||||
|
} else if project_path.join("_GameAssets").exists() {
|
||||||
|
project_path.join("_GameAssets")
|
||||||
|
} else {
|
||||||
return Err(Error::invalid_format(format!(
|
return Err(Error::invalid_format(format!(
|
||||||
"Not a Unity project: missing Assets/ directory at {}",
|
"Not a Unity project: missing Assets/ or _GameAssets/ directory at {}",
|
||||||
project_path.display()
|
project_path.display()
|
||||||
)));
|
)));
|
||||||
}
|
};
|
||||||
|
|
||||||
let mut resolver = Self::new();
|
let mut resolver = Self::new();
|
||||||
|
|
||||||
@@ -305,15 +308,20 @@ fn is_cs_meta_file(path: &Path) -> bool {
|
|||||||
fn extract_class_name(cs_path: &Path) -> Result<Option<String>> {
|
fn extract_class_name(cs_path: &Path) -> Result<Option<String>> {
|
||||||
let content = std::fs::read_to_string(cs_path)?;
|
let content = std::fs::read_to_string(cs_path)?;
|
||||||
|
|
||||||
// Regex to match MonoBehaviour class declarations
|
// Regex to match MonoBehaviour class declarations (direct or indirect inheritance)
|
||||||
// Matches: public class ClassName : MonoBehaviour
|
// Matches: public class ClassName : MonoBehaviour
|
||||||
// Also matches: class ClassName:MonoBehaviour (no space)
|
// Also matches: public class ClassName : SomeBaseClass (which may inherit from MonoBehaviour)
|
||||||
// Captures the class name in group 1
|
// Captures the class name in group 1
|
||||||
|
//
|
||||||
|
// We match any class with inheritance (: BaseClass) because in Unity,
|
||||||
|
// scripts can inherit from MonoBehaviour indirectly through base classes.
|
||||||
|
// The component registration system will filter for actual MonoBehaviours.
|
||||||
let class_regex = Regex::new(
|
let class_regex = Regex::new(
|
||||||
r"(?:public\s+)?class\s+(\w+)\s*:\s*MonoBehaviour"
|
r"(?:public\s+)?class\s+(\w+)\s*:\s*\w+"
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
// Find the first MonoBehaviour class in the file
|
// Find the first class with inheritance in the file
|
||||||
|
// Unity typically has one main class per script file
|
||||||
if let Some(captures) = class_regex.captures(&content) {
|
if let Some(captures) = class_regex.captures(&content) {
|
||||||
if let Some(class_name) = captures.get(1) {
|
if let Some(class_name) = captures.get(1) {
|
||||||
return Ok(Some(class_name.as_str().to_string()));
|
return Ok(Some(class_name.as_str().to_string()));
|
||||||
@@ -359,10 +367,11 @@ pub fn find_project_root(path: impl AsRef<Path>) -> Result<PathBuf> {
|
|||||||
path
|
path
|
||||||
};
|
};
|
||||||
|
|
||||||
// Search upward for Assets/ directory
|
// Search upward for Assets/ or _GameAssets/ directory
|
||||||
loop {
|
loop {
|
||||||
let assets_dir = current.join("Assets");
|
let assets_dir = current.join("Assets");
|
||||||
if assets_dir.exists() && assets_dir.is_dir() {
|
let game_assets_dir = current.join("_GameAssets");
|
||||||
|
if (assets_dir.exists() && assets_dir.is_dir()) || (game_assets_dir.exists() && game_assets_dir.is_dir()) {
|
||||||
return Ok(current.to_path_buf());
|
return Ok(current.to_path_buf());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
pub mod guid_resolver;
|
pub mod guid_resolver;
|
||||||
pub mod meta;
|
pub mod meta;
|
||||||
|
pub mod prefab_guid_resolver;
|
||||||
mod unity_tag;
|
mod unity_tag;
|
||||||
mod yaml;
|
mod yaml;
|
||||||
|
|
||||||
pub use guid_resolver::{find_project_root, GuidResolver};
|
pub use guid_resolver::{find_project_root, GuidResolver};
|
||||||
pub use meta::{get_meta_path, MetaFile};
|
pub use meta::{get_meta_path, MetaFile};
|
||||||
|
pub use prefab_guid_resolver::PrefabGuidResolver;
|
||||||
pub use unity_tag::{parse_unity_tag, UnityTag};
|
pub use unity_tag::{parse_unity_tag, UnityTag};
|
||||||
pub use yaml::split_yaml_documents;
|
pub use yaml::split_yaml_documents;
|
||||||
|
|
||||||
@@ -132,28 +134,48 @@ fn detect_file_type(path: &Path) -> FileType {
|
|||||||
fn parse_scene(path: &Path, content: &str) -> Result<UnityFile> {
|
fn parse_scene(path: &Path, content: &str) -> Result<UnityFile> {
|
||||||
let raw_documents = parse_raw_documents(content)?;
|
let raw_documents = parse_raw_documents(content)?;
|
||||||
|
|
||||||
// Try to find Unity project root and build GUID resolver
|
// Try to find Unity project root and build both GUID resolvers
|
||||||
let guid_resolver = match find_project_root(path) {
|
let (guid_resolver, prefab_guid_resolver) = match find_project_root(path) {
|
||||||
Ok(project_root) => {
|
Ok(project_root) => {
|
||||||
// Build GUID resolver from project
|
eprintln!("📦 Found Unity project root: {}", project_root.display());
|
||||||
match GuidResolver::from_project(&project_root) {
|
|
||||||
Ok(resolver) => Some(resolver),
|
// Build script GUID resolver
|
||||||
|
let guid_res = match GuidResolver::from_project(&project_root) {
|
||||||
|
Ok(resolver) => {
|
||||||
|
eprintln!(" ✅ Script GUID resolver built ({} mappings)", resolver.len());
|
||||||
|
Some(resolver)
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Warning: Failed to build GUID resolver: {}", e);
|
eprintln!(" ⚠️ Warning: Failed to build script GUID resolver: {}", e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Build prefab GUID resolver
|
||||||
|
let prefab_res = match PrefabGuidResolver::from_project(&project_root) {
|
||||||
|
Ok(resolver) => {
|
||||||
|
eprintln!(" ✅ Prefab GUID resolver built ({} mappings)", resolver.len());
|
||||||
|
Some(resolver)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(" ⚠️ Warning: Failed to build prefab GUID resolver: {}", e);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(guid_res, prefab_res)
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Not part of a Unity project, or project root not found
|
// Not part of a Unity project, or project root not found
|
||||||
None
|
(None, None)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build ECS world from documents
|
// Build ECS world from documents with both resolvers
|
||||||
let (world, entity_map) = crate::ecs::build_world_from_documents(
|
let (world, entity_map) = crate::ecs::build_world_from_documents(
|
||||||
raw_documents,
|
raw_documents,
|
||||||
guid_resolver.as_ref(),
|
guid_resolver.as_ref(),
|
||||||
|
prefab_guid_resolver.as_ref(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(UnityFile::Scene(UnityScene::new(
|
Ok(UnityFile::Scene(UnityScene::new(
|
||||||
|
|||||||
314
cursebreaker-parser/src/parser/prefab_guid_resolver.rs
Normal file
314
cursebreaker-parser/src/parser/prefab_guid_resolver.rs
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
//! Unity GUID resolution for Prefab files
|
||||||
|
//!
|
||||||
|
//! This module resolves Unity GUIDs to their corresponding Prefab file paths
|
||||||
|
//! by scanning .prefab.meta files.
|
||||||
|
//!
|
||||||
|
//! # How Unity Prefab GUID Resolution Works
|
||||||
|
//!
|
||||||
|
//! 1. Every prefab has a `.prefab.meta` file with a unique GUID
|
||||||
|
//! 2. PrefabInstance components reference prefabs via GUID in `m_CorrespondingSourceObject`
|
||||||
|
//! 3. We scan `.prefab.meta` files to build a GUID → Prefab Path mapping
|
||||||
|
//! 4. Result: GUID → File Path mapping for automatic prefab instantiation
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use cursebreaker_parser::parser::PrefabGuidResolver;
|
||||||
|
//! use std::path::Path;
|
||||||
|
//!
|
||||||
|
//! // Build resolver from Unity project directory
|
||||||
|
//! let project_path = Path::new("path/to/UnityProject");
|
||||||
|
//! let resolver = PrefabGuidResolver::from_project(project_path)?;
|
||||||
|
//!
|
||||||
|
//! // Resolve a GUID to prefab path
|
||||||
|
//! let guid = "091c537484687e9419460cdcd7038234";
|
||||||
|
//! if let Some(path) = resolver.resolve_path(guid) {
|
||||||
|
//! println!("GUID {} → {}", guid, path.display());
|
||||||
|
//! }
|
||||||
|
//! # Ok::<(), cursebreaker_parser::Error>(())
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use crate::parser::meta::MetaFile;
|
||||||
|
use crate::types::Guid;
|
||||||
|
use crate::{Error, Result};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
|
/// Resolves Unity GUIDs to Prefab file paths
|
||||||
|
///
|
||||||
|
/// This struct builds a mapping from GUID to prefab file path by scanning
|
||||||
|
/// a Unity project's `.prefab.meta` files.
|
||||||
|
///
|
||||||
|
/// Uses a 128-bit `Guid` type for efficient storage and fast comparisons.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PrefabGuidResolver {
|
||||||
|
/// Map from GUID to Prefab file path
|
||||||
|
guid_to_path: HashMap<Guid, PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrefabGuidResolver {
|
||||||
|
/// Create a new empty PrefabGuidResolver
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
guid_to_path: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build a PrefabGuidResolver by scanning a Unity project directory
|
||||||
|
///
|
||||||
|
/// This scans for all `.prefab.meta` files and extracts their GUIDs
|
||||||
|
/// to build a GUID → Prefab Path mapping.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `project_path` - Path to the Unity project root (containing Assets/ folder)
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use cursebreaker_parser::parser::PrefabGuidResolver;
|
||||||
|
/// use std::path::Path;
|
||||||
|
///
|
||||||
|
/// let resolver = PrefabGuidResolver::from_project(Path::new("MyUnityProject"))?;
|
||||||
|
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||||
|
/// ```
|
||||||
|
pub fn from_project(project_path: impl AsRef<Path>) -> Result<Self> {
|
||||||
|
let project_path = project_path.as_ref();
|
||||||
|
|
||||||
|
// Verify this looks like a Unity project - check for Assets/ or _GameAssets/
|
||||||
|
let assets_dir = if project_path.join("Assets").exists() {
|
||||||
|
project_path.join("Assets")
|
||||||
|
} else if project_path.join("_GameAssets").exists() {
|
||||||
|
project_path.join("_GameAssets")
|
||||||
|
} else {
|
||||||
|
return Err(Error::invalid_format(format!(
|
||||||
|
"Not a Unity project: missing Assets/ or _GameAssets/ directory at {}",
|
||||||
|
project_path.display()
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut resolver = Self::new();
|
||||||
|
|
||||||
|
// Walk the Assets directory looking for .prefab.meta files
|
||||||
|
for entry in WalkDir::new(&assets_dir)
|
||||||
|
.follow_links(false)
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
{
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
// Only process .prefab.meta files
|
||||||
|
if !is_prefab_meta_file(path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the .meta file to get the GUID
|
||||||
|
let meta = match MetaFile::from_path(path) {
|
||||||
|
Ok(meta) => meta,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Warning: Failed to parse {}: {}", path.display(), e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the corresponding .prefab file path
|
||||||
|
let prefab_path = path.with_file_name(
|
||||||
|
path.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.and_then(|s| s.strip_suffix(".meta"))
|
||||||
|
.unwrap_or(""),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse the GUID string to Guid type
|
||||||
|
let guid = match Guid::from_hex(meta.guid()) {
|
||||||
|
Ok(g) => g,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!(
|
||||||
|
"Warning: Invalid GUID in {}: {}",
|
||||||
|
path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store the mapping
|
||||||
|
resolver.guid_to_path.insert(guid, prefab_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a GUID to its prefab file path
|
||||||
|
///
|
||||||
|
/// Accepts either a `&Guid` or a string that can be parsed as a GUID.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `guid` - The Unity GUID (e.g., "091c537484687e9419460cdcd7038234" or a `Guid`)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The prefab file path if the GUID is found, otherwise None
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # use cursebreaker_parser::{PrefabGuidResolver, Guid};
|
||||||
|
/// # use std::path::Path;
|
||||||
|
/// # let resolver = PrefabGuidResolver::from_project(Path::new("."))?;
|
||||||
|
/// // Resolve by string
|
||||||
|
/// if let Some(path) = resolver.resolve_path("091c537484687e9419460cdcd7038234") {
|
||||||
|
/// println!("Found prefab: {}", path.display());
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// // Resolve by Guid
|
||||||
|
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234")?;
|
||||||
|
/// if let Some(path) = resolver.resolve_path(&guid) {
|
||||||
|
/// println!("Found prefab: {}", path.display());
|
||||||
|
/// }
|
||||||
|
/// # Ok::<(), cursebreaker_parser::Error>(())
|
||||||
|
/// ```
|
||||||
|
pub fn resolve_path<G: AsGuid>(&self, guid: G) -> Option<&Path> {
|
||||||
|
guid.as_guid()
|
||||||
|
.and_then(|g| self.guid_to_path.get(&g))
|
||||||
|
.map(|p| p.as_path())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of GUID mappings
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.guid_to_path.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the resolver is empty
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.guid_to_path.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a GUID → prefab path mapping manually
|
||||||
|
///
|
||||||
|
/// Useful for testing or adding custom mappings
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use cursebreaker_parser::{PrefabGuidResolver, Guid};
|
||||||
|
/// use std::path::PathBuf;
|
||||||
|
///
|
||||||
|
/// let mut resolver = PrefabGuidResolver::new();
|
||||||
|
/// let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||||
|
/// resolver.insert(guid, PathBuf::from("Assets/Prefabs/Player.prefab"));
|
||||||
|
/// ```
|
||||||
|
pub fn insert(&mut self, guid: Guid, path: PathBuf) {
|
||||||
|
self.guid_to_path.insert(guid, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all resolved GUIDs
|
||||||
|
///
|
||||||
|
/// Returns an iterator over all GUIDs in the resolver.
|
||||||
|
pub fn guids(&self) -> impl Iterator<Item = Guid> + '_ {
|
||||||
|
self.guid_to_path.keys().copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PrefabGuidResolver {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trait for types that can be converted to a GUID for lookup
|
||||||
|
///
|
||||||
|
/// This allows the `resolve_path` method to accept both `&Guid` and `&str`.
|
||||||
|
pub trait AsGuid {
|
||||||
|
/// Convert to a GUID, returning None if conversion fails
|
||||||
|
fn as_guid(&self) -> Option<Guid>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsGuid for Guid {
|
||||||
|
fn as_guid(&self) -> Option<Guid> {
|
||||||
|
Some(*self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsGuid for &Guid {
|
||||||
|
fn as_guid(&self) -> Option<Guid> {
|
||||||
|
Some(**self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsGuid for str {
|
||||||
|
fn as_guid(&self) -> Option<Guid> {
|
||||||
|
Guid::from_hex(self).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsGuid for &str {
|
||||||
|
fn as_guid(&self) -> Option<Guid> {
|
||||||
|
Guid::from_hex(self).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsGuid for String {
|
||||||
|
fn as_guid(&self) -> Option<Guid> {
|
||||||
|
Guid::from_hex(self).ok()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a path points to a Prefab .meta file
|
||||||
|
fn is_prefab_meta_file(path: &Path) -> bool {
|
||||||
|
path.extension()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.map(|ext| ext == "meta")
|
||||||
|
.unwrap_or(false)
|
||||||
|
&& path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|s| s.to_str())
|
||||||
|
.map(|name| name.ends_with(".prefab.meta"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_prefab_meta_file() {
|
||||||
|
assert!(is_prefab_meta_file(Path::new("Player.prefab.meta")));
|
||||||
|
assert!(is_prefab_meta_file(Path::new("Assets/Prefabs/Player.prefab.meta")));
|
||||||
|
assert!(!is_prefab_meta_file(Path::new("Player.prefab")));
|
||||||
|
assert!(!is_prefab_meta_file(Path::new("PlaySFX.cs.meta")));
|
||||||
|
assert!(!is_prefab_meta_file(Path::new("Scene.unity.meta")));
|
||||||
|
assert!(!is_prefab_meta_file(Path::new("texture.png.meta")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_prefab_guid_resolver_manual() {
|
||||||
|
let mut resolver = PrefabGuidResolver::new();
|
||||||
|
assert!(resolver.is_empty());
|
||||||
|
|
||||||
|
let guid = Guid::from_hex("091c537484687e9419460cdcd7038234").unwrap();
|
||||||
|
resolver.insert(guid, PathBuf::from("Assets/Prefabs/Player.prefab"));
|
||||||
|
|
||||||
|
assert_eq!(resolver.len(), 1);
|
||||||
|
|
||||||
|
// Test resolving by string
|
||||||
|
assert_eq!(
|
||||||
|
resolver.resolve_path("091c537484687e9419460cdcd7038234").map(|p| p.to_str().unwrap()),
|
||||||
|
Some("Assets/Prefabs/Player.prefab")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test resolving by Guid
|
||||||
|
assert_eq!(
|
||||||
|
resolver.resolve_path(&guid).map(|p| p.to_str().unwrap()),
|
||||||
|
Some("Assets/Prefabs/Player.prefab")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test nonexistent GUID
|
||||||
|
assert_eq!(
|
||||||
|
resolver.resolve_path("00000000000000000000000000000000"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -277,25 +277,35 @@ impl PrefabInstance {
|
|||||||
/// # Arguments
|
/// # Arguments
|
||||||
/// * `world` - The Sparsey ECS world to spawn entities into
|
/// * `world` - The Sparsey ECS world to spawn entities into
|
||||||
/// * `entity_map` - HashMap to track FileID → Entity mappings
|
/// * `entity_map` - HashMap to track FileID → Entity mappings
|
||||||
|
/// * `guid_resolver` - Optional GUID resolver for MonoBehaviour scripts
|
||||||
|
/// * `prefab_guid_resolver` - Optional prefab GUID resolver for nested prefabs
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// Vec of newly created entities
|
/// Vec of newly created entities
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let entities = instance.spawn_into(&mut scene.world, &mut scene.entity_map)?;
|
/// let entities = instance.spawn_into(&mut scene.world, &mut scene.entity_map, Some(&guid_resolver), None)?;
|
||||||
/// println!("Spawned {} entities", entities.len());
|
/// println!("Spawned {} entities", entities.len());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn spawn_into(
|
pub fn spawn_into(
|
||||||
mut self,
|
mut self,
|
||||||
world: &mut World,
|
world: &mut World,
|
||||||
entity_map: &mut HashMap<FileID, Entity>,
|
entity_map: &mut HashMap<FileID, Entity>,
|
||||||
|
guid_resolver: Option<&crate::parser::GuidResolver>,
|
||||||
|
prefab_guid_resolver: Option<&crate::parser::PrefabGuidResolver>,
|
||||||
) -> Result<Vec<Entity>> {
|
) -> Result<Vec<Entity>> {
|
||||||
// Apply overrides before spawning
|
// Apply overrides before spawning
|
||||||
self.apply_overrides()?;
|
self.apply_overrides()?;
|
||||||
|
|
||||||
// Spawn into existing world using the builder
|
// Spawn into existing world using the builder
|
||||||
crate::ecs::build_world_from_documents_into(self.documents, world, entity_map)
|
crate::ecs::build_world_from_documents_into(
|
||||||
|
self.documents,
|
||||||
|
world,
|
||||||
|
entity_map,
|
||||||
|
guid_resolver,
|
||||||
|
prefab_guid_resolver,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the source prefab path (for debugging)
|
/// Get the source prefab path (for debugging)
|
||||||
@@ -322,6 +332,18 @@ pub struct PrefabInstanceComponent {
|
|||||||
pub modifications: Vec<PrefabModification>,
|
pub modifications: Vec<PrefabModification>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for PrefabInstanceComponent {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
prefab_ref: ExternalRef {
|
||||||
|
guid: String::new(),
|
||||||
|
type_id: 0,
|
||||||
|
},
|
||||||
|
modifications: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl UnityComponent for PrefabInstanceComponent {
|
impl UnityComponent for PrefabInstanceComponent {
|
||||||
fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option<Self> {
|
fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option<Self> {
|
||||||
// Extract m_SourcePrefab (external GUID reference)
|
// Extract m_SourcePrefab (external GUID reference)
|
||||||
@@ -412,7 +434,7 @@ fn parse_single_modification(yaml: &Mapping) -> Option<PrefabModification> {
|
|||||||
/// - Caching loaded prefabs
|
/// - Caching loaded prefabs
|
||||||
/// - Detecting circular prefab references
|
/// - Detecting circular prefab references
|
||||||
/// - Recursively instantiating nested prefabs
|
/// - Recursively instantiating nested prefabs
|
||||||
pub struct PrefabResolver {
|
pub struct PrefabResolver<'a> {
|
||||||
/// Cache of loaded prefabs (GUID → Prefab)
|
/// Cache of loaded prefabs (GUID → Prefab)
|
||||||
prefab_cache: HashMap<String, Arc<UnityPrefab>>,
|
prefab_cache: HashMap<String, Arc<UnityPrefab>>,
|
||||||
|
|
||||||
@@ -421,9 +443,15 @@ pub struct PrefabResolver {
|
|||||||
|
|
||||||
/// Stack of GUIDs currently being instantiated (for cycle detection)
|
/// Stack of GUIDs currently being instantiated (for cycle detection)
|
||||||
instantiation_stack: Vec<String>,
|
instantiation_stack: Vec<String>,
|
||||||
|
|
||||||
|
/// GUID resolver for MonoBehaviour scripts
|
||||||
|
guid_resolver: Option<&'a crate::parser::GuidResolver>,
|
||||||
|
|
||||||
|
/// Prefab GUID resolver for nested prefabs
|
||||||
|
prefab_guid_resolver: Option<&'a crate::parser::PrefabGuidResolver>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PrefabResolver {
|
impl<'a> PrefabResolver<'a> {
|
||||||
/// Create a new PrefabResolver
|
/// Create a new PrefabResolver
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -433,9 +461,116 @@ impl PrefabResolver {
|
|||||||
prefab_cache: HashMap::new(),
|
prefab_cache: HashMap::new(),
|
||||||
guid_to_path,
|
guid_to_path,
|
||||||
instantiation_stack: Vec::new(),
|
instantiation_stack: Vec::new(),
|
||||||
|
guid_resolver: None,
|
||||||
|
prefab_guid_resolver: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a PrefabResolver from resolvers
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `guid_resolver` - Script GUID resolver for MonoBehaviour components
|
||||||
|
/// * `prefab_guid_resolver` - Prefab GUID resolver for prefab file paths
|
||||||
|
pub fn from_resolvers(
|
||||||
|
guid_resolver: Option<&'a crate::parser::GuidResolver>,
|
||||||
|
prefab_guid_resolver: &'a crate::parser::PrefabGuidResolver,
|
||||||
|
) -> Self {
|
||||||
|
// Convert Guid → PathBuf mapping to String → PathBuf mapping
|
||||||
|
let guid_to_path = prefab_guid_resolver
|
||||||
|
.guids()
|
||||||
|
.filter_map(|guid| {
|
||||||
|
let path = prefab_guid_resolver.resolve_path(&guid)?;
|
||||||
|
Some((guid.to_hex(), path.to_path_buf()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
prefab_cache: HashMap::new(),
|
||||||
|
guid_to_path,
|
||||||
|
instantiation_stack: Vec::new(),
|
||||||
|
guid_resolver,
|
||||||
|
prefab_guid_resolver: Some(prefab_guid_resolver),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instantiate a prefab from a PrefabInstanceComponent
|
||||||
|
///
|
||||||
|
/// This is the main entry point for automatic prefab instantiation during
|
||||||
|
/// scene parsing. It:
|
||||||
|
/// 1. Loads the prefab by GUID
|
||||||
|
/// 2. Creates a PrefabInstance
|
||||||
|
/// 3. Applies modifications from the component
|
||||||
|
/// 4. Recursively instantiates nested prefabs
|
||||||
|
/// 5. Links the prefab root to the parent entity
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `component` - The PrefabInstanceComponent from the scene
|
||||||
|
/// * `parent_entity` - The GameObject entity that contains this PrefabInstance component
|
||||||
|
/// * `world` - The ECS world to spawn entities into
|
||||||
|
/// * `entity_map` - Entity mapping to update
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Vec of spawned entities (including the root and all children)
|
||||||
|
pub fn instantiate_from_component(
|
||||||
|
&mut self,
|
||||||
|
component: &PrefabInstanceComponent,
|
||||||
|
parent_entity: Option<Entity>,
|
||||||
|
world: &mut World,
|
||||||
|
entity_map: &mut HashMap<FileID, Entity>,
|
||||||
|
) -> Result<Vec<Entity>> {
|
||||||
|
// 1. Extract GUID from component.prefab_ref
|
||||||
|
let guid = &component.prefab_ref.guid;
|
||||||
|
|
||||||
|
// 2. Load prefab via load_prefab()
|
||||||
|
let prefab = self.load_prefab(guid)?;
|
||||||
|
|
||||||
|
// 3. Create PrefabInstance
|
||||||
|
let mut instance = prefab.instantiate();
|
||||||
|
|
||||||
|
// 4. Apply component.modifications using override_value()
|
||||||
|
for modification in &component.modifications {
|
||||||
|
instance.override_value(
|
||||||
|
modification.target_file_id,
|
||||||
|
&modification.property_path,
|
||||||
|
modification.value.clone(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Spawn the instance
|
||||||
|
// Note: We pass None for prefab_guid_resolver to prevent infinite recursion
|
||||||
|
// Nested prefabs should be handled explicitly if needed
|
||||||
|
let spawned = instance.spawn_into(world, entity_map, self.guid_resolver, None)?;
|
||||||
|
|
||||||
|
// 6. Link spawned root to parent_entity (if provided)
|
||||||
|
if let Some(parent) = parent_entity {
|
||||||
|
if let Some(&root_entity) = spawned.first() {
|
||||||
|
// Find the Transform component on the root and link to parent
|
||||||
|
// Note: This is a simplified version. Full implementation would:
|
||||||
|
// - Find the actual root GameObject Transform
|
||||||
|
// - Set m_Father to parent entity
|
||||||
|
// - Update parent's m_Children array
|
||||||
|
//
|
||||||
|
// For now, we'll let the deferred linking system handle this
|
||||||
|
// via the normal Transform hierarchy resolution in Pass 3
|
||||||
|
|
||||||
|
// Try to get Transform components for both parent and child
|
||||||
|
let mut transforms = world.borrow_mut::<crate::types::Transform>();
|
||||||
|
if let Some(_child_transform) = transforms.get_mut(root_entity) {
|
||||||
|
// Store the parent FileID for deferred linking
|
||||||
|
// This will be picked up by Pass 3's hierarchy resolution
|
||||||
|
if let Some(_parent_transform) = transforms.get(parent) {
|
||||||
|
// The parent linking will be handled by the deferred linking system
|
||||||
|
// We just need to ensure the entities are in the world
|
||||||
|
drop(transforms); // Release the borrow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Return spawned entities
|
||||||
|
Ok(spawned)
|
||||||
|
}
|
||||||
|
|
||||||
/// Recursively instantiate a prefab and its nested prefabs
|
/// Recursively instantiate a prefab and its nested prefabs
|
||||||
///
|
///
|
||||||
/// This handles:
|
/// This handles:
|
||||||
@@ -498,7 +633,8 @@ impl PrefabResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Spawn this prefab's entities
|
// Spawn this prefab's entities
|
||||||
let spawned = instance.spawn_into(world, entity_map)?;
|
// Note: Pass None for prefab_guid_resolver since we handle nesting manually
|
||||||
|
let spawned = instance.spawn_into(world, entity_map, self.guid_resolver, None)?;
|
||||||
|
|
||||||
// Pop from stack
|
// Pop from stack
|
||||||
self.instantiation_stack.pop();
|
self.instantiation_stack.pop();
|
||||||
|
|||||||
693
resource_prefabs_output.txt
Normal file
693
resource_prefabs_output.txt
Normal file
@@ -0,0 +1,693 @@
|
|||||||
|
Cursebreaker Resource Prefabs
|
||||||
|
======================================================================
|
||||||
|
|
||||||
|
Total resources found: 171
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_73Medicinal Herbs
|
||||||
|
TypeID: 73
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_65Radishes
|
||||||
|
TypeID: 65
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_22Sandstone Grouper
|
||||||
|
TypeID: 22
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_14Mageflower
|
||||||
|
TypeID: 14
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_108mountain Ore3
|
||||||
|
TypeID: 108
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_120Swamp Brambles
|
||||||
|
TypeID: 120
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_20Caveshroom
|
||||||
|
TypeID: 20
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_30HeartbellVine
|
||||||
|
TypeID: 30
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_76Speed herb
|
||||||
|
TypeID: 76
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_22SandstoneGrouper
|
||||||
|
TypeID: 22
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_64Carrot_Field
|
||||||
|
TypeID: 64
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_26Betta Iotachi
|
||||||
|
TypeID: 26
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_85Magic res ampoule crafting mat
|
||||||
|
TypeID: 85
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_15Blueberries
|
||||||
|
TypeID: 15
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_58Radiant Sunflower
|
||||||
|
TypeID: 58
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_76Waspheart Iris
|
||||||
|
TypeID: 76
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_30Heartbell Vine
|
||||||
|
TypeID: 30
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_61Eggplants
|
||||||
|
TypeID: 61
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_8Deadwood
|
||||||
|
TypeID: 0
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_70Cabbage
|
||||||
|
TypeID: 70
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_19Coal
|
||||||
|
TypeID: 19
|
||||||
|
MaxHealth: 20
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_12PebbleSturgeon
|
||||||
|
TypeID: 12
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_6Oak
|
||||||
|
TypeID: 6
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_2Copper Ore
|
||||||
|
TypeID: 2
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_111Deep Earth Moss
|
||||||
|
TypeID: 111
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_49Dreamylion
|
||||||
|
TypeID: 49
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_71Nest
|
||||||
|
TypeID: 71
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_19Coal
|
||||||
|
TypeID: 19
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_21CharcoalSnapper
|
||||||
|
TypeID: 21
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_12Pebble Sturgeon
|
||||||
|
TypeID: 12
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_25ObsidianWalleye
|
||||||
|
TypeID: 25
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_65Radish_Field
|
||||||
|
TypeID: 65
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_83Phys res ampoule crafting mat
|
||||||
|
TypeID: 83
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_78Firestick flower
|
||||||
|
TypeID: 78
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_13ArcaneSeedTree
|
||||||
|
TypeID: 13
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_116Tree
|
||||||
|
TypeID: 116
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_6Oak
|
||||||
|
TypeID: 6
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_37Ancients' Tears
|
||||||
|
TypeID: 37
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_63Corn
|
||||||
|
TypeID: 63
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_44Voidlight Tendril
|
||||||
|
TypeID: 44
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_5TitaniumOre
|
||||||
|
TypeID: 5
|
||||||
|
MaxHealth: 20
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_55Poppy of the Fallen
|
||||||
|
TypeID: 55
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_102mountain fish3
|
||||||
|
TypeID: 102
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_15Blueberries
|
||||||
|
TypeID: 15
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_41Falls Hydrangea
|
||||||
|
TypeID: 41
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_26BettaIotachi
|
||||||
|
TypeID: 26
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_36Noblerose
|
||||||
|
TypeID: 36
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_58Sunflower
|
||||||
|
TypeID: 58
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_45Autumnleaf Lily
|
||||||
|
TypeID: 45
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_32Lakesberry
|
||||||
|
TypeID: 32
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_106Imberite Ore
|
||||||
|
TypeID: 106
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_100mountain fish1
|
||||||
|
TypeID: 100
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_27Rock Lobster
|
||||||
|
TypeID: 27
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_9Wild Goopy
|
||||||
|
TypeID: 9
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_68Epic ore
|
||||||
|
TypeID: 68
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_113Soulgrowth Mushrooms
|
||||||
|
TypeID: 113
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_28Horsetail Reed Bass
|
||||||
|
TypeID: 28
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_8Deadwood tree
|
||||||
|
TypeID: 8
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_77Glowing Willow
|
||||||
|
TypeID: 77
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_33Silvermirror
|
||||||
|
TypeID: 33
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_42Wight's Pearls
|
||||||
|
TypeID: 42
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_16Spellbound Oak
|
||||||
|
TypeID: 16
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_25Obsidian Walleye
|
||||||
|
TypeID: 25
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_50Haniflower
|
||||||
|
TypeID: 50
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_2CopperOre
|
||||||
|
TypeID: 2
|
||||||
|
MaxHealth: 20
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_64Carrots
|
||||||
|
TypeID: 64
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_32Lakesberry
|
||||||
|
TypeID: 32
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_115Malicious Sporecap
|
||||||
|
TypeID: 115
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_88Saltpeter Mine
|
||||||
|
TypeID: 88
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_23FuchsiaPerch
|
||||||
|
TypeID: 23
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_54GoldenSunflower
|
||||||
|
TypeID: 54
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_58Sunflower
|
||||||
|
TypeID: 58
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_117Ore
|
||||||
|
TypeID: 117
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_1Spruce
|
||||||
|
TypeID: 1
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_79Minigame tree
|
||||||
|
TypeID: 79
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_7Evark tree
|
||||||
|
TypeID: 7
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_81Puppet Flower
|
||||||
|
TypeID: 81
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_6Oak tree
|
||||||
|
TypeID: 6
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_14Mageflower
|
||||||
|
TypeID: 14
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_7Evark
|
||||||
|
TypeID: 0
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_86Generic mining
|
||||||
|
TypeID: 86
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_85Darkmire Damp Root
|
||||||
|
TypeID: 85
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_110Crystalvein Rose
|
||||||
|
TypeID: 110
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_4Imberite ore
|
||||||
|
TypeID: 4
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_16SpellboundOak
|
||||||
|
TypeID: 16
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_59Evening's Cup
|
||||||
|
TypeID: 59
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_38Dandelions
|
||||||
|
TypeID: 38
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_101mountain fish2
|
||||||
|
TypeID: 101
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_115MaliciousSporecap
|
||||||
|
TypeID: 115
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_43Skyfold Flower
|
||||||
|
TypeID: 43
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_67Cotton
|
||||||
|
TypeID: 67
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_84Ranged res ampoule crafting mat
|
||||||
|
TypeID: 84
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_23Fuchsia Perch
|
||||||
|
TypeID: 23
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_84Marshland Feather Reed
|
||||||
|
TypeID: 84
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_24GoblinShad
|
||||||
|
TypeID: 24
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_109mountain herb1
|
||||||
|
TypeID: 109
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_35Red Nymph
|
||||||
|
TypeID: 35
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_80Sunburst Marlin
|
||||||
|
TypeID: 80
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_74Soothing Mallow
|
||||||
|
TypeID: 74
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_60Golden Tulip
|
||||||
|
TypeID: 60
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_114ColdwaterLily
|
||||||
|
TypeID: 114
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_24Goblin Shad
|
||||||
|
TypeID: 24
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_46Cloud Yarrow
|
||||||
|
TypeID: 46
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_113SoulgrowthMushroom
|
||||||
|
TypeID: 113
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_104mountain tree2
|
||||||
|
TypeID: 104
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_62Chili_Pepper_Field
|
||||||
|
TypeID: 62
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_67Cotton
|
||||||
|
TypeID: 67
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_111mountain herb3
|
||||||
|
TypeID: 111
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_18Dust
|
||||||
|
TypeID: 18
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_18Dust
|
||||||
|
TypeID: 18
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_1Spruce
|
||||||
|
TypeID: 1
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_73Sweetgrass
|
||||||
|
TypeID: 73
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_35RedNymph
|
||||||
|
TypeID: 35
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_72MinigameOre
|
||||||
|
TypeID: 72
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_67Northwind Cotton
|
||||||
|
TypeID: 67
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_57Queen's Lily
|
||||||
|
TypeID: 57
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_103mountain tree1
|
||||||
|
TypeID: 103
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_3IronOre
|
||||||
|
TypeID: 3
|
||||||
|
MaxHealth: 20
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_87Generic alchemy
|
||||||
|
TypeID: 87
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_31Bloodpetal
|
||||||
|
TypeID: 31
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_1Spruce tree
|
||||||
|
TypeID: 1
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_40Night's Bloom
|
||||||
|
TypeID: 40
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_28HorsetailReedBass
|
||||||
|
TypeID: 28
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_11Redberries
|
||||||
|
TypeID: 11
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_20Cavern Puffball
|
||||||
|
TypeID: 20
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_37Ancient's Tears
|
||||||
|
TypeID: 37
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_107mountain Ore2
|
||||||
|
TypeID: 107
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_27RockLobster
|
||||||
|
TypeID: 27
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_11RedberryBush
|
||||||
|
TypeID: 11
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_62Chili Peppers
|
||||||
|
TypeID: 62
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_106mountain Ore1
|
||||||
|
TypeID: 106
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_56Deepforest Daisy
|
||||||
|
TypeID: 56
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_105mountain tree3
|
||||||
|
TypeID: 105
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_53Drakefire Root
|
||||||
|
TypeID: 53
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_66Pumpkins
|
||||||
|
TypeID: 66
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_34SorcerersWeed
|
||||||
|
TypeID: 34
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_112Rye
|
||||||
|
TypeID: 112
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_3Iron ore
|
||||||
|
TypeID: 3
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_66Pumpkin_Patch
|
||||||
|
TypeID: 66
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_73MedicinalHerbs
|
||||||
|
TypeID: 73
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_33Silvermirror Iris
|
||||||
|
TypeID: 33
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_69Cabbage
|
||||||
|
TypeID: 69
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_13Arcane Everbloom
|
||||||
|
TypeID: 13
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_61Eggplant_Field
|
||||||
|
TypeID: 61
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_48Sorrowleaf
|
||||||
|
TypeID: 48
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_63Corn_Field
|
||||||
|
TypeID: 63
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_39Dawnflame Ivy
|
||||||
|
TypeID: 39
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_54Golden Sunflower
|
||||||
|
TypeID: 54
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_37AncientsTears
|
||||||
|
TypeID: 37
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_21Charcoal Snapper
|
||||||
|
TypeID: 21
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_82Spider Egg
|
||||||
|
TypeID: 82
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_34Sorcerer's Weed
|
||||||
|
TypeID: 34
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_75Aurora Trout
|
||||||
|
TypeID: 75
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_47Spellspore Cap
|
||||||
|
TypeID: 47
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_109Skyfall Orchid
|
||||||
|
TypeID: 109
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_52Shyflower Orchid
|
||||||
|
TypeID: 52
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_110mountain herb2
|
||||||
|
TypeID: 110
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_38Dandelions
|
||||||
|
TypeID: 38
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_10Barley
|
||||||
|
TypeID: 10
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_51Royal Daisy
|
||||||
|
TypeID: 51
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_20CaveShroom
|
||||||
|
TypeID: 20
|
||||||
|
MaxHealth: 20
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_74Sven Herb 2
|
||||||
|
TypeID: 74
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_69Potatoes
|
||||||
|
TypeID: 69
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_114Coldwater Lily
|
||||||
|
TypeID: 114
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_36Noblerose
|
||||||
|
TypeID: 36
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_10Barley_Field
|
||||||
|
TypeID: 10
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_31Bloodpetal Rose
|
||||||
|
TypeID: 31
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_9WildGoopy
|
||||||
|
TypeID: 9
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_83Gravelstem Whiteflower
|
||||||
|
TypeID: 83
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
Prefab: Prefab_Resource_41FallsHydrangea
|
||||||
|
TypeID: 41
|
||||||
|
MaxHealth: 5
|
||||||
|
|
||||||
|
Prefab: HarvestableSpawner_50Haniflower
|
||||||
|
TypeID: 50
|
||||||
|
MaxHealth: 0
|
||||||
|
|
||||||
|
======================================================================
|
||||||
|
End of resource data
|
||||||
Reference in New Issue
Block a user