1use crate::{AssetPack, AssetPackError};
4use bevy::prelude::{Resource, debug, info};
5use semver::Version;
6use serialization::{Deserialize, SerializationFormat, Serialize, deserialize, serialize_to};
7use std::collections::HashMap;
8use std::fs::{File, create_dir_all};
9use std::io::Read;
10use std::path::{Path, PathBuf};
11use thiserror::Error;
12use utils::{DirectoryError, cache_path, config_path};
13
14const LIBRARY_FILE_NAME: &str = "library.toml";
16
17#[derive(Resource, Debug, Serialize, Deserialize)]
21pub struct AssetLibrary {
22 version: Version,
24 registered_packs: HashMap<String, AssetLibraryEntry>,
26 #[serde(skip)]
31 loaded_packs: HashMap<String, AssetPack>,
32}
33
34#[derive(Default, Debug, Serialize, Deserialize)]
36struct AssetLibraryEntry {
37 root: PathBuf,
41 index: PathBuf,
43}
44
45#[derive(Error, Debug)]
47pub enum AssetLibraryError {
48 #[error("failed to locate library configuration")]
50 LocateConfigFolder(#[from] DirectoryError),
51 #[error("failed to read library configuration")]
53 ReadFile(#[from] std::io::Error),
54 #[error("failed to (de)serialize library configuration")]
56 Serialisation(#[from] serialization::SerializationError),
57 #[error(transparent)]
59 OpenAssetPack(#[from] AssetPackError),
60 #[error("Could not resolve AssetPack with ID '{0}'")]
62 NotFound(String),
63}
64
65impl Default for AssetLibrary {
66 fn default() -> Self {
67 Self {
68 version: utils::version().clone(),
69 registered_packs: HashMap::new(),
70 loaded_packs: HashMap::new(),
71 }
72 }
73}
74
75impl AssetLibrary {
76 pub fn load_or_default(path: Option<PathBuf>) -> Result<Self, AssetLibraryError> {
83 match Self::load(path) {
84 Err(AssetLibraryError::ReadFile(_)) => {
85 debug!("Failed to load library, using default");
86 Ok(Self::default())
87 }
88 result => result,
89 }
90 }
91
92 pub fn load(path: Option<PathBuf>) -> Result<Self, AssetLibraryError> {
102 let path = Self::get_path(path)?.join(LIBRARY_FILE_NAME);
103
104 debug!("Attempting to load {}", path.display());
105 let mut file = File::open(path).map_err(AssetLibraryError::ReadFile)?;
106 let mut contents = String::new();
107 file.read_to_string(&mut contents)
108 .map_err(AssetLibraryError::ReadFile)?;
109
110 deserialize(contents.as_bytes(), &SerializationFormat::Toml)
111 .map_err(AssetLibraryError::Serialisation)
112 }
113
114 pub fn delete(&mut self) -> Result<(), AssetLibraryError> {
120 info!("Running delete on asset library");
121 let ids = self.iter().map(|(id, _)| id.clone()).collect::<Vec<_>>();
122 for id in ids {
123 self.delete_pack(&id)?;
124 }
125
126 let path = Self::get_path(None)?.join(LIBRARY_FILE_NAME);
127 let _ = std::fs::remove_file(path);
128 Ok(())
129 }
130
131 pub fn delete_pack(&mut self, id: &String) -> Result<(), AssetLibraryError> {
138 debug!("Deleting pack {}", id);
139 if !self.is_pack_loaded(id) {
140 self.load_pack(id)?;
141 }
142
143 if let Some(pack) = self.get_pack_mut(id) {
144 pack.delete()?;
145
146 self.loaded_packs.remove(id);
147 self.registered_packs.remove(id);
148 }
149
150 Ok(())
151 }
152
153 pub fn save(&self, path: Option<PathBuf>) -> Result<(), AssetLibraryError> {
162 let path = Self::get_path(path)?;
163
164 debug!("Saving library to {}", path.display());
165 create_dir_all(&path)?; let file =
167 File::create(path.join(LIBRARY_FILE_NAME)).map_err(AssetLibraryError::ReadFile)?;
168 serialize_to(self, &SerializationFormat::Toml, &file)?;
169 Ok(())
170 }
171
172 pub fn add_pack(
182 &mut self,
183 root: &Path,
184 name: Option<String>,
185 ) -> Result<String, AssetLibraryError> {
186 let meta_dir = cache_path()?;
187 let pack = AssetPack::new(root, meta_dir.as_path(), name)?;
188 let pack_id = pack.id.clone();
189 let entry = AssetLibraryEntry {
190 root: root.to_path_buf(),
191 index: meta_dir.clone(),
192 };
193
194 self.registered_packs.insert(pack_id.clone(), entry);
195 self.loaded_packs.insert(pack_id.clone(), pack);
196
197 info!("Registered pack {}", pack_id);
198 Ok(pack_id)
199 }
200
201 pub fn load_pack(&mut self, id: &String) -> Result<&mut AssetPack, AssetLibraryError> {
208 let Some(entry) = self.registered_packs.get(id) else {
209 return Err(AssetLibraryError::NotFound(id.clone()));
210 };
211
212 let pack = AssetPack::load_manifest(entry.root.as_path(), entry.index.as_path())?;
213 self.loaded_packs.insert(id.clone(), pack);
214
215 debug!("Loaded pack {}", id);
216 self.loaded_packs
217 .get_mut(id)
218 .ok_or(AssetLibraryError::NotFound(id.clone()))
219 }
220
221 #[inline]
223 #[must_use]
224 pub fn is_pack_loaded(&self, id: &String) -> bool {
225 self.loaded_packs.contains_key(id)
226 }
227
228 #[inline]
232 #[must_use]
233 pub fn is_pack_registered(&self, id: &String) -> bool {
234 self.registered_packs.contains_key(id)
235 }
236
237 #[inline]
241 #[must_use]
242 pub fn get_pack(&self, id: &String) -> Option<&AssetPack> {
243 self.loaded_packs.get(id)
244 }
245
246 #[inline]
250 #[must_use]
251 pub fn get_pack_mut(&mut self, id: &String) -> Option<&mut AssetPack> {
252 self.loaded_packs.get_mut(id)
253 }
254
255 #[inline]
257 pub fn iter(&self) -> impl Iterator<Item = (&String, &PathBuf)> {
258 self.registered_packs
259 .iter()
260 .map(|(key, value)| (key, &value.root))
261 }
262
263 fn get_path(path: Option<PathBuf>) -> Result<PathBuf, AssetLibraryError> {
268 let path = if let Some(path) = path {
269 path
270 } else {
271 config_path().map_err(AssetLibraryError::LocateConfigFolder)?
272 };
273
274 Ok(path)
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 #![allow(clippy::missing_panics_doc)]
281 #![allow(clippy::missing_errors_doc)]
282
283 use super::*;
284
285 #[test]
286 fn add_pack_creates_asset_pack() -> anyhow::Result<()> {
287 let tmp = tempfile::tempdir()?;
288 let mut library = AssetLibrary::default();
289 let pack_id = library.add_pack(tmp.path(), None)?;
290
291 assert_eq!(library.registered_packs.len(), 1);
292 assert!(library.registered_packs.contains_key(&pack_id));
293 Ok(())
294 }
295
296 #[test]
297 fn save_and_load_library() -> anyhow::Result<()> {
298 let tmp = tempfile::tempdir()?;
299 let mut library = AssetLibrary::default();
300 let pack_id = library.add_pack(tmp.path(), None)?;
301
302 library.save(Some(tmp.path().to_path_buf()))?;
303 let library = AssetLibrary::load_or_default(Some(tmp.path().to_path_buf()))?;
304
305 assert_eq!(library.registered_packs.len(), 1);
306 assert!(library.registered_packs.contains_key(&pack_id));
307 Ok(())
308 }
309
310 #[test]
311 fn load_asset_pack_requires_registration() -> anyhow::Result<()> {
312 let tmp = tempfile::tempdir()?;
313 let mut library = AssetLibrary::default();
314 let pack = AssetPack::new(tmp.path(), tmp.path(), None)?;
315
316 library
317 .load_pack(&pack.id)
318 .expect_err("Asset pack should not be registered");
319 assert!(library.loaded_packs.is_empty());
320 assert!(library.registered_packs.is_empty());
321 Ok(())
322 }
323
324 #[test]
325 fn iterate_registered_packs() -> anyhow::Result<()> {
326 let tmp = tempfile::tempdir()?;
327 let mut library = AssetLibrary::default();
328 let pack_id = library.add_pack(tmp.path(), None)?;
329 library.loaded_packs.clear();
330
331 for (id, entry) in library.iter() {
332 assert_eq!(id, &pack_id);
333 assert_eq!(entry, &tmp.path().to_path_buf());
334 }
335 Ok(())
336 }
337}