veloren_common_state/plugin/
module.rs

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