prefab instanciation

This commit is contained in:
2026-01-03 12:06:47 +00:00
parent cd35339151
commit 0552b4dff0
12 changed files with 1743 additions and 526 deletions

View File

@@ -12,6 +12,9 @@
"Bash(ls:*)",
"Bash(find:*)",
"Bash(grep:*)"
],
"additionalDirectories": [
"/home/connor/repos/CBAssets/"
]
}
}

View File

@@ -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

View 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(())
}

View 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(())
}

View 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

View File

@@ -1,10 +1,10 @@
//! ECS world building from Unity documents
use crate::model::RawDocument;
use crate::parser::GuidResolver;
use crate::parser::{GuidResolver, PrefabGuidResolver};
use crate::types::{
yaml_helpers, ComponentContext, FileID, GameObject, LinkingContext, PrefabInstanceComponent,
RectTransform, Transform, TypeFilter, UnityComponent,
PrefabResolver, RectTransform, Transform, TypeFilter, UnityComponent,
};
use crate::{Error, Result};
use sparsey::{Entity, World};
@@ -13,20 +13,23 @@ use std::collections::HashMap;
/// 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
/// 2. Attach components (Transform, RectTransform, etc.) to entities
/// 2.5. Resolve and instantiate prefab instances (NEW)
/// 3. Resolve Transform hierarchy (parent/children Entity references)
///
/// # Arguments
/// - `documents`: Parsed Unity documents to build the world from
/// - `guid_resolver`: Optional GUID resolver for resolving MonoBehaviour scripts to class names
/// - `prefab_guid_resolver`: Optional prefab GUID resolver for automatic prefab instantiation
///
/// # Returns
/// A tuple of (World, FileID → Entity mapping)
pub fn build_world_from_documents(
documents: Vec<RawDocument>,
guid_resolver: Option<&GuidResolver>,
prefab_guid_resolver: Option<&PrefabGuidResolver>,
) -> Result<(World, HashMap<FileID, Entity>)> {
// Create World builder with registered component types
let mut builder = World::builder();
@@ -45,18 +48,87 @@ pub fn build_world_from_documents(
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") {
let entity = spawn_game_object(&mut world, doc)?;
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
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)?;
}
// 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
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
/// 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
/// 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)
///
/// # Arguments
/// - `documents`: Parsed Unity documents to build entities from
/// - `world`: Existing Sparsey ECS world to spawn entities 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
/// Vec of newly spawned entities
@@ -84,6 +159,8 @@ pub fn build_world_from_documents_into(
documents: Vec<RawDocument>,
world: &mut World,
entity_map: &mut HashMap<FileID, Entity>,
guid_resolver: Option<&GuidResolver>,
prefab_guid_resolver: Option<&PrefabGuidResolver>,
) -> Result<Vec<Entity>> {
let linking_ctx = RefCell::new(LinkingContext::new());
@@ -93,18 +170,87 @@ pub fn build_world_from_documents_into(
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") {
let entity = spawn_game_object(world, doc)?;
linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, 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
let type_filter = TypeFilter::parse_all();
for doc in documents.iter().filter(|d| d.type_id != 1 && d.class_name != "GameObject") {
// Note: Prefab instantiation doesn't need GUID resolution (uses base prefab's components)
attach_component(world, doc, &linking_ctx, &type_filter, None)?;
for doc in documents.iter().filter(|d| {
d.type_id != 1 && d.class_name != "GameObject" &&
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

View File

@@ -40,6 +40,7 @@ pub use error::{Error, Result};
pub use model::{RawDocument, UnityAsset, UnityFile, UnityPrefab, UnityScene};
pub use parser::{
find_project_root, meta::MetaFile, parse_unity_file, parse_unity_file_filtered, GuidResolver,
PrefabGuidResolver,
};
// TODO: Re-enable once project module is updated
// pub use project::UnityProject;

View File

@@ -80,14 +80,17 @@ impl GuidResolver {
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
let assets_dir = project_path.join("Assets");
if !assets_dir.exists() {
// 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/ directory at {}",
"Not a Unity project: missing Assets/ or _GameAssets/ directory at {}",
project_path.display()
)));
}
};
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>> {
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
// 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
//
// 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(
r"(?:public\s+)?class\s+(\w+)\s*:\s*MonoBehaviour"
r"(?:public\s+)?class\s+(\w+)\s*:\s*\w+"
).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(class_name) = captures.get(1) {
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
};
// Search upward for Assets/ directory
// Search upward for Assets/ or _GameAssets/ directory
loop {
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());
}

View File

@@ -2,11 +2,13 @@
pub mod guid_resolver;
pub mod meta;
pub mod prefab_guid_resolver;
mod unity_tag;
mod yaml;
pub use guid_resolver::{find_project_root, GuidResolver};
pub use meta::{get_meta_path, MetaFile};
pub use prefab_guid_resolver::PrefabGuidResolver;
pub use unity_tag::{parse_unity_tag, UnityTag};
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> {
let raw_documents = parse_raw_documents(content)?;
// Try to find Unity project root and build GUID resolver
let guid_resolver = match find_project_root(path) {
// Try to find Unity project root and build both GUID resolvers
let (guid_resolver, prefab_guid_resolver) = match find_project_root(path) {
Ok(project_root) => {
// Build GUID resolver from project
match GuidResolver::from_project(&project_root) {
Ok(resolver) => Some(resolver),
eprintln!("📦 Found Unity project root: {}", project_root.display());
// Build script GUID resolver
let guid_res = match GuidResolver::from_project(&project_root) {
Ok(resolver) => {
eprintln!(" ✅ Script GUID resolver built ({} mappings)", resolver.len());
Some(resolver)
}
Err(e) => {
eprintln!("Warning: Failed to build GUID resolver: {}", e);
None
}
}
}
Err(_) => {
// Not part of a Unity project, or project root not found
eprintln!(" ⚠️ Warning: Failed to build script GUID resolver: {}", e);
None
}
};
// Build ECS world from documents
// 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(_) => {
// Not part of a Unity project, or project root not found
(None, None)
}
};
// Build ECS world from documents with both resolvers
let (world, entity_map) = crate::ecs::build_world_from_documents(
raw_documents,
guid_resolver.as_ref(),
prefab_guid_resolver.as_ref(),
)?;
Ok(UnityFile::Scene(UnityScene::new(

View 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
);
}
}

View File

@@ -277,25 +277,35 @@ impl PrefabInstance {
/// # Arguments
/// * `world` - The Sparsey ECS world to spawn entities into
/// * `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
/// Vec of newly created entities
///
/// # Example
/// ```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());
/// ```
pub fn spawn_into(
mut self,
world: &mut World,
entity_map: &mut HashMap<FileID, Entity>,
guid_resolver: Option<&crate::parser::GuidResolver>,
prefab_guid_resolver: Option<&crate::parser::PrefabGuidResolver>,
) -> Result<Vec<Entity>> {
// Apply overrides before spawning
self.apply_overrides()?;
// 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)
@@ -322,6 +332,18 @@ pub struct PrefabInstanceComponent {
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 {
fn parse(yaml: &Mapping, _ctx: &ComponentContext) -> Option<Self> {
// Extract m_SourcePrefab (external GUID reference)
@@ -412,7 +434,7 @@ fn parse_single_modification(yaml: &Mapping) -> Option<PrefabModification> {
/// - Caching loaded prefabs
/// - Detecting circular prefab references
/// - Recursively instantiating nested prefabs
pub struct PrefabResolver {
pub struct PrefabResolver<'a> {
/// Cache of loaded prefabs (GUID → Prefab)
prefab_cache: HashMap<String, Arc<UnityPrefab>>,
@@ -421,9 +443,15 @@ pub struct PrefabResolver {
/// Stack of GUIDs currently being instantiated (for cycle detection)
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
///
/// # Arguments
@@ -433,9 +461,116 @@ impl PrefabResolver {
prefab_cache: HashMap::new(),
guid_to_path,
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
///
/// This handles:
@@ -498,7 +633,8 @@ impl PrefabResolver {
}
// 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
self.instantiation_stack.pop();

693
resource_prefabs_output.txt Normal file
View 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