veloren_common_assets/
plugin_cache.rs

1use std::{path::PathBuf, sync::RwLock};
2
3use crate::Concatenate;
4
5use super::{ASSETS_PATH, fs::FileSystem};
6use assets_manager::{
7    AnyCache, AssetCache, BoxedError, Compound, Storable,
8    asset::DirLoadable,
9    hot_reloading::EventSender,
10    source::{FileContent, Source, Tar},
11};
12
13struct PluginEntry {
14    path: PathBuf,
15    cache: AssetCache<Tar>,
16}
17
18/// The location of this asset
19enum AssetSource {
20    FileSystem,
21    Plugin { index: usize },
22}
23
24struct SourceAndContents<'a>(AssetSource, FileContent<'a>);
25
26/// This source combines assets loaded from the filesystem and from plugins.
27/// It is typically used via the CombinedCache type.
28///
29/// A load will search through all sources and warn about unhandled duplicates.
30pub struct CombinedSource {
31    fs: AssetCache<FileSystem>,
32    plugin_list: RwLock<Vec<PluginEntry>>,
33}
34
35impl CombinedSource {
36    pub fn new() -> std::io::Result<Self> {
37        Ok(Self {
38            fs: AssetCache::with_source(FileSystem::new()?),
39            plugin_list: RwLock::new(Vec::new()),
40        })
41    }
42}
43
44impl CombinedSource {
45    /// Look for an asset in all known sources
46    fn read_multiple(&self, id: &str, ext: &str) -> Vec<SourceAndContents<'_>> {
47        let mut result = Vec::new();
48        if let Ok(file_entry) = self.fs.raw_source().read(id, ext) {
49            result.push(SourceAndContents(AssetSource::FileSystem, file_entry));
50        }
51        for (n, p) in self.plugin_list.read().unwrap().iter().enumerate() {
52            if let Ok(entry) = p.cache.raw_source().read(id, ext) {
53                // the data is behind an RwLockReadGuard, so own it for returning
54                result.push(SourceAndContents(
55                    AssetSource::Plugin { index: n },
56                    match entry {
57                        FileContent::Slice(s) => FileContent::Buffer(Vec::from(s)),
58                        FileContent::Buffer(b) => FileContent::Buffer(b),
59                        FileContent::Owned(s) => {
60                            FileContent::Buffer(Vec::from(s.as_ref().as_ref()))
61                        },
62                    },
63                ));
64            }
65        }
66        result
67    }
68
69    /// Return the path of a source
70    fn plugin_path(&self, index: &AssetSource) -> Option<PathBuf> {
71        match index {
72            AssetSource::FileSystem => Some(ASSETS_PATH.clone()),
73            AssetSource::Plugin { index } => self.plugin_list
74                .read()
75                .unwrap()
76                .get(*index)
77                // We don't want to keep the lock, so we clone
78                .map(|plugin| plugin.path.clone()),
79        }
80    }
81}
82
83impl Source for CombinedSource {
84    fn read(&self, id: &str, ext: &str) -> std::io::Result<FileContent<'_>> {
85        // We could shortcut on fs if we dont check for conflicts
86        let mut entries = self.read_multiple(id, ext);
87        if entries.is_empty() {
88            Err(std::io::ErrorKind::NotFound.into())
89        } else {
90            if entries.len() > 1 {
91                let patha = self.plugin_path(&entries[0].0);
92                let pathb = self.plugin_path(&entries[1].0);
93                tracing::error!("Duplicate asset {id} in {patha:?} and {pathb:?}");
94            }
95            // unconditionally return the first asset found
96            Ok(entries.swap_remove(0).1)
97        }
98    }
99
100    fn read_dir(
101        &self,
102        id: &str,
103        f: &mut dyn FnMut(assets_manager::source::DirEntry),
104    ) -> std::io::Result<()> {
105        // TODO: We should combine the sources, but this isn't used in veloren
106        self.fs.raw_source().read_dir(id, f)
107    }
108
109    fn exists(&self, entry: assets_manager::source::DirEntry) -> bool {
110        self.fs.raw_source().exists(entry)
111            || self
112                .plugin_list
113                .read()
114                .unwrap()
115                .iter()
116                .any(|plugin| plugin.cache.raw_source().exists(entry))
117    }
118
119    // TODO: Enable hot reloading for plugins
120    fn make_source(&self) -> Option<Box<dyn Source + Send>> { self.fs.raw_source().make_source() }
121
122    fn configure_hot_reloading(&self, events: EventSender) -> Result<(), BoxedError> {
123        self.fs.raw_source().configure_hot_reloading(events)
124    }
125}
126
127/// A cache combining filesystem and plugin assets
128pub struct CombinedCache(AssetCache<CombinedSource>);
129
130impl CombinedCache {
131    pub fn new() -> std::io::Result<Self> {
132        CombinedSource::new().map(|combined_source| Self(AssetCache::with_source(combined_source)))
133    }
134
135    #[doc(hidden)]
136    // Provide a cache to the "combine_static" functions as they omit
137    // wrapping in a Compound (which enables hot-reload)
138    pub(crate) fn non_reloading_cache(&self) -> AnyCache<'_> {
139        self.0.raw_source().fs.as_any_cache()
140    }
141
142    /// Combine objects from filesystem and plugins
143    pub fn combine<T: Concatenate>(
144        &self,
145        // this cache registers with hot reloading
146        reloading_cache: AnyCache,
147        mut load_from: impl FnMut(AnyCache) -> Result<T, assets_manager::Error>,
148    ) -> Result<T, assets_manager::Error> {
149        let mut result = load_from(reloading_cache);
150        // Report a severe error from the filesystem asset even if later overwritten by
151        // an Ok value from a plugin
152        if let Err(ref fs_error) = result {
153            match fs_error
154                .reason()
155                .downcast_ref::<std::io::Error>()
156                .map(|io_error| io_error.kind())
157            {
158                Some(std::io::ErrorKind::NotFound) => (),
159                _ => tracing::error!("Filesystem asset load {fs_error:?}"),
160            }
161        }
162        for plugin in self.0.raw_source().plugin_list.read().unwrap().iter() {
163            match load_from(plugin.cache.as_any_cache()) {
164                Ok(b) => {
165                    result = if let Ok(a) = result {
166                        Ok(a.concatenate(b))
167                    } else {
168                        Ok(b)
169                    };
170                },
171                // Report any error other than NotFound
172                Err(plugin_error) => {
173                    match plugin_error
174                        .reason()
175                        .downcast_ref::<std::io::Error>()
176                        .map(|io_error| io_error.kind())
177                    {
178                        Some(std::io::ErrorKind::NotFound) => (),
179                        _ => tracing::error!(
180                            "Loading from {:?} failed {plugin_error:?}",
181                            plugin.path
182                        ),
183                    }
184                },
185            }
186        }
187        result
188    }
189
190    /// Add a tar archive (a plugin) to the system.
191    /// All files in that tar file become potential assets.
192    pub fn register_tar(&self, path: PathBuf) -> std::io::Result<()> {
193        let tar_source = Tar::open(&path)?;
194        let cache = AssetCache::with_source(tar_source);
195        self.0
196            .raw_source()
197            .plugin_list
198            .write()
199            .unwrap()
200            .push(PluginEntry { path, cache });
201        Ok(())
202    }
203
204    // Just forward these methods to the cache
205    #[inline]
206    #[cfg(feature = "hot-reloading")]
207    pub fn enhance_hot_reloading(&'static self) { self.0.enhance_hot_reloading(); }
208
209    #[inline]
210    pub fn load_rec_dir<A: DirLoadable>(
211        &self,
212        id: &str,
213    ) -> Result<&assets_manager::Handle<assets_manager::RecursiveDirectory<A>>, assets_manager::Error>
214    {
215        self.0.load_rec_dir(id)
216    }
217
218    #[inline]
219    pub fn load<A: Compound>(
220        &self,
221        id: &str,
222    ) -> Result<&assets_manager::Handle<A>, assets_manager::Error> {
223        self.0.load(id)
224    }
225
226    #[inline]
227    pub fn get_or_insert<A: Storable>(&self, id: &str, a: A) -> &assets_manager::Handle<A> {
228        self.0.get_or_insert(id, a)
229    }
230
231    #[inline]
232    pub fn load_owned<A: Compound>(&self, id: &str) -> Result<A, assets_manager::Error> {
233        self.0.load_owned(id)
234    }
235}