veloren_common_state/plugin/
mod.rs

1pub mod errors;
2pub mod memory_manager;
3pub mod module;
4
5use bincode::ErrorKind;
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(|e| PluginError::Encoding(Box::new(ErrorKind::InvalidUtf8Encoding(e))))?,
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        match Self::from_assets() {
205            Ok(plugin_mgr) => plugin_mgr,
206            Err(e) => {
207                tracing::error!(?e, "Failed to read plugins from assets");
208                PluginMgr::default()
209            },
210        }
211    }
212
213    pub fn from_assets() -> Result<Self, PluginError> {
214        let mut assets_path = (*ASSETS_PATH).clone();
215        assets_path.push("plugins");
216        info!("Searching {:?} for plugins...", assets_path);
217        Self::from_dir(assets_path)
218    }
219
220    pub fn from_dir<P: AsRef<Path>>(path: P) -> Result<Self, PluginError> {
221        let plugins = fs::read_dir(path)
222            .map_err(PluginError::Io)?
223            .filter_map(|e| e.ok())
224            .map(|entry| {
225                if entry.file_type().map(|ft| ft.is_file()).unwrap_or(false)
226                    && entry
227                        .path()
228                        .file_name()
229                        .and_then(|n| n.to_str())
230                        .map(|s| s.ends_with(".plugin.tar"))
231                        .unwrap_or(false)
232                {
233                    info!("Loading plugin at {:?}", entry.path());
234                    Plugin::from_path(entry.path()).map(|plugin| {
235                        if let Err(e) = common::assets::register_tar(entry.path()) {
236                            error!("Plugin {:?} tar error {e:?}", entry.path());
237                        }
238                        Some(plugin)
239                    })
240                } else {
241                    Ok(None)
242                }
243            })
244            .filter_map(Result::transpose)
245            .inspect(|p| {
246                let _ = p.as_ref().map_err(|e| error!(?e, "Failed to load plugin"));
247            })
248            .collect::<Result<Vec<_>, _>>()?;
249
250        for plugin in &plugins {
251            info!(
252                "Loaded plugin '{}' with {} module(s)",
253                plugin.data.name,
254                plugin.modules.len()
255            );
256        }
257
258        Ok(Self { plugins })
259    }
260
261    /// Add a plugin received from the server
262    pub fn load_server_plugin(&mut self, path: PathBuf) -> Result<PluginHash, PluginError> {
263        Plugin::from_path(path.clone()).map(|plugin| {
264            if let Err(e) = common::assets::register_tar(path.clone()) {
265                error!("Plugin {:?} tar error {e:?}", path.as_path());
266            }
267            let hash = plugin.hash;
268            self.plugins.push(plugin);
269            hash
270        })
271    }
272
273    pub fn cache_server_plugin(
274        &mut self,
275        base_dir: &Path,
276        data: Vec<u8>,
277    ) -> Result<PluginHash, PluginError> {
278        let path = store_server_plugin(base_dir, data).map_err(PluginError::Io)?;
279        self.load_server_plugin(path)
280    }
281
282    /// list all registered plugins
283    pub fn plugin_list(&self) -> Vec<PluginHash> {
284        self.plugins.iter().map(|plugin| plugin.hash).collect()
285    }
286
287    /// retrieve a specific plugin
288    pub fn find(&self, hash: &PluginHash) -> Option<&Plugin> {
289        self.plugins.iter().find(|plugin| &plugin.hash == hash)
290    }
291
292    pub fn load_event(
293        &mut self,
294        ecs: &EcsWorld,
295        mode: common::resources::GameMode,
296    ) -> Result<(), PluginModuleError> {
297        self.plugins
298            .iter_mut()
299            .try_for_each(|plugin| plugin.load_event(ecs, mode))
300    }
301
302    pub fn command_event(
303        &mut self,
304        ecs: &EcsWorld,
305        name: &str,
306        args: &[String],
307        player: Uid,
308    ) -> Result<Vec<String>, CommandResults> {
309        // return last value or last error
310        let mut result = Err(CommandResults::UnknownCommand);
311        self.plugins.iter_mut().for_each(|plugin| {
312            match plugin.command_event(ecs, name, args, player) {
313                Ok(val) => result = Ok(val),
314                Err(CommandResults::UnknownCommand) => (),
315                Err(err) => {
316                    if result.is_err() {
317                        result = Err(err);
318                    }
319                },
320            }
321        });
322        result
323    }
324
325    pub fn create_body(&mut self, name: &str) -> Option<module::Body> {
326        let mut result = None;
327        self.plugins.iter_mut().for_each(|plugin| {
328            if let Some(body) = plugin.create_body(name) {
329                result = Some(body);
330            }
331        });
332        result
333    }
334
335    pub fn update_skeleton(
336        &mut self,
337        body: &module::Body,
338        dep: &module::Dependency,
339        time: f32,
340    ) -> Option<module::Skeleton> {
341        let mut result = None;
342        self.plugins.iter_mut().for_each(|plugin| {
343            if let Some(skeleton) = plugin.update_skeleton(body, dep, time) {
344                result = Some(skeleton);
345            }
346        });
347        result
348    }
349}
350
351/// Error returned by plugin based server commands
352pub enum CommandResults {
353    UnknownCommand,
354    HostError(wasmtime::Error),
355    PluginError(String),
356}