veloren_common_state/plugin/
mod.rs1pub 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 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
54pub 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 pub fn path(&self) -> &Path { self.path.as_path() }
167
168 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 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 pub fn plugin_list(&self) -> Vec<PluginHash> {
284 self.plugins.iter().map(|plugin| plugin.hash).collect()
285 }
286
287 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 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
351pub enum CommandResults {
353 UnknownCommand,
354 HostError(wasmtime::Error),
355 PluginError(String),
356}