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