veloren_common_state/plugin/
module.rs

1use std::sync::{Arc, Mutex};
2
3use super::{
4    CommandResults,
5    errors::PluginModuleError,
6    memory_manager::{EcsAccessManager, EcsWorld},
7};
8use hashbrown::{HashMap, HashSet};
9use wasmtime::{
10    Config, Engine, Store,
11    component::{Component, Linker},
12};
13use wasmtime_wasi::WasiView;
14
15pub(crate) mod types_mod {
16    wasmtime::component::bindgen!({
17        path: "../../plugin/wit/veloren.wit",
18        world: "common-types",
19    });
20}
21
22wasmtime::component::bindgen!({
23    path: "../../plugin/wit/veloren.wit",
24    world: "plugin",
25    with: {
26        "veloren:plugin/types@0.0.1": types_mod::veloren::plugin::types,
27        "veloren:plugin/information@0.0.1/entity": Entity,
28    },
29});
30
31mod animation_plugin {
32    wasmtime::component::bindgen!({
33        path: "../../plugin/wit/veloren.wit",
34        world: "animation-plugin",
35        with: {
36            "veloren:plugin/types@0.0.1": super::types_mod::veloren::plugin::types,
37        },
38    });
39}
40
41mod server_plugin {
42    wasmtime::component::bindgen!({
43        path: "../../plugin/wit/veloren.wit",
44        world: "server-plugin",
45        with: {
46            "veloren:plugin/types@0.0.1": super::types_mod::veloren::plugin::types,
47            "veloren:plugin/information@0.0.1/entity": super::Entity,
48        },
49    });
50}
51
52pub struct Entity {
53    uid: common::uid::Uid,
54}
55
56pub use animation::Body;
57use exports::veloren::plugin::animation;
58pub use types_mod::veloren::plugin::types::{
59    self, CharacterState, Dependency, Skeleton, Transform,
60};
61use veloren::plugin::{actions, information};
62
63type StoreType = wasmtime::Store<WasiHostCtx>;
64
65/// This enum abstracts over the different types of plugins we defined
66enum PluginWrapper {
67    Full(Plugin),
68    Animation(animation_plugin::AnimationPlugin),
69    Server(server_plugin::ServerPlugin),
70}
71
72impl PluginWrapper {
73    fn load_event<S: wasmtime::AsContextMut>(
74        &self,
75        store: S,
76        mode: common::resources::GameMode,
77    ) -> wasmtime::Result<()>
78    where
79        <S as wasmtime::AsContext>::Data: std::marker::Send,
80    {
81        let mode = match mode {
82            common::resources::GameMode::Server => types::GameMode::Server,
83            common::resources::GameMode::Client => types::GameMode::Client,
84            common::resources::GameMode::Singleplayer => types::GameMode::SinglePlayer,
85        };
86        match self {
87            PluginWrapper::Full(pl) => pl.veloren_plugin_events().call_load(store, mode),
88            PluginWrapper::Animation(pl) => pl.veloren_plugin_events().call_load(store, mode),
89            PluginWrapper::Server(pl) => pl.veloren_plugin_events().call_load(store, mode),
90        }
91    }
92
93    fn command_event<S: wasmtime::AsContextMut>(
94        &self,
95        store: S,
96        name: &str,
97        args: &[String],
98        player: types::Uid,
99    ) -> wasmtime::Result<Result<Vec<String>, String>>
100    where
101        <S as wasmtime::AsContext>::Data: std::marker::Send,
102    {
103        match self {
104            PluginWrapper::Full(pl) => pl
105                .veloren_plugin_server_events()
106                .call_command(store, name, args, player),
107            PluginWrapper::Animation(_) => Ok(Err("not implemented".into())),
108            PluginWrapper::Server(pl) => pl
109                .veloren_plugin_server_events()
110                .call_command(store, name, args, player),
111        }
112    }
113
114    fn player_join_event(
115        &self,
116        store: &mut StoreType,
117        name: &str,
118        uuid: (types::Uid, types::Uid),
119    ) -> wasmtime::Result<types::JoinResult> {
120        match self {
121            PluginWrapper::Full(pl) => pl
122                .veloren_plugin_server_events()
123                .call_join(store, name, uuid),
124            PluginWrapper::Animation(_) => Ok(types::JoinResult::None),
125            PluginWrapper::Server(pl) => pl
126                .veloren_plugin_server_events()
127                .call_join(store, name, uuid),
128        }
129    }
130
131    fn create_body(&self, store: &mut StoreType, bodytype: i32) -> Option<animation::Body> {
132        match self {
133            PluginWrapper::Full(pl) => {
134                let body_iface = pl.veloren_plugin_animation().body();
135                body_iface.call_constructor(store, bodytype).ok()
136            },
137            PluginWrapper::Animation(pl) => {
138                let body_iface = pl.veloren_plugin_animation().body();
139                body_iface.call_constructor(store, bodytype).ok()
140            },
141            PluginWrapper::Server(_) => None,
142        }
143    }
144
145    fn update_skeleton(
146        &self,
147        store: &mut StoreType,
148        body: animation::Body,
149        dep: types::Dependency,
150        time: f32,
151    ) -> Option<types::Skeleton> {
152        match self {
153            PluginWrapper::Full(pl) => {
154                let body_iface = pl.veloren_plugin_animation().body();
155                body_iface.call_update_skeleton(store, body, dep, time).ok()
156            },
157            PluginWrapper::Animation(pl) => {
158                let body_iface = pl.veloren_plugin_animation().body();
159                body_iface.call_update_skeleton(store, body, dep, time).ok()
160            },
161            PluginWrapper::Server(_) => None,
162        }
163    }
164}
165
166/// This structure represent the WASM State of the plugin.
167pub struct PluginModule {
168    ecs: Arc<EcsAccessManager>,
169    plugin: PluginWrapper,
170    store: Mutex<wasmtime::Store<WasiHostCtx>>,
171    name: String,
172}
173
174struct WasiHostCtx {
175    preview2_ctx: wasmtime_wasi::WasiCtx,
176    preview2_table: wasmtime::component::ResourceTable,
177    ecs: Arc<EcsAccessManager>,
178    registered_commands: HashSet<String>,
179    registered_bodies: HashMap<String, types::BodyIndex>,
180}
181
182impl wasmtime_wasi::WasiView for WasiHostCtx {
183    fn table(&mut self) -> &mut wasmtime::component::ResourceTable { &mut self.preview2_table }
184
185    fn ctx(&mut self) -> &mut wasmtime_wasi::WasiCtx { &mut self.preview2_ctx }
186}
187
188impl information::Host for WasiHostCtx {}
189
190impl types::Host for WasiHostCtx {}
191
192impl actions::Host for WasiHostCtx {
193    fn register_command(&mut self, name: String) {
194        tracing::info!("Plugin registers /{name}");
195        self.registered_commands.insert(name);
196    }
197
198    fn player_send_message(&mut self, uid: actions::Uid, text: String) {
199        tracing::info!("Plugin sends message {text} to player {uid:?}");
200    }
201
202    fn register_animation(&mut self, name: String, id: types::BodyIndex) {
203        let _ = self.registered_bodies.insert(name, id);
204    }
205}
206
207impl information::HostEntity for WasiHostCtx {
208    fn find_entity(
209        &mut self,
210        uid: actions::Uid,
211    ) -> Result<wasmtime::component::Resource<information::Entity>, types::Error> {
212        self.table()
213            .push(Entity {
214                uid: common::uid::Uid(uid),
215            })
216            .map_err(|_err| types::Error::RuntimeError)
217    }
218
219    fn health(
220        &mut self,
221        self_: wasmtime::component::Resource<information::Entity>,
222    ) -> Result<information::Health, types::Error> {
223        let uid = self
224            .table()
225            .get(&self_)
226            .map_err(|_err| types::Error::RuntimeError)?
227            .uid;
228        // Safety: No reference is leaked out the function so it is safe.
229        let world = unsafe { self.ecs.get().ok_or(types::Error::EcsPointerNotAvailable)? };
230        let player = world
231            .id_maps
232            .uid_entity(uid)
233            .ok_or(types::Error::EcsEntityNotFound)?;
234        world
235            .health
236            .get(player)
237            .map(|health| information::Health {
238                current: health.current(),
239                base_max: health.base_max(),
240                maximum: health.maximum(),
241            })
242            .ok_or(types::Error::EcsComponentNotFound)
243    }
244
245    fn name(
246        &mut self,
247        self_: wasmtime::component::Resource<information::Entity>,
248    ) -> Result<String, types::Error> {
249        let uid = self
250            .table()
251            .get(&self_)
252            .map_err(|_err| types::Error::RuntimeError)?
253            .uid;
254        // Safety: No reference is leaked out the function so it is safe.
255        let world = unsafe { self.ecs.get().ok_or(types::Error::EcsPointerNotAvailable)? };
256        let player = world
257            .id_maps
258            .uid_entity(uid)
259            .ok_or(types::Error::EcsEntityNotFound)?;
260        Ok(world
261            .player
262            .get(player)
263            .ok_or(types::Error::EcsComponentNotFound)?
264            .alias
265            .to_owned())
266    }
267
268    fn drop(
269        &mut self,
270        rep: wasmtime::component::Resource<information::Entity>,
271    ) -> wasmtime::Result<()> {
272        Ok(self.table().delete(rep).map(|_entity| ())?)
273    }
274}
275
276struct InfoStream(String);
277
278impl wasmtime_wasi::HostOutputStream for InfoStream {
279    fn write(&mut self, bytes: bytes::Bytes) -> wasmtime_wasi::StreamResult<()> {
280        tracing::info!("{}: {}", self.0, String::from_utf8_lossy(bytes.as_ref()));
281        Ok(())
282    }
283
284    fn flush(&mut self) -> wasmtime_wasi::StreamResult<()> { Ok(()) }
285
286    fn check_write(&mut self) -> wasmtime_wasi::StreamResult<usize> { Ok(1024) }
287}
288
289#[wasmtime_wasi::async_trait]
290impl wasmtime_wasi::Subscribe for InfoStream {
291    async fn ready(&mut self) {}
292}
293
294struct ErrorStream(String);
295
296impl wasmtime_wasi::HostOutputStream for ErrorStream {
297    fn write(&mut self, bytes: bytes::Bytes) -> wasmtime_wasi::StreamResult<()> {
298        tracing::error!("{}: {}", self.0, String::from_utf8_lossy(bytes.as_ref()));
299        Ok(())
300    }
301
302    fn flush(&mut self) -> wasmtime_wasi::StreamResult<()> { Ok(()) }
303
304    fn check_write(&mut self) -> wasmtime_wasi::StreamResult<usize> { Ok(1024) }
305}
306
307#[wasmtime_wasi::async_trait]
308impl wasmtime_wasi::Subscribe for ErrorStream {
309    async fn ready(&mut self) {}
310}
311
312struct LogStream(String, tracing::Level);
313
314impl wasmtime_wasi::StdoutStream for LogStream {
315    fn stream(&self) -> Box<dyn wasmtime_wasi::HostOutputStream> {
316        if self.1 == tracing::Level::INFO {
317            Box::new(InfoStream(self.0.clone()))
318        } else {
319            Box::new(ErrorStream(self.0.clone()))
320        }
321    }
322
323    fn isatty(&self) -> bool { true }
324}
325
326impl PluginModule {
327    /// This function takes bytes from a WASM File and compile them
328    pub fn new(name: String, wasm_data: &[u8]) -> Result<Self, PluginModuleError> {
329        let ecs = Arc::new(EcsAccessManager::default());
330
331        // configure the wasm runtime
332        let mut config = Config::new();
333        config.wasm_component_model(true);
334
335        let engine = Engine::new(&config).map_err(PluginModuleError::Wasmtime)?;
336        // create a WASI environment (std implementing system calls)
337        let wasi = wasmtime_wasi::WasiCtxBuilder::new()
338            .stdout(LogStream(name.clone(), tracing::Level::INFO))
339            .stderr(LogStream(name.clone(), tracing::Level::ERROR))
340            .build();
341        let host_ctx = WasiHostCtx {
342            preview2_ctx: wasi,
343            preview2_table: wasmtime_wasi::ResourceTable::new(),
344            ecs: Arc::clone(&ecs),
345            registered_commands: HashSet::new(),
346            registered_bodies: HashMap::new(),
347        };
348        // the store contains all data of a wasm instance
349        let mut store = Store::new(&engine, host_ctx);
350
351        // load wasm from binary
352        let module =
353            Component::from_binary(&engine, wasm_data).map_err(PluginModuleError::Wasmtime)?;
354
355        // register WASI and Veloren methods with the runtime
356        let mut linker = Linker::new(&engine);
357        wasmtime_wasi::add_to_linker_sync(&mut linker).map_err(PluginModuleError::Wasmtime)?;
358        Plugin::add_to_linker(&mut linker, |x| x).map_err(PluginModuleError::Wasmtime)?;
359
360        let instance_fut = linker.instantiate(&mut store, &module);
361        let instance = (instance_fut).map_err(PluginModuleError::Wasmtime)?;
362
363        let plugin = match Plugin::new(&mut store, &instance) {
364            Ok(pl) => Ok(PluginWrapper::Full(pl)),
365            Err(_) => match animation_plugin::AnimationPlugin::new(&mut store, &instance) {
366                Ok(pl) => Ok(PluginWrapper::Animation(pl)),
367                Err(_) => server_plugin::ServerPlugin::new(&mut store, &instance)
368                    .map(PluginWrapper::Server),
369            },
370        }
371        .map_err(PluginModuleError::Wasmtime)?;
372
373        Ok(Self {
374            plugin,
375            ecs,
376            store: store.into(),
377            name,
378        })
379    }
380
381    pub fn name(&self) -> &str { &self.name }
382
383    // Implementation of the commands called from veloren and provided in plugins
384    pub fn load_event(
385        &mut self,
386        ecs: &EcsWorld,
387        mode: common::resources::GameMode,
388    ) -> Result<(), PluginModuleError> {
389        self.ecs
390            .execute_with(ecs, || {
391                self.plugin.load_event(self.store.get_mut().unwrap(), mode)
392            })
393            .map_err(PluginModuleError::Wasmtime)
394    }
395
396    pub fn command_event(
397        &mut self,
398        ecs: &EcsWorld,
399        name: &str,
400        args: &[String],
401        player: common::uid::Uid,
402    ) -> Result<Vec<String>, CommandResults> {
403        if !self
404            .store
405            .get_mut()
406            .unwrap()
407            .data()
408            .registered_commands
409            .contains(name)
410        {
411            return Err(CommandResults::UnknownCommand);
412        }
413        self.ecs.execute_with(ecs, || {
414            match self
415                .plugin
416                .command_event(self.store.get_mut().unwrap(), name, args, player.0)
417            {
418                Err(err) => Err(CommandResults::HostError(err)),
419                Ok(result) => result.map_err(CommandResults::PluginError),
420            }
421        })
422    }
423
424    pub fn player_join_event(
425        &mut self,
426        ecs: &EcsWorld,
427        name: &str,
428        uuid: common::uuid::Uuid,
429    ) -> types::JoinResult {
430        self.ecs.execute_with(ecs, || {
431            match self.plugin.player_join_event(
432                self.store.get_mut().unwrap(),
433                name,
434                uuid.as_u64_pair(),
435            ) {
436                Ok(value) => {
437                    tracing::info!("JoinResult {value:?}");
438                    value
439                },
440                Err(err) => {
441                    tracing::error!("join_event: {err:?}");
442                    types::JoinResult::None
443                },
444            }
445        })
446    }
447
448    pub fn create_body(&mut self, bodytype: &str) -> Option<animation::Body> {
449        let store = self.store.get_mut().unwrap();
450        let bodytype = store.data().registered_bodies.get(bodytype).copied();
451        bodytype.and_then(|bd| self.plugin.create_body(store, bd))
452    }
453
454    pub fn update_skeleton(
455        &mut self,
456        body: &animation::Body,
457        dep: &types::Dependency,
458        time: f32,
459    ) -> Option<types::Skeleton> {
460        self.plugin
461            .update_skeleton(self.store.get_mut().unwrap(), *body, *dep, time)
462    }
463}