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