veloren_common_state/plugin/
mod.rs1pub 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 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(|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 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 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 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 pub fn plugin_list(&self) -> Vec<PluginHash> {
294 self.plugins.iter().map(|plugin| plugin.hash).collect()
295 }
296
297 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 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
361pub enum CommandResults {
363 UnknownCommand,
364 HostError(wasmtime::Error),
365 PluginError(String),
366}