veloren_common_state/plugin/
module.rs

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