veloren_common_state/plugin/
mod.rs

1pub mod errors;
2pub mod memory_manager;
3pub mod module;
4
5use bincode::error::DecodeError;
6use common::{assets::ASSETS_PATH, event::PluginHash, uid::Uid};
7use serde::{Deserialize, Serialize};
8use std::{
9    collections::{HashMap, HashSet},
10    fs,
11    io::{Read, Write},
12    path::{Path, PathBuf},
13};
14use tracing::{error, info};
15
16use self::{
17    errors::{PluginError, PluginModuleError},
18    memory_manager::EcsWorld,
19    module::PluginModule,
20};
21
22use sha2::Digest;
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct PluginData {
26    name: String,
27    modules: HashSet<PathBuf>,
28    dependencies: HashSet<String>,
29}
30
31fn compute_hash(data: &[u8]) -> PluginHash {
32    let shasum = sha2::Sha256::digest(data);
33    let mut shasum_iter = shasum.iter();
34    // a newer generic-array supports into_array ...
35    let shasum: PluginHash = std::array::from_fn(|_| *shasum_iter.next().unwrap());
36    shasum
37}
38
39fn cache_file_name(
40    mut base_dir: PathBuf,
41    hash: &PluginHash,
42    create_dir: bool,
43) -> Result<PathBuf, std::io::Error> {
44    base_dir.push("server-plugins");
45    if create_dir {
46        std::fs::create_dir_all(base_dir.as_path())?;
47    }
48    let name = hex::encode(hash);
49    base_dir.push(name);
50    base_dir.set_extension("plugin.tar");
51    Ok(base_dir)
52}
53
54// write received plugin to disk cache
55pub fn store_server_plugin(base_dir: &Path, data: Vec<u8>) -> Result<PathBuf, std::io::Error> {
56    let shasum = compute_hash(data.as_slice());
57    let result = cache_file_name(base_dir.to_path_buf(), &shasum, true)?;
58    let mut file = std::fs::File::create(result.as_path())?;
59    file.write_all(data.as_slice())?;
60    Ok(result)
61}
62
63pub fn find_cached(base_dir: &Path, hash: &PluginHash) -> Result<PathBuf, std::io::Error> {
64    let local_path = cache_file_name(base_dir.to_path_buf(), hash, false)?;
65    if local_path.as_path().exists() {
66        Ok(local_path)
67    } else {
68        Err(std::io::Error::from(std::io::ErrorKind::NotFound))
69    }
70}
71
72pub struct Plugin {
73    data: PluginData,
74    modules: Vec<PluginModule>,
75    hash: PluginHash,
76    path: PathBuf,
77    data_buf: Vec<u8>,
78}
79
80impl Plugin {
81    pub fn from_path(path_buf: PathBuf) -> Result<Self, PluginError> {
82        let mut reader = fs::File::open(path_buf.as_path()).map_err(PluginError::Io)?;
83        let mut buf = Vec::new();
84        reader.read_to_end(&mut buf).map_err(PluginError::Io)?;
85        let shasum = compute_hash(buf.as_slice());
86
87        let mut files = tar::Archive::new(&*buf)
88            .entries()
89            .map_err(PluginError::Io)?
90            .map(|e| {
91                e.and_then(|e| {
92                    Ok((e.path()?.into_owned(), {
93                        let offset = e.raw_file_position() as usize;
94                        buf[offset..offset + e.size() as usize].to_vec()
95                    }))
96                })
97            })
98            .collect::<Result<HashMap<_, _>, _>>()
99            .map_err(PluginError::Io)?;
100
101        let data = toml::de::from_str::<PluginData>(
102            std::str::from_utf8(
103                files
104                    .get(Path::new("plugin.toml"))
105                    .ok_or(PluginError::NoConfig)?,
106            )
107            .map_err(|inner| PluginError::Encoding(Box::new(DecodeError::Utf8 { inner })))?,
108        )
109        .map_err(PluginError::Toml)?;
110
111        let modules = data
112            .modules
113            .iter()
114            .map(|path| {
115                let wasm_data = files.remove(path).ok_or(PluginError::NoSuchModule)?;
116                PluginModule::new(data.name.to_owned(), &wasm_data).map_err(|e| {
117                    PluginError::PluginModuleError(data.name.to_owned(), "<init>".to_owned(), e)
118                })
119            })
120            .collect::<Result<_, _>>()?;
121
122        let data_buf = fs::read(&path_buf).map_err(PluginError::Io)?;
123
124        Ok(Plugin {
125            data,
126            modules,
127            hash: shasum,
128            path: path_buf,
129            data_buf,
130        })
131    }
132
133    pub fn load_event(
134        &mut self,
135        ecs: &EcsWorld,
136        mode: common::resources::GameMode,
137    ) -> Result<(), PluginModuleError> {
138        self.modules
139            .iter_mut()
140            .try_for_each(|module| module.load_event(ecs, mode))
141    }
142
143    pub fn command_event(
144        &mut self,
145        ecs: &EcsWorld,
146        name: &str,
147        args: &[String],
148        player: common::uid::Uid,
149    ) -> Result<Vec<String>, CommandResults> {
150        let mut result = Err(CommandResults::UnknownCommand);
151        self.modules.iter_mut().for_each(|module| {
152            match module.command_event(ecs, name, args, player) {
153                Ok(res) => result = Ok(res),
154                Err(CommandResults::UnknownCommand) => (),
155                Err(err) => {
156                    if result.is_err() {
157                        result = Err(err)
158                    }
159                },
160            }
161        });
162        result
163    }
164
165    /// get the path to the plugin file
166    pub fn path(&self) -> &Path { self.path.as_path() }
167
168    /// Get the data of this plugin
169    pub fn data_buf(&self) -> &[u8] { &self.data_buf }
170
171    pub fn create_body(&mut self, name: &str) -> Option<module::Body> {
172        let mut result = None;
173        self.modules.iter_mut().for_each(|module| {
174            if let Some(body) = module.create_body(name) {
175                result = Some(body);
176            }
177        });
178        result
179    }
180
181    pub fn update_skeleton(
182        &mut self,
183        body: &module::Body,
184        dep: &module::Dependency,
185        time: f32,
186    ) -> Option<module::Skeleton> {
187        let mut result = None;
188        self.modules.iter_mut().for_each(|module| {
189            if let Some(skel) = module.update_skeleton(body, dep, time) {
190                result = Some(skel);
191            }
192        });
193        result
194    }
195}
196
197#[derive(Default)]
198pub struct PluginMgr {
199    plugins: Vec<Plugin>,
200}
201
202impl PluginMgr {
203    pub fn from_asset_or_default() -> Self {
204        let mut path = (*ASSETS_PATH).clone();
205        path.push("plugins");
206        info!("Searching {:?} for plugins...", path);
207
208        match Self::from_dir(&path) {
209            Ok(plugin_mgr) => {
210                info!("{} plugin(s) loaded", plugin_mgr.plugins.len());
211                plugin_mgr
212            },
213            Err(e) => {
214                if let PluginError::FromDirDoesNotExist = e {
215                    info!("{:?} does not exist, no plugins loaded", path);
216                } else {
217                    error!(?e, "Failed to read plugins from assets");
218                };
219                PluginMgr::default()
220            },
221        }
222    }
223
224    fn from_dir(path: &Path) -> Result<Self, PluginError> {
225        let plugins = fs::read_dir(path)
226            .map_err(|e| {
227                if e.kind() == std::io::ErrorKind::NotFound {
228                    PluginError::FromDirDoesNotExist
229                } else {
230                    PluginError::Io(e)
231                }
232            })?
233            .filter_map(|e| e.ok())
234            .map(|entry| {
235                if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
236                    && entry
237                        .path()
238                        .file_name()
239                        .and_then(|n| n.to_str())
240                        .map(|s| s.ends_with(".plugin.tar"))
241                        .unwrap_or(false)
242                {
243                    info!("Loading plugin at {:?}", entry.path());
244                    Plugin::from_path(entry.path()).map(|plugin| {
245                        if let Err(e) = common::assets::register_tar(entry.path()) {
246                            error!("Plugin {:?} tar error {e:?}", entry.path());
247                        }
248                        Some(plugin)
249                    })
250                } else {
251                    Ok(None)
252                }
253            })
254            .filter_map(Result::transpose)
255            .inspect(|p| {
256                let _ = p.as_ref().map_err(|e| error!(?e, "Failed to load plugin"));
257            })
258            .collect::<Result<Vec<_>, _>>()?;
259
260        for plugin in &plugins {
261            info!(
262                "Loaded plugin '{}' with {} module(s)",
263                plugin.data.name,
264                plugin.modules.len()
265            );
266        }
267
268        Ok(Self { plugins })
269    }
270
271    /// Add a plugin received from the server
272    pub fn load_server_plugin(&mut self, path: PathBuf) -> Result<PluginHash, PluginError> {
273        Plugin::from_path(path.clone()).map(|plugin| {
274            if let Err(e) = common::assets::register_tar(path.clone()) {
275                error!("Plugin {:?} tar error {e:?}", path.as_path());
276            }
277            let hash = plugin.hash;
278            self.plugins.push(plugin);
279            hash
280        })
281    }
282
283    pub fn cache_server_plugin(
284        &mut self,
285        base_dir: &Path,
286        data: Vec<u8>,
287    ) -> Result<PluginHash, PluginError> {
288        let path = store_server_plugin(base_dir, data).map_err(PluginError::Io)?;
289        self.load_server_plugin(path)
290    }
291
292    /// list all registered plugins
293    pub fn plugin_list(&self) -> Vec<PluginHash> {
294        self.plugins.iter().map(|plugin| plugin.hash).collect()
295    }
296
297    /// retrieve a specific plugin
298    pub fn find(&self, hash: &PluginHash) -> Option<&Plugin> {
299        self.plugins.iter().find(|plugin| &plugin.hash == hash)
300    }
301
302    pub fn load_event(
303        &mut self,
304        ecs: &EcsWorld,
305        mode: common::resources::GameMode,
306    ) -> Result<(), PluginModuleError> {
307        self.plugins
308            .iter_mut()
309            .try_for_each(|plugin| plugin.load_event(ecs, mode))
310    }
311
312    pub fn command_event(
313        &mut self,
314        ecs: &EcsWorld,
315        name: &str,
316        args: &[String],
317        player: Uid,
318    ) -> Result<Vec<String>, CommandResults> {
319        // return last value or last error
320        let mut result = Err(CommandResults::UnknownCommand);
321        self.plugins.iter_mut().for_each(|plugin| {
322            match plugin.command_event(ecs, name, args, player) {
323                Ok(val) => result = Ok(val),
324                Err(CommandResults::UnknownCommand) => (),
325                Err(err) => {
326                    if result.is_err() {
327                        result = Err(err);
328                    }
329                },
330            }
331        });
332        result
333    }
334
335    pub fn create_body(&mut self, name: &str) -> Option<module::Body> {
336        let mut result = None;
337        self.plugins.iter_mut().for_each(|plugin| {
338            if let Some(body) = plugin.create_body(name) {
339                result = Some(body);
340            }
341        });
342        result
343    }
344
345    pub fn update_skeleton(
346        &mut self,
347        body: &module::Body,
348        dep: &module::Dependency,
349        time: f32,
350    ) -> Option<module::Skeleton> {
351        let mut result = None;
352        self.plugins.iter_mut().for_each(|plugin| {
353            if let Some(skeleton) = plugin.update_skeleton(body, dep, time) {
354                result = Some(skeleton);
355            }
356        });
357        result
358    }
359}
360
361/// Error returned by plugin based server commands
362pub enum CommandResults {
363    UnknownCommand,
364    HostError(wasmtime::Error),
365    PluginError(String),
366}