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
76enum 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
177pub 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 pub fn new(name: String, wasm_data: &[u8]) -> Result<Self, PluginModuleError> {
366 let ecs = Arc::new(EcsAccessManager::default());
367
368 let mut config = Config::new();
370 config.wasm_component_model(true);
371
372 let engine = Engine::new(&config).map_err(PluginModuleError::Wasmtime)?;
373 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 let mut store = Store::new(&engine, host_ctx);
387
388 let module =
390 Component::from_binary(&engine, wasm_data).map_err(PluginModuleError::Wasmtime)?;
391
392 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 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}