veloren_common_assets/
plugin_cache.rs

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