unity_pack/
asset.rs

1//! Asset types for Unity packages
2
3use crate::{Error, ImporterType, MetaFile, Result, UnityGuid};
4use std::fs;
5use std::path::Path;
6
7/// Type of asset content
8#[derive(Debug, Clone)]
9pub enum AssetType {
10    /// Raw binary data (heightmaps, custom formats)
11    Binary(Vec<u8>),
12    /// Text content (JSON, YAML, scripts)
13    Text(String),
14    /// Folder (no content, just metadata)
15    Folder,
16}
17
18/// An asset to be included in a Unity package
19#[derive(Debug, Clone)]
20pub struct Asset {
21    /// Path where the asset will be placed in Unity (e.g., "Assets/Terrain/heightmap.raw")
22    pub path: String,
23    /// The asset's GUID
24    pub guid: UnityGuid,
25    /// The asset content
26    pub content: AssetType,
27    /// Import settings
28    pub importer_type: ImporterType,
29}
30
31impl Asset {
32    /// Create a new binary asset (raw files, images, etc.)
33    pub fn binary(path: impl Into<String>, data: Vec<u8>) -> Self {
34        let path = path.into();
35        let ext = path.rsplit('.').next().unwrap_or("");
36        let importer_type = ImporterType::from_extension(ext);
37
38        Self {
39            guid: UnityGuid::from_path(&path),
40            path,
41            content: AssetType::Binary(data),
42            importer_type,
43        }
44    }
45
46    /// Create a new text asset (JSON, scripts, etc.)
47    pub fn text(path: impl Into<String>, content: impl Into<String>) -> Self {
48        let path = path.into();
49        let ext = path.rsplit('.').next().unwrap_or("");
50        let importer_type = ImporterType::from_extension(ext);
51
52        Self {
53            guid: UnityGuid::from_path(&path),
54            path,
55            content: AssetType::Text(content.into()),
56            importer_type,
57        }
58    }
59
60    /// Create a folder asset
61    pub fn folder(path: impl Into<String>) -> Self {
62        let path = path.into();
63
64        Self {
65            guid: UnityGuid::from_path(&path),
66            path,
67            content: AssetType::Folder,
68            importer_type: ImporterType::Folder,
69        }
70    }
71
72    /// Set a custom GUID (for reproducible builds or specific requirements)
73    pub fn with_guid(mut self, guid: UnityGuid) -> Self {
74        self.guid = guid;
75        self
76    }
77
78    /// Set a random GUID instead of path-derived one
79    pub fn with_random_guid(mut self) -> Self {
80        self.guid = UnityGuid::new();
81        self
82    }
83
84    /// Override the importer type
85    pub fn with_importer(mut self, importer: ImporterType) -> Self {
86        self.importer_type = importer;
87        self
88    }
89
90    /// Generate the .meta file for this asset
91    pub fn meta_file(&self) -> MetaFile {
92        if matches!(self.content, AssetType::Folder) {
93            MetaFile::folder(self.guid)
94        } else {
95            MetaFile::new(self.guid, self.importer_type)
96        }
97    }
98
99    /// Get the asset content as bytes
100    pub fn content_bytes(&self) -> Option<Vec<u8>> {
101        match &self.content {
102            AssetType::Binary(data) => Some(data.clone()),
103            AssetType::Text(text) => Some(text.as_bytes().to_vec()),
104            AssetType::Folder => None,
105        }
106    }
107
108    /// Check if this is a folder
109    pub fn is_folder(&self) -> bool {
110        matches!(self.content, AssetType::Folder)
111    }
112
113    /// Create a binary asset from a byte slice (convenience for &[u8])
114    pub fn binary_from_slice(path: impl Into<String>, data: &[u8]) -> Self {
115        Self::binary(path, data.to_vec())
116    }
117
118    /// Create an asset by reading a file from disk
119    ///
120    /// The Unity path is the destination path in the Unity project.
121    /// The file path is the source file on disk to read.
122    ///
123    /// Automatically detects whether content is text or binary based on extension.
124    pub fn from_file(unity_path: impl Into<String>, file_path: impl AsRef<Path>) -> Result<Self> {
125        let unity_path = unity_path.into();
126        let file_path = file_path.as_ref();
127        let data = fs::read(file_path)?;
128
129        let ext = unity_path.rsplit('.').next().unwrap_or("");
130
131        // Detect if file is likely text based on extension
132        let is_text = matches!(
133            ext.to_lowercase().as_str(),
134            "json" | "txt" | "cs" | "js" | "xml" | "yaml" | "yml" | "md" | "shader" | "cginc"
135        );
136
137        if is_text {
138            match String::from_utf8(data) {
139                Ok(content) => Ok(Self::text(unity_path, content)),
140                Err(e) => Err(Error::Serialization(format!(
141                    "File appears to be text but is not valid UTF-8: {}",
142                    e
143                ))),
144            }
145        } else {
146            Ok(Self::binary(unity_path, data))
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_binary_asset() {
157        let asset = Asset::binary("Assets/Data/test.raw", vec![1, 2, 3, 4]);
158        assert_eq!(asset.path, "Assets/Data/test.raw");
159        assert_eq!(asset.importer_type, ImporterType::RawHeightmap);
160        assert!(!asset.is_folder());
161    }
162
163    #[test]
164    fn test_text_asset() {
165        let asset = Asset::text("Assets/Config/settings.json", r#"{"key": "value"}"#);
166        assert_eq!(asset.importer_type, ImporterType::Text);
167    }
168
169    #[test]
170    fn test_folder_asset() {
171        let asset = Asset::folder("Assets/Terrain");
172        assert!(asset.is_folder());
173        assert_eq!(asset.importer_type, ImporterType::Folder);
174    }
175
176    #[test]
177    fn test_deterministic_guid() {
178        let asset1 = Asset::binary("Assets/test.raw", vec![]);
179        let asset2 = Asset::binary("Assets/test.raw", vec![1, 2, 3]);
180        assert_eq!(asset1.guid, asset2.guid); // Same path = same GUID
181    }
182}