1use bevy::prelude::{Asset, AssetServer, Component, Handle, debug, info};
4use rhai::{Engine, OptimizationLevel, Scope};
5use serialization::{Deserialize, SerializationFormat, Serialize, deserialize, serialize_to};
6use std::collections::HashMap;
7use std::fs::File;
8use std::io::read_to_string;
9use std::path::Path;
10use std::path::PathBuf;
11use thiserror::Error;
12use utils::file_name;
13use walkdir::WalkDir;
14
15const MANIFEST_FILE_NAME: &str = "asset_pack.toml";
17
18#[derive(Component, Debug)]
22pub struct AssetPack {
23 pub state: AssetPackState,
28 pub id: String,
33 pub name: String,
37 pub root: PathBuf,
42 pub meta_dir: PathBuf,
47 index: HashMap<String, PathBuf>,
51 script: Option<String>,
54}
55
56#[derive(Serialize, Deserialize, Debug)]
58#[allow(
59 clippy::missing_docs_in_private_items,
60 reason = "Copied from the original struct"
61)]
62struct _AssetPack {
63 pub id: String,
64 pub name: String,
65 index: HashMap<String, PathBuf>,
66 script: Option<String>,
67}
68
69#[derive(Default, Debug, Serialize, Deserialize)]
71pub enum AssetPackState {
72 #[default]
75 Created,
76 Indexing,
78 Invalid(String),
80 Ready,
82}
83
84#[derive(Error, Debug)]
86pub enum AssetPackError {
87 #[error("An IO error occurred while reading/writing the asset pack manifest")]
89 ManifestFile(#[from] std::io::Error),
90 #[error("An error occurred while serialising the asset pack manifest")]
92 Serialisation(#[from] serialization::SerializationError),
93}
94
95impl AssetPack {
96 pub fn new(root: &Path, meta_dir: &Path, name: Option<String>) -> Result<Self, AssetPackError> {
102 let root = root.canonicalize()?;
103 let id = blake3::hash(root.as_os_str().as_encoded_bytes()).to_string();
104 let name = name
105 .or_else(|| file_name(&root))
106 .unwrap_or_else(|| id.clone());
107
108 info!("Created new asset pack with ID: {}", id);
109 Ok(Self {
110 state: AssetPackState::Created,
111 id: id.clone(),
112 name,
113 root,
114 meta_dir: meta_dir.to_path_buf(),
115 index: HashMap::new(),
116 script: None,
117 })
118 }
119
120 pub(crate) fn delete(&self) -> Result<(), AssetPackError> {
125 info!("Deleting asset pack: {}", self.id);
126 let config_file = self.root.join(MANIFEST_FILE_NAME);
127
128 std::fs::remove_file(config_file)?;
129 std::fs::remove_dir_all(self.meta_dir.clone())?;
130 Ok(())
131 }
132
133 pub fn save_manifest(&self) -> Result<(), AssetPackError> {
140 debug!("Saving manifest for {}", self.id);
141 let config = _AssetPack::from(self);
142 let manifest = self.root.join(MANIFEST_FILE_NAME);
143 let manifest = File::create(manifest).map_err(AssetPackError::ManifestFile)?;
144
145 serialize_to(&config, &SerializationFormat::Toml, manifest)
146 .map_err(AssetPackError::Serialisation)
147 }
148
149 pub fn load_manifest(root: &Path, meta_dir: &Path) -> Result<Self, AssetPackError> {
156 let manifest = root.join(MANIFEST_FILE_NAME);
157 debug!("Loading manifest for {}", manifest.display());
158 let manifest = File::open(manifest).map_err(AssetPackError::ManifestFile)?;
159 let manifest = read_to_string(manifest).map_err(AssetPackError::ManifestFile)?;
160
161 let manifest: _AssetPack = deserialize(manifest.as_bytes(), &SerializationFormat::Toml)?;
162 info!("Loaded manifest for {}", manifest.id);
163 Ok(Self {
164 state: AssetPackState::Created,
165 id: manifest.id,
166 name: manifest.name,
167 root: root.to_path_buf(),
168 meta_dir: meta_dir.to_path_buf(),
169 index: manifest.index,
170 script: manifest.script,
171 })
172 }
173
174 #[allow(clippy::missing_panics_doc, reason = "Temporary implementation")]
176 pub fn index(&mut self) {
177 let walker = WalkDir::new(&self.root);
178 let engine = Engine::new();
179 let mut scope = Scope::new();
180 let script = engine
181 .compile(include_str!("../scripts/filter.rhai"))
182 .unwrap();
183 let script = engine.optimize_ast(&scope, script, OptimizationLevel::Full);
184
185 {
186 #[cfg(feature = "dev")]
187 let _span = bevy::prelude::info_span!("Indexing", name = "indexing").entered();
188
189 let mut count = 0;
190 for entry in walker.into_iter().flatten() {
191 if !engine
192 .call_fn::<bool>(&mut scope, &script, "filter", (String::new(),))
193 .unwrap()
194 {
195 continue;
196 }
197
198 let path = entry.path().to_path_buf();
199 let path = path.strip_prefix(&self.root).unwrap();
200 let key = blake3::hash(path.as_os_str().as_encoded_bytes()).to_string();
201
202 self.index.insert(key, path.to_path_buf());
203 count += 1;
204 }
205
206 info!("Finished indexing {count} assets");
207 }
208
209 self.save_manifest().unwrap();
210 }
211
212 #[must_use]
214 pub fn resolve(&self, id: &String) -> Option<PathBuf> {
215 debug!("{} is resolving asset {}", self.id, id);
216 self.index.get(id).map(|path| self.root.join(path))
217 }
218
219 #[must_use = "Unused asset handle would be dropped immediately"]
221 pub fn load<T>(&self, asset_server: &AssetServer, id: &String) -> Option<Handle<T>>
222 where
223 T: Asset,
224 {
225 if let Some(path) = self.resolve(id) {
226 return Some(asset_server.load::<T>(path));
227 }
228
229 None
230 }
231}
232
233impl From<&AssetPack> for _AssetPack {
234 fn from(pack: &AssetPack) -> Self {
235 Self {
236 id: pack.id.clone(),
237 name: pack.name.clone(),
238 index: pack.index.clone(),
239 script: pack.script.clone(),
240 }
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 #![allow(clippy::missing_panics_doc)]
247 #![allow(clippy::missing_errors_doc)]
248
249 use super::*;
250 use tempfile::tempdir;
251
252 #[test]
253 fn new_asset_pack_id_is_stable() {
254 let path = Path::new(".");
255 let pack = AssetPack::new(path, path, None).unwrap();
256 let pack2 = AssetPack::new(path, path, None).unwrap();
257
258 assert_eq!(pack.id, pack2.id);
259 }
260
261 #[test]
262 fn new_asset_pack_id_unique() -> anyhow::Result<()> {
263 let path1 = tempdir()?;
264 let path2 = tempdir()?;
265 let pack1 = AssetPack::new(path1.path(), path1.path(), None)?;
266 let pack2 = AssetPack::new(path2.path(), path2.path(), None)?;
267
268 assert_ne!(pack1.id, pack2.id);
269 Ok(())
270 }
271
272 #[test]
273 #[should_panic = "Should fail to create asset pack"]
274 fn new_asset_error_on_invalid_path() {
275 let path = Path::new("./does/not/exist");
276 AssetPack::new(path, path, None).expect("Should fail to create asset pack");
277 }
278}