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        self.ecs.with(|world| {
229            let world = world.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
246    fn name(
247        &mut self,
248        self_: wasmtime::component::Resource<information::Entity>,
249    ) -> Result<String, types::Error> {
250        let uid = self
251            .table()
252            .get(&self_)
253            .map_err(|_err| types::Error::RuntimeError)?
254            .uid;
255        self.ecs.with(|world| {
256            let world = world.ok_or(types::Error::EcsPointerNotAvailable)?;
257            let player = world
258                .id_maps
259                .uid_entity(uid)
260                .ok_or(types::Error::EcsEntityNotFound)?;
261            Ok(world
262                .player
263                .get(player)
264                .ok_or(types::Error::EcsComponentNotFound)?
265                .alias
266                .to_owned())
267        })
268    }
269
270    fn drop(
271        &mut self,
272        rep: wasmtime::component::Resource<information::Entity>,
273    ) -> wasmtime::Result<()> {
274        Ok(self.table().delete(rep).map(|_entity| ())?)
275    }
276}
277
278struct InfoStream(String);
279
280impl wasmtime_wasi::HostOutputStream for InfoStream {
281    fn write(&mut self, bytes: bytes::Bytes) -> wasmtime_wasi::StreamResult<()> {
282        tracing::info!("{}: {}", self.0, String::from_utf8_lossy(bytes.as_ref()));
283        Ok(())
284    }
285
286    fn flush(&mut self) -> wasmtime_wasi::StreamResult<()> { Ok(()) }
287
288    fn check_write(&mut self) -> wasmtime_wasi::StreamResult<usize> { Ok(1024) }
289}
290
291#[wasmtime_wasi::async_trait]
292impl wasmtime_wasi::Subscribe for InfoStream {
293    async fn ready(&mut self) {}
294}
295
296struct ErrorStream(String);
297
298impl wasmtime_wasi::HostOutputStream for ErrorStream {
299    fn write(&mut self, bytes: bytes::Bytes) -> wasmtime_wasi::StreamResult<()> {
300        tracing::error!("{}: {}", self.0, String::from_utf8_lossy(bytes.as_ref()));
301        Ok(())
302    }
303
304    fn flush(&mut self) -> wasmtime_wasi::StreamResult<()> { Ok(()) }
305
306    fn check_write(&mut self) -> wasmtime_wasi::StreamResult<usize> { Ok(1024) }
307}
308
309#[wasmtime_wasi::async_trait]
310impl wasmtime_wasi::Subscribe for ErrorStream {
311    async fn ready(&mut self) {}
312}
313
314struct LogStream(String, tracing::Level);
315
316impl wasmtime_wasi::StdoutStream for LogStream {
317    fn stream(&self) -> Box<dyn wasmtime_wasi::HostOutputStream> {
318        if self.1 == tracing::Level::INFO {
319            Box::new(InfoStream(self.0.clone()))
320        } else {
321            Box::new(ErrorStream(self.0.clone()))
322        }
323    }
324
325    fn isatty(&self) -> bool { true }
326}
327
328impl PluginModule {
329    /// This function takes bytes from a WASM File and compile them
330    pub fn new(name: String, wasm_data: &[u8]) -> Result<Self, PluginModuleError> {
331        let ecs = Arc::new(EcsAccessManager::default());
332
333        // configure the wasm runtime
334        let mut config = Config::new();
335        config.wasm_component_model(true);
336
337        let engine = Engine::new(&config).map_err(PluginModuleError::Wasmtime)?;
338        // create a WASI environment (std implementing system calls)
339        let wasi = wasmtime_wasi::WasiCtxBuilder::new()
340            .stdout(LogStream(name.clone(), tracing::Level::INFO))
341            .stderr(LogStream(name.clone(), tracing::Level::ERROR))
342            .build();
343        let host_ctx = WasiHostCtx {
344            preview2_ctx: wasi,
345            preview2_table: wasmtime_wasi::ResourceTable::new(),
346            ecs: Arc::clone(&ecs),
347            registered_commands: HashSet::new(),
348            registered_bodies: HashMap::new(),
349        };
350        // the store contains all data of a wasm instance
351        let mut store = Store::new(&engine, host_ctx);
352
353        // load wasm from binary
354        let module =
355            Component::from_binary(&engine, wasm_data).map_err(PluginModuleError::Wasmtime)?;
356
357        // register WASI and Veloren methods with the runtime
358        let mut linker = Linker::new(&engine);
359        wasmtime_wasi::add_to_linker_sync(&mut linker).map_err(PluginModuleError::Wasmtime)?;
360        Plugin::add_to_linker(&mut linker, |x| x).map_err(PluginModuleError::Wasmtime)?;
361
362        let instance_fut = linker.instantiate(&mut store, &module);
363        let instance = (instance_fut).map_err(PluginModuleError::Wasmtime)?;
364
365        let plugin = match Plugin::new(&mut store, &instance) {
366            Ok(pl) => Ok(PluginWrapper::Full(pl)),
367            Err(_) => match animation_plugin::AnimationPlugin::new(&mut store, &instance) {
368                Ok(pl) => Ok(PluginWrapper::Animation(pl)),
369                Err(_) => server_plugin::ServerPlugin::new(&mut store, &instance)
370                    .map(PluginWrapper::Server),
371            },
372        }
373        .map_err(PluginModuleError::Wasmtime)?;
374
375        Ok(Self {
376            plugin,
377            ecs,
378            store: store.into(),
379            name,
380        })
381    }
382
383    pub fn name(&self) -> &str { &self.name }
384
385    // Implementation of the commands called from veloren and provided in plugins
386    pub fn load_event(
387        &mut self,
388        ecs: &EcsWorld,
389        mode: common::resources::GameMode,
390    ) -> Result<(), PluginModuleError> {
391        self.ecs
392            .execute_with(ecs, || {
393                self.plugin.load_event(self.store.get_mut().unwrap(), mode)
394            })
395            .map_err(PluginModuleError::Wasmtime)
396    }
397
398    pub fn command_event(
399        &mut self,
400        ecs: &EcsWorld,
401        name: &str,
402        args: &[String],
403        player: common::uid::Uid,
404    ) -> Result<Vec<String>, CommandResults> {
405        if !self
406            .store
407            .get_mut()
408            .unwrap()
409            .data()
410            .registered_commands
411            .contains(name)
412        {
413            return Err(CommandResults::UnknownCommand);
414        }
415        self.ecs.execute_with(ecs, || {
416            match self
417                .plugin
418                .command_event(self.store.get_mut().unwrap(), name, args, player.0)
419            {
420                Err(err) => Err(CommandResults::HostError(err)),
421                Ok(result) => result.map_err(CommandResults::PluginError),
422            }
423        })
424    }
425
426    pub fn player_join_event(
427        &mut self,
428        ecs: &EcsWorld,
429        name: &str,
430        uuid: common::uuid::Uuid,
431    ) -> types::JoinResult {
432        self.ecs.execute_with(ecs, || {
433            match self.plugin.player_join_event(
434                self.store.get_mut().unwrap(),
435                name,
436                uuid.as_u64_pair(),
437            ) {
438                Ok(value) => {
439                    tracing::info!("JoinResult {value:?}");
440                    value
441                },
442                Err(err) => {
443                    tracing::error!("join_event: {err:?}");
444                    types::JoinResult::None
445                },
446            }
447        })
448    }
449
450    pub fn create_body(&mut self, bodytype: &str) -> Option<animation::Body> {
451        let store = self.store.get_mut().unwrap();
452        let bodytype = store.data().registered_bodies.get(bodytype).copied();
453        bodytype.and_then(|bd| self.plugin.create_body(store, bd))
454    }
455
456    pub fn update_skeleton(
457        &mut self,
458        body: &animation::Body,
459        dep: &types::Dependency,
460        time: f32,
461    ) -> Option<types::Skeleton> {
462        self.plugin
463            .update_skeleton(self.store.get_mut().unwrap(), *body, *dep, time)
464    }
465}