feat: add plugin support
This commit is contained in:
parent
9ca11c175f
commit
a100ba9549
1010
Cargo.lock
generated
1010
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,9 @@ license = "GPL-3.0"
|
||||
pumpkin-protocol = { path = "../Pumpkin/crates/pumpkin-protocol" }
|
||||
pumpkin-util = { path = "../Pumpkin/crates/pumpkin-util" }
|
||||
pumpkin-command = { path = "../Pumpkin/crates/pumpkin-command" }
|
||||
pumpkin-plugin-runtime = { path = "../Pumpkin/crates/pumpkin-plugin-runtime" }
|
||||
wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "f6b3140dbb1d9b436ef523d14740292c956b0364", default-features = false, features = ["runtime", "component-model", "component-model-async", "async", "cache", "cranelift", "gc", "gc-drc", "threads", "std"] }
|
||||
wasmtime-wasi = { git = "https://github.com/bytecodealliance/wasmtime", rev = "f6b3140dbb1d9b436ef523d14740292c956b0364", default-features = false, features = ["p2", "p3"] }
|
||||
|
||||
tokio = { version = "1.53", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
||||
@ -17,7 +17,9 @@ use pumpkin_protocol::java::packet_decoder::TCPNetworkDecoder;
|
||||
use pumpkin_protocol::java::packet_encoder::TCPNetworkEncoder;
|
||||
use pumpkin_protocol::java::server::handshake::SHandShake;
|
||||
use pumpkin_protocol::java::server::login::{SLoginPluginResponse, SLoginStart};
|
||||
use pumpkin_protocol::java::server::play::{SChatCommand, SCommandSuggestion};
|
||||
use pumpkin_protocol::java::server::play::{
|
||||
SChatCommand, SChatMessage, SCommandSuggestion, SCustomPayload,
|
||||
};
|
||||
use pumpkin_protocol::packet::MultiVersionJavaPacket;
|
||||
use pumpkin_protocol::ser::{NetworkReadExt, NetworkReadSliceExt};
|
||||
use pumpkin_protocol::{ClientPacket, ConnectionState, ServerPacket};
|
||||
@ -37,6 +39,11 @@ use crate::backend::forwarding::{
|
||||
use crate::command::{CommandDispatcher, CommandSender, ProxyCommandSource};
|
||||
use crate::config::{BackendConfig, Config, ForwardingMode, NetworkConfig};
|
||||
use crate::network;
|
||||
use crate::plugin::PluginManager;
|
||||
use crate::plugin::api::{
|
||||
PlayerChatEvent, PlayerCommandEvent, PlayerDisconnectEvent, PlayerJoinEvent,
|
||||
PluginMessageEvent, ServerConnectEvent, ServerConnectedEvent,
|
||||
};
|
||||
use crate::security::PacketRateLimiter;
|
||||
use crate::session::{PlayerAction, PlayerSession, SessionManager};
|
||||
|
||||
@ -87,6 +94,7 @@ pub struct BackendBridge {
|
||||
session_manager: Arc<SessionManager>,
|
||||
command_dispatcher: Arc<CommandDispatcher>,
|
||||
proxy_config: Arc<Config>,
|
||||
plugin_manager: Arc<PluginManager>,
|
||||
}
|
||||
|
||||
impl BackendBridge {
|
||||
@ -106,6 +114,7 @@ impl BackendBridge {
|
||||
session_manager: Arc<SessionManager>,
|
||||
command_dispatcher: Arc<CommandDispatcher>,
|
||||
proxy_config: Arc<Config>,
|
||||
plugin_manager: Arc<PluginManager>,
|
||||
) -> Self {
|
||||
Self {
|
||||
server_name,
|
||||
@ -122,6 +131,7 @@ impl BackendBridge {
|
||||
session_manager,
|
||||
command_dispatcher,
|
||||
proxy_config,
|
||||
plugin_manager,
|
||||
}
|
||||
}
|
||||
|
||||
@ -413,8 +423,39 @@ impl BackendBridge {
|
||||
|
||||
let mut current_server_name = self.server_name.clone();
|
||||
|
||||
let mut connect_event = ServerConnectEvent {
|
||||
uuid: self.player.id.to_string(),
|
||||
username: self.player.username.clone(),
|
||||
current_server: None,
|
||||
target_server: current_server_name.clone(),
|
||||
cancelled: false,
|
||||
cancel_reason: None,
|
||||
};
|
||||
self.plugin_manager
|
||||
.fire_server_connect(&mut connect_event)
|
||||
.await;
|
||||
if connect_event.cancelled {
|
||||
let reason = connect_event
|
||||
.cancel_reason
|
||||
.unwrap_or_else(|| "Connection cancelled by plugin".to_string());
|
||||
warn!(
|
||||
"Player '{}' connection to '{}' cancelled by plugin: {}",
|
||||
self.player.username, connect_event.target_server, reason
|
||||
);
|
||||
return Err(BridgeError::VineError(reason));
|
||||
}
|
||||
current_server_name = connect_event.target_server;
|
||||
|
||||
let initial_backend_cfg = if current_server_name == self.server_name {
|
||||
self.config.clone()
|
||||
} else if let Some(cfg) = self.proxy_config.servers.get(¤t_server_name) {
|
||||
cfg.clone()
|
||||
} else {
|
||||
self.config.clone()
|
||||
};
|
||||
|
||||
let (mut backend_decoder, mut backend_encoder, success_bytes) = match self
|
||||
.login_backend(¤t_server_name, &self.config)
|
||||
.login_backend(¤t_server_name, &initial_backend_cfg)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
@ -454,12 +495,31 @@ impl BackendBridge {
|
||||
self.online_players.load(Ordering::Relaxed)
|
||||
);
|
||||
|
||||
let connected_event = ServerConnectedEvent {
|
||||
uuid: self.player.id.to_string(),
|
||||
username: self.player.username.clone(),
|
||||
server_name: current_server_name.clone(),
|
||||
};
|
||||
self.plugin_manager
|
||||
.fire_server_connected(&connected_event)
|
||||
.await;
|
||||
|
||||
let join_event = PlayerJoinEvent {
|
||||
uuid: self.player.id.to_string(),
|
||||
username: self.player.username.clone(),
|
||||
client_ip: self.client_ip.to_string(),
|
||||
initial_server: current_server_name.clone(),
|
||||
};
|
||||
self.plugin_manager.fire_player_join(&join_event).await;
|
||||
|
||||
let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
self.session_manager.register(PlayerSession {
|
||||
username: self.player.username.clone(),
|
||||
uuid: self.player.id,
|
||||
current_server: current_server_name.clone(),
|
||||
action_tx: action_tx.clone(),
|
||||
client_ip: self.client_ip.to_string(),
|
||||
protocol_version: self.client_version.protocol_version() as u32,
|
||||
});
|
||||
|
||||
loop {
|
||||
@ -467,8 +527,8 @@ impl BackendBridge {
|
||||
&self.player.username,
|
||||
&mut client_decoder,
|
||||
&mut client_encoder,
|
||||
backend_decoder,
|
||||
backend_encoder,
|
||||
&mut backend_decoder,
|
||||
&mut backend_encoder,
|
||||
self.max_packets_per_sec,
|
||||
self.client_version,
|
||||
&mut action_rx,
|
||||
@ -476,6 +536,9 @@ impl BackendBridge {
|
||||
self.command_dispatcher.clone(),
|
||||
self.proxy_config.clone(),
|
||||
self.session_manager.clone(),
|
||||
self.plugin_manager.clone(),
|
||||
self.player.id,
|
||||
current_server_name.clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -518,6 +581,32 @@ impl BackendBridge {
|
||||
self.player.username, server_name
|
||||
);
|
||||
|
||||
let mut connect_event = ServerConnectEvent {
|
||||
uuid: self.player.id.to_string(),
|
||||
username: self.player.username.clone(),
|
||||
current_server: Some(current_server_name.clone()),
|
||||
target_server: server_name.clone(),
|
||||
cancelled: false,
|
||||
cancel_reason: None,
|
||||
};
|
||||
self.plugin_manager
|
||||
.fire_server_connect(&mut connect_event)
|
||||
.await;
|
||||
if connect_event.cancelled {
|
||||
let reason = connect_event
|
||||
.cancel_reason
|
||||
.as_deref()
|
||||
.unwrap_or("Server switch cancelled by plugin");
|
||||
let msg = TextComponent::text(format!("§c{}", reason));
|
||||
let chat_msg = CSystemChatMessage::new(&msg, false);
|
||||
if let Ok(bytes) = chat_msg.serialize_packet(&self.client_version) {
|
||||
let _ = client_encoder.write_packet(bytes).await;
|
||||
let _ = client_encoder.flush().await;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let server_name = connect_event.target_server;
|
||||
|
||||
let target_config = match self.proxy_config.servers.get(&server_name) {
|
||||
Some(cfg) => cfg.clone(),
|
||||
None => {
|
||||
@ -552,6 +641,14 @@ impl BackendBridge {
|
||||
"Player '{}' successfully switched to '{}'",
|
||||
self.player.username, current_server_name
|
||||
);
|
||||
let connected_event = ServerConnectedEvent {
|
||||
uuid: self.player.id.to_string(),
|
||||
username: self.player.username.clone(),
|
||||
server_name: current_server_name.clone(),
|
||||
};
|
||||
self.plugin_manager
|
||||
.fire_server_connected(&connected_event)
|
||||
.await;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
@ -695,6 +792,15 @@ impl BackendBridge {
|
||||
}
|
||||
}
|
||||
|
||||
let disconnect_event = PlayerDisconnectEvent {
|
||||
uuid: self.player.id.to_string(),
|
||||
username: self.player.username.clone(),
|
||||
last_server: Some(current_server_name.clone()),
|
||||
};
|
||||
self.plugin_manager
|
||||
.fire_player_disconnect(&disconnect_event)
|
||||
.await;
|
||||
|
||||
self.session_manager.unregister(&self.player.username);
|
||||
self.online_players.fetch_sub(1, Ordering::Relaxed);
|
||||
info!(
|
||||
@ -907,8 +1013,8 @@ impl BackendBridge {
|
||||
username: &str,
|
||||
client_decoder: &mut TCPNetworkDecoder<R1>,
|
||||
client_encoder: &mut TCPNetworkEncoder<W1>,
|
||||
mut backend_decoder: TCPNetworkDecoder<R2>,
|
||||
mut backend_encoder: TCPNetworkEncoder<W2>,
|
||||
backend_decoder: &mut TCPNetworkDecoder<R2>,
|
||||
backend_encoder: &mut TCPNetworkEncoder<W2>,
|
||||
max_packets_per_sec: u32,
|
||||
client_version: JavaMinecraftVersion,
|
||||
action_rx: &mut tokio::sync::mpsc::UnboundedReceiver<PlayerAction>,
|
||||
@ -916,6 +1022,9 @@ impl BackendBridge {
|
||||
command_dispatcher: Arc<CommandDispatcher>,
|
||||
proxy_config: Arc<Config>,
|
||||
session_manager: Arc<SessionManager>,
|
||||
plugin_manager: Arc<PluginManager>,
|
||||
player_uuid: uuid::Uuid,
|
||||
current_server_name: String,
|
||||
) -> Result<TunnelExit, BridgeError>
|
||||
where
|
||||
R1: AsyncRead + Unpin,
|
||||
@ -925,6 +1034,8 @@ impl BackendBridge {
|
||||
{
|
||||
let mut client_rate_limiter = PacketRateLimiter::new(max_packets_per_sec);
|
||||
let chat_command_id = SChatCommand::to_id(client_version);
|
||||
let chat_message_id = SChatMessage::to_id(client_version);
|
||||
let custom_payload_id = SCustomPayload::to_id(client_version);
|
||||
let command_suggestion_id = SCommandSuggestion::to_id(client_version);
|
||||
let command_suggestions_clientbound_id = CCommandSuggestions::to_id(client_version);
|
||||
let commands_packet_id = CCommands::to_id(client_version);
|
||||
@ -967,8 +1078,22 @@ impl BackendBridge {
|
||||
if let Ok(chat_cmd) = SChatCommand::read(&mut payload, &client_version)
|
||||
{
|
||||
let cmd_text = chat_cmd.command;
|
||||
let first_word =
|
||||
cmd_text.split_whitespace().next().unwrap_or(cmd_text);
|
||||
let mut cmd_event = PlayerCommandEvent {
|
||||
uuid: player_uuid.to_string(),
|
||||
username: username.to_string(),
|
||||
command: cmd_text.to_string(),
|
||||
cancelled: false,
|
||||
};
|
||||
plugin_manager.fire_player_command(&mut cmd_event).await;
|
||||
if cmd_event.cancelled {
|
||||
continue;
|
||||
}
|
||||
|
||||
let first_word = cmd_event
|
||||
.command
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.unwrap_or(&cmd_event.command);
|
||||
if command_dispatcher.has_command(first_word) {
|
||||
let source = ProxyCommandSource {
|
||||
sender: CommandSender::Player {
|
||||
@ -978,7 +1103,43 @@ impl BackendBridge {
|
||||
config: proxy_config.clone(),
|
||||
session_manager: session_manager.clone(),
|
||||
};
|
||||
command_dispatcher.handle_command(&source, cmd_text);
|
||||
command_dispatcher.handle_command(&source, &cmd_event.command);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if raw_packet.id == chat_message_id {
|
||||
let mut payload = &raw_packet.payload[..];
|
||||
if let Ok(chat_msg) = SChatMessage::read(&mut payload, &client_version)
|
||||
{
|
||||
let mut chat_event = PlayerChatEvent {
|
||||
uuid: player_uuid.to_string(),
|
||||
username: username.to_string(),
|
||||
message: chat_msg.message.to_string(),
|
||||
cancelled: false,
|
||||
};
|
||||
plugin_manager.fire_player_chat(&mut chat_event).await;
|
||||
if chat_event.cancelled {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if raw_packet.id == custom_payload_id {
|
||||
let mut payload = &raw_packet.payload[..];
|
||||
if let Ok(custom_payload) =
|
||||
SCustomPayload::read(&mut payload, &client_version)
|
||||
{
|
||||
let mut msg_event = PluginMessageEvent {
|
||||
channel: custom_payload.channel.to_string(),
|
||||
data: custom_payload.data.to_vec(),
|
||||
player_uuid: Some(player_uuid.to_string()),
|
||||
server_name: Some(current_server_name.clone()),
|
||||
cancelled: false,
|
||||
};
|
||||
plugin_manager.fire_plugin_message(&mut msg_event).await;
|
||||
if msg_event.cancelled {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@ -1032,9 +1193,8 @@ impl BackendBridge {
|
||||
// 2. Root command name completion (no space, e.g. "/", "/s", "/ser", "/server")
|
||||
if !clean_cmd.contains(' ') {
|
||||
let clean_lower = clean_cmd.to_ascii_lowercase();
|
||||
let permitted = command_dispatcher
|
||||
.raw()
|
||||
.get_all_permitted_commands(&source);
|
||||
let permitted =
|
||||
command_dispatcher.get_all_permitted_commands(&source);
|
||||
let proxy_matches: Vec<(String, Option<String>)> = permitted
|
||||
.into_iter()
|
||||
.filter(|(cmd_name, _)| cmd_name.starts_with(&clean_lower))
|
||||
@ -1171,9 +1331,11 @@ impl BackendBridge {
|
||||
config: proxy_config.clone(),
|
||||
session_manager: session_manager.clone(),
|
||||
};
|
||||
let permitted =
|
||||
command_dispatcher.raw().get_all_permitted_commands(&source);
|
||||
let permitted_refs: Vec<(&str, &str)> = permitted.into_iter().collect();
|
||||
let permitted = command_dispatcher.get_all_permitted_commands(&source);
|
||||
let permitted_refs: Vec<(&str, &str)> = permitted
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.as_str()))
|
||||
.collect();
|
||||
|
||||
if let Some(modified_payload) = inject_proxy_commands(
|
||||
&raw_packet.payload,
|
||||
@ -1225,6 +1387,25 @@ impl BackendBridge {
|
||||
let _ = client_tx.send(bytes);
|
||||
}
|
||||
}
|
||||
PlayerAction::Disconnect(reason) => {
|
||||
let text = TextComponent::text(reason.clone());
|
||||
let disconnect_pkt = CPlayDisconnect::new(&text);
|
||||
if let Ok(bytes) = disconnect_pkt.serialize_packet(&client_version) {
|
||||
let _ = client_tx.send(bytes);
|
||||
}
|
||||
return TunnelExit::BackendDisconnected {
|
||||
reason: Some(reason),
|
||||
};
|
||||
}
|
||||
PlayerAction::PluginMessage { channel, data } => {
|
||||
let custom_payload =
|
||||
pumpkin_protocol::java::client::play::CCustomPayload::new(
|
||||
&channel, &data,
|
||||
);
|
||||
if let Ok(bytes) = custom_payload.serialize_packet(&client_version) {
|
||||
let _ = client_tx.send(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TunnelExit::ClientDisconnected
|
||||
|
||||
@ -25,6 +25,8 @@ use crate::backend::{BackendBridge, Router};
|
||||
use crate::command::CommandDispatcher;
|
||||
use crate::config::Config;
|
||||
use crate::network::PrefixedRead;
|
||||
use crate::plugin::PluginManager;
|
||||
use crate::plugin::api::{PlayerLoginEvent, PlayerPreLoginEvent, ProxyPingEvent};
|
||||
use crate::security::Sanitizer;
|
||||
use crate::session::SessionManager;
|
||||
use crate::status::StatusHandler;
|
||||
@ -55,6 +57,7 @@ pub struct ClientHandler {
|
||||
online_players: Arc<AtomicUsize>,
|
||||
session_manager: Arc<SessionManager>,
|
||||
command_dispatcher: Arc<CommandDispatcher>,
|
||||
plugin_manager: Arc<PluginManager>,
|
||||
}
|
||||
|
||||
impl ClientHandler {
|
||||
@ -69,6 +72,7 @@ impl ClientHandler {
|
||||
online_players: Arc<AtomicUsize>,
|
||||
session_manager: Arc<SessionManager>,
|
||||
command_dispatcher: Arc<CommandDispatcher>,
|
||||
plugin_manager: Arc<PluginManager>,
|
||||
) -> Self {
|
||||
Self {
|
||||
peer_addr,
|
||||
@ -80,6 +84,7 @@ impl ClientHandler {
|
||||
online_players,
|
||||
session_manager,
|
||||
command_dispatcher,
|
||||
plugin_manager,
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,9 +166,29 @@ impl ClientHandler {
|
||||
.map_err(|e| ClientError::DecodeError(e.to_string()))?;
|
||||
|
||||
if raw_status.id == 0x00 {
|
||||
let status_pkt = self
|
||||
.status_handler
|
||||
.create_status_packet(client_version, Some(virtual_host));
|
||||
let initial_motd = self.status_handler.resolve_motd_text(Some(virtual_host));
|
||||
let initial_online = self
|
||||
.online_players
|
||||
.load(std::sync::atomic::Ordering::Relaxed) as u32;
|
||||
let mut ping_event = ProxyPingEvent {
|
||||
client_ip: self.peer_addr.ip().to_string(),
|
||||
protocol_version: client_version.protocol_version() as u32,
|
||||
virtual_host: virtual_host.to_string(),
|
||||
motd: initial_motd,
|
||||
max_players: self.config.motd.max_players,
|
||||
online_players: initial_online,
|
||||
version_name: self.config.motd.version_name.clone(),
|
||||
};
|
||||
self.plugin_manager.fire_proxy_ping(&mut ping_event).await;
|
||||
|
||||
let status_pkt = self.status_handler.create_custom_status_packet(
|
||||
client_version,
|
||||
&ping_event.motd,
|
||||
ping_event.max_players,
|
||||
ping_event.online_players,
|
||||
&ping_event.version_name,
|
||||
Some(virtual_host),
|
||||
);
|
||||
let status_bytes = status_pkt
|
||||
.serialize_packet(&client_version)
|
||||
.map_err(|e| ClientError::EncodeError(e.to_string()))?;
|
||||
@ -215,6 +240,30 @@ impl ClientHandler {
|
||||
|
||||
let username = login_start.name.to_string();
|
||||
|
||||
let mut pre_login_event = PlayerPreLoginEvent {
|
||||
client_ip: client_ip.to_string(),
|
||||
username: username.clone(),
|
||||
protocol_version: client_version.protocol_version() as u32,
|
||||
virtual_host: virtual_host.clone(),
|
||||
cancelled: false,
|
||||
cancel_reason: None,
|
||||
};
|
||||
self.plugin_manager
|
||||
.fire_player_pre_login(&mut pre_login_event)
|
||||
.await;
|
||||
if pre_login_event.cancelled {
|
||||
let reason = pre_login_event
|
||||
.cancel_reason
|
||||
.as_deref()
|
||||
.unwrap_or("Connection cancelled by plugin");
|
||||
warn!(
|
||||
"[{}] Player '{}' pre-login cancelled by plugin: {}",
|
||||
self.peer_addr, username, reason
|
||||
);
|
||||
Self::kick(&mut encoder, client_version, reason).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.config.security.validate_usernames && !Sanitizer::is_valid_username(&username) {
|
||||
warn!(
|
||||
"[{}] Rejecting invalid username '{}' (failed sanitization)",
|
||||
@ -347,6 +396,29 @@ impl ClientHandler {
|
||||
AuthenticatedPlayer::offline(&username)
|
||||
};
|
||||
|
||||
let mut login_event = PlayerLoginEvent {
|
||||
uuid: authenticated_player.id.to_string(),
|
||||
username: authenticated_player.username.clone(),
|
||||
client_ip: client_ip.to_string(),
|
||||
cancelled: false,
|
||||
cancel_reason: None,
|
||||
};
|
||||
self.plugin_manager
|
||||
.fire_player_login(&mut login_event)
|
||||
.await;
|
||||
if login_event.cancelled {
|
||||
let reason = login_event
|
||||
.cancel_reason
|
||||
.as_deref()
|
||||
.unwrap_or("Login cancelled by plugin");
|
||||
warn!(
|
||||
"[{}] Player '{}' login cancelled by plugin: {}",
|
||||
self.peer_addr, authenticated_player.username, reason
|
||||
);
|
||||
Self::kick(&mut encoder, client_version, reason).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let comp_threshold = self.config.server.compression_threshold;
|
||||
if comp_threshold > 0 {
|
||||
let set_comp = CSetCompression::new(VarInt(comp_threshold));
|
||||
@ -416,6 +488,7 @@ impl ClientHandler {
|
||||
self.session_manager.clone(),
|
||||
self.command_dispatcher.clone(),
|
||||
self.config.clone(),
|
||||
self.plugin_manager.clone(),
|
||||
);
|
||||
|
||||
bridge.run(decoder, encoder).await?;
|
||||
|
||||
@ -158,12 +158,16 @@ mod tests {
|
||||
uuid: Uuid::new_v4(),
|
||||
current_server: "lobby".to_string(),
|
||||
action_tx: tx1,
|
||||
client_ip: "127.0.0.1".to_string(),
|
||||
protocol_version: 765,
|
||||
});
|
||||
session_manager.register(PlayerSession {
|
||||
username: "Alex".to_string(),
|
||||
uuid: Uuid::new_v4(),
|
||||
current_server: "survival".to_string(),
|
||||
action_tx: tx2,
|
||||
client_ip: "127.0.0.1".to_string(),
|
||||
protocol_version: 765,
|
||||
});
|
||||
|
||||
let (action_tx, mut action_rx) = mpsc::unbounded_channel();
|
||||
|
||||
@ -215,9 +215,11 @@ impl CommandExecutor<ProxyCommandSource> for ListCommandExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
use std::sync::RwLock;
|
||||
|
||||
/// Central registry and dispatcher for commands using `pumpkin-command`
|
||||
pub struct CommandDispatcher {
|
||||
inner: RawCommandDispatcher<ProxyCommandSource>,
|
||||
inner: RwLock<RawCommandDispatcher<ProxyCommandSource>>,
|
||||
}
|
||||
|
||||
impl Default for CommandDispatcher {
|
||||
@ -246,18 +248,36 @@ impl CommandDispatcher {
|
||||
.executes(CommandSpecificHelpExecutor),
|
||||
),
|
||||
);
|
||||
Self { inner: dispatcher }
|
||||
Self {
|
||||
inner: RwLock::new(dispatcher),
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a new command builder into the dispatcher
|
||||
pub fn register(
|
||||
&self,
|
||||
builder: pumpkin_command::argument_builder::CommandArgumentBuilder<ProxyCommandSource>,
|
||||
) {
|
||||
if let Ok(mut lock) = self.inner.write() {
|
||||
lock.register(builder);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if a command with the given name is registered
|
||||
pub fn has_command(&self, name: &str) -> bool {
|
||||
let clean = name.trim_start_matches('/').to_ascii_lowercase();
|
||||
self.inner.has_command(&clean)
|
||||
if let Ok(lock) = self.inner.read() {
|
||||
lock.has_command(&clean)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispatches a command for execution, reporting any error messages back to the source
|
||||
pub fn handle_command(&self, source: &ProxyCommandSource, input: &str) {
|
||||
self.inner.handle_command(source, input);
|
||||
if let Ok(lock) = self.inner.read() {
|
||||
lock.handle_command(source, input);
|
||||
}
|
||||
}
|
||||
|
||||
/// Executes raw input directly, returning the execution result
|
||||
@ -268,7 +288,23 @@ impl CommandDispatcher {
|
||||
) -> Result<i32, CommandSyntaxError> {
|
||||
let trimmed = input.trim();
|
||||
let clean = trimmed.strip_prefix('/').unwrap_or(trimmed);
|
||||
self.inner.execute_input(clean, source)
|
||||
if let Ok(lock) = self.inner.read() {
|
||||
lock.execute_input(clean, source)
|
||||
} else {
|
||||
Err(command_error("Command dispatcher lock poisoned"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns all permitted commands for the given source
|
||||
pub fn get_all_permitted_commands(&self, source: &ProxyCommandSource) -> Vec<(String, String)> {
|
||||
if let Ok(lock) = self.inner.read() {
|
||||
lock.get_all_permitted_commands(source)
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns suggestions with range information for any command input.
|
||||
@ -278,14 +314,16 @@ impl CommandDispatcher {
|
||||
let clean = input.strip_prefix('/').unwrap_or(input);
|
||||
if clean.contains(' ') {
|
||||
let first_word = clean.split_whitespace().next().unwrap_or(clean);
|
||||
if self.has_command(first_word) {
|
||||
return self.inner.suggest_with_range(clean, source);
|
||||
if self.has_command(first_word)
|
||||
&& let Ok(lock) = self.inner.read()
|
||||
{
|
||||
return lock.suggest_with_range(clean, source);
|
||||
}
|
||||
return Suggestions::empty();
|
||||
}
|
||||
|
||||
let clean_lower = clean.to_ascii_lowercase();
|
||||
let permitted = self.inner.get_all_permitted_commands(source);
|
||||
let permitted = self.get_all_permitted_commands(source);
|
||||
let mut suggestions = Vec::new();
|
||||
for (cmd_name, description) in permitted {
|
||||
if cmd_name.starts_with(&clean_lower) {
|
||||
@ -316,16 +354,6 @@ impl CommandDispatcher {
|
||||
pub fn suggest_with_range(&self, input: &str, source: &ProxyCommandSource) -> Suggestions {
|
||||
self.suggest_command(input, source)
|
||||
}
|
||||
|
||||
/// Exposes the inner pumpkin command dispatcher
|
||||
pub fn raw(&self) -> &RawCommandDispatcher<ProxyCommandSource> {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Exposes a mutable reference to the inner pumpkin command dispatcher
|
||||
pub fn raw_mut(&mut self) -> &mut RawCommandDispatcher<ProxyCommandSource> {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@ -205,6 +205,8 @@ mod tests {
|
||||
uuid: Uuid::new_v4(),
|
||||
current_server: "lobby".to_string(),
|
||||
action_tx: tx,
|
||||
client_ip: "127.0.0.1".to_string(),
|
||||
protocol_version: 765,
|
||||
});
|
||||
|
||||
let source = ProxyCommandSource {
|
||||
@ -333,6 +335,8 @@ mod tests {
|
||||
uuid: Uuid::new_v4(),
|
||||
current_server: "lobby".to_string(),
|
||||
action_tx: tx,
|
||||
client_ip: "127.0.0.1".to_string(),
|
||||
protocol_version: 765,
|
||||
});
|
||||
|
||||
let source = ProxyCommandSource {
|
||||
|
||||
@ -182,6 +182,8 @@ mod tests {
|
||||
uuid: Uuid::new_v4(),
|
||||
current_server: "lobby".to_string(),
|
||||
action_tx: action_tx.clone(),
|
||||
client_ip: "127.0.0.1".to_string(),
|
||||
protocol_version: 765,
|
||||
};
|
||||
session_manager.register(session);
|
||||
|
||||
|
||||
@ -60,6 +60,29 @@ pub struct Config {
|
||||
pub bedrock: crate::bedrock::BedrockConfig,
|
||||
#[serde(default)]
|
||||
pub permissions: PermissionsConfig,
|
||||
#[serde(default)]
|
||||
pub plugins: PluginConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PluginConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_plugins_dir")]
|
||||
pub plugin_dir: String,
|
||||
}
|
||||
|
||||
fn default_plugins_dir() -> String {
|
||||
"./plugins".to_string()
|
||||
}
|
||||
|
||||
impl Default for PluginConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
plugin_dir: default_plugins_dir(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@ -361,6 +384,7 @@ impl Default for Config {
|
||||
telemetry: TelemetryConfig::default(),
|
||||
bedrock: crate::bedrock::BedrockConfig::default(),
|
||||
permissions: PermissionsConfig::default(),
|
||||
plugins: PluginConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ pub mod command;
|
||||
pub mod config;
|
||||
pub mod network;
|
||||
pub mod permissions;
|
||||
pub mod plugin;
|
||||
pub mod proxy;
|
||||
pub mod security;
|
||||
pub mod session;
|
||||
|
||||
151
src/plugin/api.rs
Normal file
151
src/plugin/api.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use std::future::Future;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub type PluginFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PluginMetadata {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
pub authors: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
pub dependencies: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum EventPriority {
|
||||
Lowest = 0,
|
||||
Low = 1,
|
||||
Normal = 2,
|
||||
High = 3,
|
||||
Highest = 4,
|
||||
Monitor = 5,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum EventType {
|
||||
ProxyPing,
|
||||
PlayerPreLogin,
|
||||
PlayerLogin,
|
||||
PlayerJoin,
|
||||
ServerConnect,
|
||||
ServerConnected,
|
||||
PlayerDisconnect,
|
||||
PlayerChat,
|
||||
PlayerCommand,
|
||||
PluginMessage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProxyPingEvent {
|
||||
pub client_ip: String,
|
||||
pub protocol_version: u32,
|
||||
pub virtual_host: String,
|
||||
pub motd: String,
|
||||
pub max_players: u32,
|
||||
pub online_players: u32,
|
||||
pub version_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlayerPreLoginEvent {
|
||||
pub client_ip: String,
|
||||
pub username: String,
|
||||
pub protocol_version: u32,
|
||||
pub virtual_host: String,
|
||||
pub cancelled: bool,
|
||||
pub cancel_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlayerLoginEvent {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub client_ip: String,
|
||||
pub cancelled: bool,
|
||||
pub cancel_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlayerJoinEvent {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub client_ip: String,
|
||||
pub initial_server: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerConnectEvent {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub current_server: Option<String>,
|
||||
pub target_server: String,
|
||||
pub cancelled: bool,
|
||||
pub cancel_reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerConnectedEvent {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub server_name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlayerDisconnectEvent {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub last_server: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlayerChatEvent {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub message: String,
|
||||
pub cancelled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PlayerCommandEvent {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub command: String,
|
||||
pub cancelled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PluginMessageEvent {
|
||||
pub channel: String,
|
||||
pub data: Vec<u8>,
|
||||
pub player_uuid: Option<String>,
|
||||
pub server_name: Option<String>,
|
||||
pub cancelled: bool,
|
||||
}
|
||||
|
||||
pub struct PluginContext {
|
||||
pub metadata: PluginMetadata,
|
||||
pub data_folder: PathBuf,
|
||||
pub registered_commands: Mutex<Vec<u32>>,
|
||||
pub registered_events: Mutex<Vec<(u32, EventType, EventPriority)>>,
|
||||
}
|
||||
|
||||
impl PluginContext {
|
||||
#[must_use]
|
||||
pub fn new(metadata: PluginMetadata, data_folder: PathBuf) -> Self {
|
||||
Self {
|
||||
metadata,
|
||||
data_folder,
|
||||
registered_commands: Mutex::new(Vec::new()),
|
||||
registered_events: Mutex::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Plugin: Send + Sync {
|
||||
fn on_load(&self, context: Arc<PluginContext>) -> PluginFuture<'_, Result<(), String>>;
|
||||
fn on_unload(&self, context: Arc<PluginContext>) -> PluginFuture<'_, Result<(), String>>;
|
||||
}
|
||||
1
src/plugin/loader/mod.rs
Normal file
1
src/plugin/loader/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod wasm;
|
||||
34
src/plugin/loader/wasm/mod.rs
Normal file
34
src/plugin/loader/wasm/mod.rs
Normal file
@ -0,0 +1,34 @@
|
||||
pub mod wasm_host;
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::plugin::api::PluginMetadata;
|
||||
use wasm_host::state::ProxyPluginContext;
|
||||
pub use wasm_host::{PluginRuntime, WasmPlugin};
|
||||
|
||||
pub struct WasmPluginLoader {
|
||||
runtime: PluginRuntime,
|
||||
}
|
||||
|
||||
impl WasmPluginLoader {
|
||||
pub fn new() -> Result<Self, wasm_host::PluginInitError> {
|
||||
let runtime = PluginRuntime::new()?;
|
||||
Ok(Self { runtime })
|
||||
}
|
||||
|
||||
pub fn can_load(&self, path: &Path) -> bool {
|
||||
path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.map(|ext| ext.eq_ignore_ascii_case("wasm"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub async fn load(
|
||||
&self,
|
||||
path: &Path,
|
||||
proxy: Option<ProxyPluginContext>,
|
||||
) -> Result<(Arc<WasmPlugin>, PluginMetadata), wasm_host::PluginInitError> {
|
||||
self.runtime.init_plugin(path, proxy).await
|
||||
}
|
||||
}
|
||||
41
src/plugin/loader/wasm/wasm_host/concurrent_store.rs
Normal file
41
src/plugin/loader/wasm/wasm_host/concurrent_store.rs
Normal file
@ -0,0 +1,41 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use pumpkin_plugin_runtime::{RuntimeSpawner, SpawnError, SpawnFuture, StoreExecutor};
|
||||
use wasmtime::Store;
|
||||
|
||||
use super::state::PluginHostState;
|
||||
|
||||
pub use pumpkin_plugin_runtime::LegacySyncReentry;
|
||||
|
||||
pub type LegacyStore = StoreExecutor<PluginHostState, LegacySyncReentry>;
|
||||
|
||||
pub struct TokioSpawner {
|
||||
runtime: tokio::runtime::Handle,
|
||||
}
|
||||
|
||||
impl TokioSpawner {
|
||||
#[must_use]
|
||||
pub const fn new(runtime: tokio::runtime::Handle) -> Self {
|
||||
Self { runtime }
|
||||
}
|
||||
}
|
||||
|
||||
impl RuntimeSpawner for TokioSpawner {
|
||||
fn spawn(&self, task: SpawnFuture) -> Result<(), SpawnError> {
|
||||
drop(self.runtime.spawn(task));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn spawn_blocking(&self, task: Box<dyn FnOnce() + Send + 'static>) -> Result<(), SpawnError> {
|
||||
drop(self.runtime.spawn_blocking(task));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_legacy_store(
|
||||
store: Store<PluginHostState>,
|
||||
policy: LegacySyncReentry,
|
||||
spawner: Arc<dyn RuntimeSpawner>,
|
||||
) -> wasmtime::Result<LegacyStore> {
|
||||
StoreExecutor::start(store, policy, spawner).await
|
||||
}
|
||||
267
src/plugin/loader/wasm/wasm_host/mod.rs
Normal file
267
src/plugin/loader/wasm/wasm_host/mod.rs
Normal file
@ -0,0 +1,267 @@
|
||||
pub mod concurrent_store;
|
||||
pub mod state;
|
||||
pub mod wit;
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use wasmtime::component::{Component, HasSelf, Linker};
|
||||
use wasmtime::{Engine, Store};
|
||||
|
||||
use crate::plugin::api::{Plugin, PluginContext, PluginFuture, PluginMetadata};
|
||||
use concurrent_store::{LegacyStore, LegacySyncReentry, TokioSpawner};
|
||||
use state::{PluginHostState, ProxyPluginContext};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PluginInitError {
|
||||
#[error("Failed to read plugin file '{0}': {1}")]
|
||||
FileReadFailed(PathBuf, std::io::Error),
|
||||
#[error("Failed to compile WebAssembly component: {0}")]
|
||||
ComponentCompilationFailed(wasmtime::Error),
|
||||
#[error("Failed to setup linker: {0}")]
|
||||
LinkerSetupFailed(wasmtime::Error),
|
||||
#[error("Failed to instantiate plugin: {0}")]
|
||||
InstantiationFailed(wasmtime::Error),
|
||||
#[error("Failed to initialize plugin: {0}")]
|
||||
InitPluginFailed(wasmtime::Error),
|
||||
#[error("Failed to retrieve metadata: {0}")]
|
||||
GetMetadataFailed(wasmtime::Error),
|
||||
#[error("Engine creation failed: {0}")]
|
||||
EngineCreationFailed(wasmtime::Error),
|
||||
}
|
||||
|
||||
pub fn setup_linker(engine: &Engine) -> wasmtime::Result<Linker<PluginHostState>> {
|
||||
let mut linker = Linker::<PluginHostState>::new(engine);
|
||||
wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
|
||||
wit::Plugin::add_to_linker::<_, HasSelf<_>>(&mut linker, |state: &mut PluginHostState| state)?;
|
||||
Ok(linker)
|
||||
}
|
||||
|
||||
pub struct WasmPlugin {
|
||||
pub instance: wit::Plugin,
|
||||
pub store: LegacyStore,
|
||||
pub metadata: PluginMetadata,
|
||||
}
|
||||
|
||||
impl WasmPlugin {
|
||||
pub async fn on_load(&self, context: Arc<PluginContext>) -> Result<(), String> {
|
||||
let func = self.instance.func_on_load();
|
||||
let context_clone = context.clone();
|
||||
self.store
|
||||
.call_guest(move |mut guest| {
|
||||
Box::pin(async move {
|
||||
let context_res =
|
||||
guest.with(|mut store| store.data_mut().add_context(context_clone))?;
|
||||
let (res,) = guest.call(func, (context_res,)).await?;
|
||||
Ok(res)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub async fn on_unload(&self, context: Arc<PluginContext>) -> Result<(), String> {
|
||||
let func = self.instance.func_on_unload();
|
||||
let context_clone = context.clone();
|
||||
self.store
|
||||
.call_guest(move |mut guest| {
|
||||
Box::pin(async move {
|
||||
let context_res =
|
||||
guest.with(|mut store| store.data_mut().add_context(context_clone))?;
|
||||
let (res,) = guest.call(func, (context_res,)).await?;
|
||||
Ok(res)
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
}
|
||||
|
||||
pub async fn handle_event(
|
||||
&self,
|
||||
handler_id: u32,
|
||||
event: wit::vine::plugin::event::Event,
|
||||
) -> Result<wit::vine::plugin::event::Event, wasmtime::Error> {
|
||||
let func = self.instance.func_handle_event();
|
||||
self.store
|
||||
.call_guest(move |mut guest| {
|
||||
Box::pin(async move {
|
||||
let (res,) = guest.call(func, (handler_id, event)).await?;
|
||||
Ok(res)
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_command(
|
||||
&self,
|
||||
command_id: u32,
|
||||
sender: wit::vine::plugin::types::CommandSender,
|
||||
args: Vec<String>,
|
||||
) -> Result<Result<i32, String>, wasmtime::Error> {
|
||||
let func = self.instance.func_handle_command();
|
||||
self.store
|
||||
.call_guest(move |mut guest| {
|
||||
Box::pin(async move {
|
||||
let (res,) = guest.call(func, (command_id, sender, args)).await?;
|
||||
Ok(res)
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn handle_command_suggestion(
|
||||
&self,
|
||||
command_id: u32,
|
||||
sender: wit::vine::plugin::types::CommandSender,
|
||||
args: Vec<String>,
|
||||
) -> Result<Vec<String>, wasmtime::Error> {
|
||||
let func = self.instance.func_handle_command_suggestion();
|
||||
self.store
|
||||
.call_guest(move |mut guest| {
|
||||
Box::pin(async move {
|
||||
let (res,) = guest.call(func, (command_id, sender, args)).await?;
|
||||
Ok(res)
|
||||
})
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl Plugin for WasmPlugin {
|
||||
fn on_load(&self, context: Arc<PluginContext>) -> PluginFuture<'_, Result<(), String>> {
|
||||
Box::pin(async move { Self::on_load(self, context).await })
|
||||
}
|
||||
|
||||
fn on_unload(&self, context: Arc<PluginContext>) -> PluginFuture<'_, Result<(), String>> {
|
||||
Box::pin(async move { Self::on_unload(self, context).await })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PluginRuntime {
|
||||
engine: Engine,
|
||||
linker: Linker<PluginHostState>,
|
||||
reentry: LegacySyncReentry,
|
||||
spawner: Arc<TokioSpawner>,
|
||||
}
|
||||
|
||||
impl PluginRuntime {
|
||||
pub fn new() -> Result<Self, PluginInitError> {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.wasm_component_model(true);
|
||||
config.wasm_component_model_async(true);
|
||||
config.concurrency_support(true);
|
||||
|
||||
let engine = Engine::new(&config).map_err(PluginInitError::EngineCreationFailed)?;
|
||||
let linker = setup_linker(&engine).map_err(PluginInitError::LinkerSetupFailed)?;
|
||||
let reentry = LegacySyncReentry::new();
|
||||
let spawner = Arc::new(TokioSpawner::new(tokio::runtime::Handle::current()));
|
||||
|
||||
Ok(Self {
|
||||
engine,
|
||||
linker,
|
||||
reentry,
|
||||
spawner,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn init_plugin(
|
||||
&self,
|
||||
path: &Path,
|
||||
proxy: Option<ProxyPluginContext>,
|
||||
) -> Result<(Arc<WasmPlugin>, PluginMetadata), PluginInitError> {
|
||||
let wasm_bytes =
|
||||
fs::read(path).map_err(|e| PluginInitError::FileReadFailed(path.to_path_buf(), e))?;
|
||||
|
||||
let component = Component::new(&self.engine, &wasm_bytes)
|
||||
.map_err(PluginInitError::ComponentCompilationFailed)?;
|
||||
|
||||
let instance_pre = self
|
||||
.linker
|
||||
.instantiate_pre(&component)
|
||||
.map_err(PluginInitError::InstantiationFailed)?;
|
||||
|
||||
let plugin_pre =
|
||||
wit::PluginPre::new(instance_pre).map_err(PluginInitError::InstantiationFailed)?;
|
||||
|
||||
let filename = path
|
||||
.file_stem()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let data_folder = PathBuf::from("plugins").join("data").join(&filename);
|
||||
let _ = fs::create_dir_all(&data_folder);
|
||||
|
||||
let mut store = Store::new(
|
||||
&self.engine,
|
||||
PluginHostState::new(filename, data_folder, proxy),
|
||||
);
|
||||
store.limiter(|state| &mut state.limits);
|
||||
|
||||
let plugin = self
|
||||
.reentry
|
||||
.scope_bootstrap(plugin_pre.instantiate_async(&mut store))
|
||||
.await
|
||||
.map_err(PluginInitError::InstantiationFailed)?;
|
||||
|
||||
store
|
||||
.run_concurrent(async |accessor| {
|
||||
self.reentry
|
||||
.scope_bootstrap(plugin.call_init_plugin(accessor))
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(PluginInitError::InitPluginFailed)?
|
||||
.map_err(PluginInitError::InitPluginFailed)?;
|
||||
|
||||
let raw_meta = store
|
||||
.run_concurrent(async |accessor| {
|
||||
self.reentry
|
||||
.scope_bootstrap(plugin.call_get_metadata(accessor))
|
||||
.await
|
||||
})
|
||||
.await
|
||||
.map_err(PluginInitError::GetMetadataFailed)?
|
||||
.map_err(PluginInitError::GetMetadataFailed)?;
|
||||
|
||||
let metadata = PluginMetadata {
|
||||
name: raw_meta.name.clone(),
|
||||
version: raw_meta.version,
|
||||
authors: raw_meta.authors,
|
||||
description: raw_meta.description,
|
||||
dependencies: raw_meta.dependencies,
|
||||
};
|
||||
|
||||
store.data_mut().plugin_name = raw_meta.name;
|
||||
|
||||
let legacy_store = concurrent_store::start_legacy_store(
|
||||
store,
|
||||
self.reentry.clone(),
|
||||
Arc::clone(&self.spawner) as Arc<dyn pumpkin_plugin_runtime::RuntimeSpawner>,
|
||||
)
|
||||
.await
|
||||
.map_err(PluginInitError::InstantiationFailed)?;
|
||||
|
||||
let wasm_plugin = Arc::new(WasmPlugin {
|
||||
instance: plugin,
|
||||
store: legacy_store,
|
||||
metadata: metadata.clone(),
|
||||
});
|
||||
|
||||
let weak = Arc::downgrade(&wasm_plugin);
|
||||
wasm_plugin
|
||||
.store
|
||||
.call(move |accessor| {
|
||||
Box::pin(async move {
|
||||
accessor.with(|mut store| {
|
||||
store.data_mut().plugin = Some(weak);
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(PluginInitError::InstantiationFailed)?;
|
||||
|
||||
Ok((wasm_plugin, metadata))
|
||||
}
|
||||
}
|
||||
438
src/plugin/loader/wasm/wasm_host/state.rs
Normal file
438
src/plugin/loader/wasm/wasm_host/state.rs
Normal file
@ -0,0 +1,438 @@
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use wasmtime::component::{Resource, ResourceTable};
|
||||
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};
|
||||
|
||||
use crate::command::CommandDispatcher;
|
||||
use crate::config::Config;
|
||||
use crate::plugin::PluginManager;
|
||||
use crate::plugin::api::{EventPriority, EventType, PluginContext};
|
||||
use crate::plugin::loader::wasm::wasm_host::WasmPlugin;
|
||||
use crate::session::{PlayerAction, SessionManager};
|
||||
|
||||
use super::wit;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProxyPluginContext {
|
||||
pub config: Arc<Config>,
|
||||
pub session_manager: Arc<SessionManager>,
|
||||
pub command_dispatcher: Arc<CommandDispatcher>,
|
||||
pub plugin_manager: Arc<PluginManager>,
|
||||
}
|
||||
|
||||
pub struct ContextResource {
|
||||
pub context: Arc<PluginContext>,
|
||||
}
|
||||
|
||||
pub struct PluginHostState {
|
||||
pub wasi_ctx: WasiCtx,
|
||||
pub resource_table: ResourceTable,
|
||||
pub limits: wasmtime::StoreLimits,
|
||||
pub proxy: Option<ProxyPluginContext>,
|
||||
pub plugin_name: String,
|
||||
pub data_folder: PathBuf,
|
||||
pub plugin: Option<Weak<WasmPlugin>>,
|
||||
}
|
||||
|
||||
impl Default for PluginHostState {
|
||||
fn default() -> Self {
|
||||
Self::new(String::new(), PathBuf::new(), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginHostState {
|
||||
#[must_use]
|
||||
pub fn new(
|
||||
plugin_name: String,
|
||||
data_folder: PathBuf,
|
||||
proxy: Option<ProxyPluginContext>,
|
||||
) -> Self {
|
||||
Self {
|
||||
wasi_ctx: WasiCtxBuilder::new()
|
||||
.inherit_stdout()
|
||||
.inherit_stderr()
|
||||
.build(),
|
||||
resource_table: ResourceTable::new(),
|
||||
limits: wasmtime::StoreLimitsBuilder::new().build(),
|
||||
proxy,
|
||||
plugin_name,
|
||||
data_folder,
|
||||
plugin: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_context(
|
||||
&mut self,
|
||||
context: Arc<PluginContext>,
|
||||
) -> wasmtime::Result<Resource<wit::vine::plugin::context::Context>> {
|
||||
let res = self.resource_table.push(ContextResource { context })?;
|
||||
Ok(Resource::new_own(res.rep()))
|
||||
}
|
||||
|
||||
pub fn get_context(
|
||||
&self,
|
||||
resource: &Resource<wit::vine::plugin::context::Context>,
|
||||
) -> wasmtime::Result<&ContextResource> {
|
||||
Ok(self
|
||||
.resource_table
|
||||
.get(&Resource::new_borrow(resource.rep()))?)
|
||||
}
|
||||
}
|
||||
|
||||
impl WasiView for PluginHostState {
|
||||
fn ctx(&mut self) -> WasiCtxView<'_> {
|
||||
WasiCtxView {
|
||||
ctx: &mut self.wasi_ctx,
|
||||
table: &mut self.resource_table,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl wit::vine::plugin::types::Host for PluginHostState {}
|
||||
impl wit::vine::plugin::metadata::Host for PluginHostState {}
|
||||
impl wit::vine::plugin::event::Host for PluginHostState {}
|
||||
impl wit::vine::plugin::context::Host for PluginHostState {}
|
||||
|
||||
impl wit::vine::plugin::logging::Host for PluginHostState {
|
||||
async fn log(
|
||||
&mut self,
|
||||
level: wit::vine::plugin::logging::Level,
|
||||
message: String,
|
||||
) -> wasmtime::Result<()> {
|
||||
match level {
|
||||
wit::vine::plugin::logging::Level::Trace => {
|
||||
tracing::trace!("[Plugin: {}] {}", self.plugin_name, message)
|
||||
}
|
||||
wit::vine::plugin::logging::Level::Debug => {
|
||||
tracing::debug!("[Plugin: {}] {}", self.plugin_name, message)
|
||||
}
|
||||
wit::vine::plugin::logging::Level::Info => {
|
||||
tracing::info!("[Plugin: {}] {}", self.plugin_name, message)
|
||||
}
|
||||
wit::vine::plugin::logging::Level::Warn => {
|
||||
tracing::warn!("[Plugin: {}] {}", self.plugin_name, message)
|
||||
}
|
||||
wit::vine::plugin::logging::Level::Error => {
|
||||
tracing::error!("[Plugin: {}] {}", self.plugin_name, message)
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl wit::vine::plugin::command::Host for PluginHostState {
|
||||
async fn reply(
|
||||
&mut self,
|
||||
sender: wit::vine::plugin::types::CommandSender,
|
||||
message: String,
|
||||
) -> wasmtime::Result<()> {
|
||||
if let Some(proxy) = &self.proxy {
|
||||
match sender.sender_type {
|
||||
wit::vine::plugin::types::CommandSenderType::Console => {
|
||||
tracing::info!("[Console] {}", message);
|
||||
}
|
||||
wit::vine::plugin::types::CommandSenderType::Player => {
|
||||
if let Some(uuid_str) = sender.player_uuid
|
||||
&& let Ok(target_uuid) = uuid::Uuid::parse_str(&uuid_str)
|
||||
&& let Some(session) = proxy
|
||||
.session_manager
|
||||
.list_sessions()
|
||||
.into_iter()
|
||||
.find(|s| s.uuid == target_uuid)
|
||||
{
|
||||
let _ = session.action_tx.send(PlayerAction::Message(
|
||||
pumpkin_util::text::TextComponent::text(message),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl wit::vine::plugin::proxy::Host for PluginHostState {
|
||||
async fn get_player_count(&mut self) -> wasmtime::Result<u32> {
|
||||
Ok(self
|
||||
.proxy
|
||||
.as_ref()
|
||||
.map(|p| p.session_manager.count() as u32)
|
||||
.unwrap_or(0))
|
||||
}
|
||||
|
||||
async fn get_players(&mut self) -> wasmtime::Result<Vec<wit::vine::plugin::types::PlayerInfo>> {
|
||||
if let Some(proxy) = &self.proxy {
|
||||
let list = proxy
|
||||
.session_manager
|
||||
.list_sessions()
|
||||
.into_iter()
|
||||
.map(|s| wit::vine::plugin::types::PlayerInfo {
|
||||
uuid: s.uuid.to_string(),
|
||||
username: s.username,
|
||||
current_server: s.current_server,
|
||||
client_ip: s.client_ip,
|
||||
protocol_version: s.protocol_version,
|
||||
})
|
||||
.collect();
|
||||
Ok(list)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_player_by_name(
|
||||
&mut self,
|
||||
username: String,
|
||||
) -> wasmtime::Result<Option<wit::vine::plugin::types::PlayerInfo>> {
|
||||
if let Some(proxy) = &self.proxy {
|
||||
let session = proxy.session_manager.get_session(&username);
|
||||
Ok(session.map(|s| wit::vine::plugin::types::PlayerInfo {
|
||||
uuid: s.uuid.to_string(),
|
||||
username: s.username,
|
||||
current_server: s.current_server,
|
||||
client_ip: s.client_ip,
|
||||
protocol_version: s.protocol_version,
|
||||
}))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_player_by_uuid(
|
||||
&mut self,
|
||||
uuid: String,
|
||||
) -> wasmtime::Result<Option<wit::vine::plugin::types::PlayerInfo>> {
|
||||
if let Some(proxy) = &self.proxy
|
||||
&& let Ok(target_uuid) = uuid::Uuid::parse_str(&uuid)
|
||||
{
|
||||
let session = proxy
|
||||
.session_manager
|
||||
.list_sessions()
|
||||
.into_iter()
|
||||
.find(|s| s.uuid == target_uuid);
|
||||
return Ok(session.map(|s| wit::vine::plugin::types::PlayerInfo {
|
||||
uuid: s.uuid.to_string(),
|
||||
username: s.username,
|
||||
current_server: s.current_server,
|
||||
client_ip: s.client_ip,
|
||||
protocol_version: s.protocol_version,
|
||||
}));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn get_servers(&mut self) -> wasmtime::Result<Vec<wit::vine::plugin::types::ServerInfo>> {
|
||||
if let Some(proxy) = &self.proxy {
|
||||
let sessions = proxy.session_manager.list_sessions();
|
||||
let mut list = Vec::new();
|
||||
for (name, srv) in &proxy.config.servers {
|
||||
let player_count = sessions
|
||||
.iter()
|
||||
.filter(|s| s.current_server == *name)
|
||||
.count() as u32;
|
||||
list.push(wit::vine::plugin::types::ServerInfo {
|
||||
name: name.clone(),
|
||||
address: srv.address.clone(),
|
||||
player_count,
|
||||
});
|
||||
}
|
||||
Ok(list)
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
async fn broadcast(&mut self, message: String) -> wasmtime::Result<()> {
|
||||
if let Some(proxy) = &self.proxy {
|
||||
for session in proxy.session_manager.list_sessions() {
|
||||
let _ = session.action_tx.send(PlayerAction::Message(
|
||||
pumpkin_util::text::TextComponent::text(message.clone()),
|
||||
));
|
||||
}
|
||||
tracing::info!("[Broadcast] {}", message);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
&mut self,
|
||||
player_uuid: String,
|
||||
message: String,
|
||||
) -> wasmtime::Result<bool> {
|
||||
if let Some(proxy) = &self.proxy
|
||||
&& let Ok(target_uuid) = uuid::Uuid::parse_str(&player_uuid)
|
||||
&& let Some(session) = proxy
|
||||
.session_manager
|
||||
.list_sessions()
|
||||
.into_iter()
|
||||
.find(|s| s.uuid == target_uuid)
|
||||
{
|
||||
let _ = session.action_tx.send(PlayerAction::Message(
|
||||
pumpkin_util::text::TextComponent::text(message),
|
||||
));
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn disconnect_player(
|
||||
&mut self,
|
||||
player_uuid: String,
|
||||
reason: String,
|
||||
) -> wasmtime::Result<bool> {
|
||||
if let Some(proxy) = &self.proxy
|
||||
&& let Ok(target_uuid) = uuid::Uuid::parse_str(&player_uuid)
|
||||
&& let Some(session) = proxy
|
||||
.session_manager
|
||||
.list_sessions()
|
||||
.into_iter()
|
||||
.find(|s| s.uuid == target_uuid)
|
||||
{
|
||||
let _ = session.action_tx.send(PlayerAction::Disconnect(reason));
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn connect_player(
|
||||
&mut self,
|
||||
player_uuid: String,
|
||||
server_name: String,
|
||||
) -> wasmtime::Result<bool> {
|
||||
if let Some(proxy) = &self.proxy
|
||||
&& proxy.config.servers.contains_key(&server_name)
|
||||
&& let Ok(target_uuid) = uuid::Uuid::parse_str(&player_uuid)
|
||||
&& let Some(session) = proxy
|
||||
.session_manager
|
||||
.list_sessions()
|
||||
.into_iter()
|
||||
.find(|s| s.uuid == target_uuid)
|
||||
{
|
||||
let _ = session
|
||||
.action_tx
|
||||
.send(PlayerAction::Connect { server_name });
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
async fn send_plugin_message(
|
||||
&mut self,
|
||||
player_uuid: String,
|
||||
channel: String,
|
||||
data: Vec<u8>,
|
||||
) -> wasmtime::Result<bool> {
|
||||
if let Some(proxy) = &self.proxy
|
||||
&& let Ok(target_uuid) = uuid::Uuid::parse_str(&player_uuid)
|
||||
&& let Some(session) = proxy
|
||||
.session_manager
|
||||
.list_sessions()
|
||||
.into_iter()
|
||||
.find(|s| s.uuid == target_uuid)
|
||||
{
|
||||
let _ = session
|
||||
.action_tx
|
||||
.send(PlayerAction::PluginMessage { channel, data });
|
||||
return Ok(true);
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl wit::vine::plugin::context::HostContext for PluginHostState {
|
||||
async fn register_event(
|
||||
&mut self,
|
||||
context: Resource<wit::vine::plugin::context::Context>,
|
||||
handler_id: u32,
|
||||
event_type: wit::vine::plugin::event::EventType,
|
||||
priority: wit::vine::plugin::types::EventPriority,
|
||||
) -> wasmtime::Result<()> {
|
||||
let plugin = self
|
||||
.plugin
|
||||
.as_ref()
|
||||
.and_then(|p| p.upgrade())
|
||||
.ok_or_else(|| wasmtime::Error::msg("Plugin reference is no longer valid"))?;
|
||||
|
||||
let api_event_type = match event_type {
|
||||
wit::vine::plugin::event::EventType::ProxyPing => EventType::ProxyPing,
|
||||
wit::vine::plugin::event::EventType::PlayerPreLogin => EventType::PlayerPreLogin,
|
||||
wit::vine::plugin::event::EventType::PlayerLogin => EventType::PlayerLogin,
|
||||
wit::vine::plugin::event::EventType::PlayerJoin => EventType::PlayerJoin,
|
||||
wit::vine::plugin::event::EventType::ServerConnect => EventType::ServerConnect,
|
||||
wit::vine::plugin::event::EventType::ServerConnected => EventType::ServerConnected,
|
||||
wit::vine::plugin::event::EventType::PlayerDisconnect => EventType::PlayerDisconnect,
|
||||
wit::vine::plugin::event::EventType::PlayerChat => EventType::PlayerChat,
|
||||
wit::vine::plugin::event::EventType::PlayerCommand => EventType::PlayerCommand,
|
||||
wit::vine::plugin::event::EventType::PluginMessage => EventType::PluginMessage,
|
||||
};
|
||||
|
||||
let api_priority = match priority {
|
||||
wit::vine::plugin::types::EventPriority::Lowest => EventPriority::Lowest,
|
||||
wit::vine::plugin::types::EventPriority::Low => EventPriority::Low,
|
||||
wit::vine::plugin::types::EventPriority::Normal => EventPriority::Normal,
|
||||
wit::vine::plugin::types::EventPriority::High => EventPriority::High,
|
||||
wit::vine::plugin::types::EventPriority::Highest => EventPriority::Highest,
|
||||
wit::vine::plugin::types::EventPriority::Monitor => EventPriority::Monitor,
|
||||
};
|
||||
|
||||
if let Some(proxy) = &self.proxy {
|
||||
proxy
|
||||
.plugin_manager
|
||||
.register_event_handler(api_event_type, api_priority, handler_id, plugin)
|
||||
.await;
|
||||
}
|
||||
|
||||
let ctx_res = self.get_context(&context)?;
|
||||
let mut events = ctx_res.context.registered_events.lock().await;
|
||||
events.push((handler_id, api_event_type, api_priority));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn register_command(
|
||||
&mut self,
|
||||
context: Resource<wit::vine::plugin::context::Context>,
|
||||
command_id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
aliases: Vec<String>,
|
||||
permission: Option<String>,
|
||||
) -> wasmtime::Result<()> {
|
||||
let plugin = self
|
||||
.plugin
|
||||
.as_ref()
|
||||
.and_then(|p| p.upgrade())
|
||||
.ok_or_else(|| wasmtime::Error::msg("Plugin reference is no longer valid"))?;
|
||||
|
||||
if let Some(proxy) = &self.proxy {
|
||||
proxy
|
||||
.plugin_manager
|
||||
.register_command(command_id, name, description, aliases, permission, plugin)
|
||||
.await;
|
||||
}
|
||||
|
||||
let ctx_res = self.get_context(&context)?;
|
||||
let mut commands = ctx_res.context.registered_commands.lock().await;
|
||||
commands.push(command_id);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_data_folder(
|
||||
&mut self,
|
||||
_context: Resource<wit::vine::plugin::context::Context>,
|
||||
) -> wasmtime::Result<String> {
|
||||
Ok(self.data_folder.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
async fn drop(
|
||||
&mut self,
|
||||
rep: Resource<wit::vine::plugin::context::Context>,
|
||||
) -> wasmtime::Result<()> {
|
||||
let _ = self
|
||||
.resource_table
|
||||
.delete::<ContextResource>(Resource::new_own(rep.rep()));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
12
src/plugin/loader/wasm/wasm_host/wit/mod.rs
Normal file
12
src/plugin/loader/wasm/wasm_host/wit/mod.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use wasmtime::component::bindgen;
|
||||
|
||||
bindgen!({
|
||||
path: "wit",
|
||||
world: "plugin",
|
||||
imports: {
|
||||
default: async | trappable,
|
||||
},
|
||||
exports: {
|
||||
default: async | store | trappable,
|
||||
},
|
||||
});
|
||||
863
src/plugin/mod.rs
Normal file
863
src/plugin/mod.rs
Normal file
@ -0,0 +1,863 @@
|
||||
pub mod api;
|
||||
pub mod loader;
|
||||
|
||||
pub use api::*;
|
||||
pub use loader::wasm::WasmPlugin;
|
||||
use loader::wasm::WasmPluginLoader;
|
||||
use loader::wasm::wasm_host::state::ProxyPluginContext;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{OnceCell, RwLock};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::command::{CommandSender, ProxyCommandSource};
|
||||
use pumpkin_command::{ArgumentBuilder, CommandSource};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RegisteredEventHandler {
|
||||
pub handler_id: u32,
|
||||
pub priority: EventPriority,
|
||||
pub plugin: Arc<WasmPlugin>,
|
||||
}
|
||||
|
||||
pub struct PluginManager {
|
||||
plugins_dir: PathBuf,
|
||||
loader: WasmPluginLoader,
|
||||
loaded_plugins: RwLock<Vec<Arc<WasmPlugin>>>,
|
||||
event_handlers: RwLock<HashMap<EventType, Vec<RegisteredEventHandler>>>,
|
||||
proxy_ctx: OnceCell<ProxyPluginContext>,
|
||||
}
|
||||
|
||||
impl PluginManager {
|
||||
pub fn new(
|
||||
plugins_dir: PathBuf,
|
||||
) -> Result<Arc<Self>, loader::wasm::wasm_host::PluginInitError> {
|
||||
let loader = WasmPluginLoader::new()?;
|
||||
Ok(Arc::new(Self {
|
||||
plugins_dir,
|
||||
loader,
|
||||
loaded_plugins: RwLock::new(Vec::new()),
|
||||
event_handlers: RwLock::new(HashMap::new()),
|
||||
proxy_ctx: OnceCell::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn init_proxy_context(
|
||||
self: &Arc<Self>,
|
||||
config: Arc<crate::config::Config>,
|
||||
session_manager: Arc<crate::session::SessionManager>,
|
||||
command_dispatcher: Arc<crate::command::CommandDispatcher>,
|
||||
) {
|
||||
let ctx = ProxyPluginContext {
|
||||
config,
|
||||
session_manager,
|
||||
command_dispatcher,
|
||||
plugin_manager: self.clone(),
|
||||
};
|
||||
let _ = self.proxy_ctx.set(ctx);
|
||||
}
|
||||
|
||||
pub async fn register_event_handler(
|
||||
&self,
|
||||
event_type: EventType,
|
||||
priority: EventPriority,
|
||||
handler_id: u32,
|
||||
plugin: Arc<WasmPlugin>,
|
||||
) {
|
||||
let mut handlers = self.event_handlers.write().await;
|
||||
let list = handlers.entry(event_type).or_default();
|
||||
list.push(RegisteredEventHandler {
|
||||
handler_id,
|
||||
priority,
|
||||
plugin,
|
||||
});
|
||||
list.sort_by_key(|h| h.priority);
|
||||
}
|
||||
|
||||
pub async fn register_command(
|
||||
&self,
|
||||
command_id: u32,
|
||||
name: String,
|
||||
description: String,
|
||||
aliases: Vec<String>,
|
||||
permission: Option<String>,
|
||||
plugin: Arc<WasmPlugin>,
|
||||
) {
|
||||
if let Some(proxy) = self.proxy_ctx.get() {
|
||||
let names = std::iter::once(name.clone())
|
||||
.chain(aliases)
|
||||
.collect::<Vec<_>>();
|
||||
for cmd_name in names {
|
||||
let plugin_cmd = plugin.clone();
|
||||
let perm_check = permission.clone();
|
||||
let cmd_builder = pumpkin_command::argument_builder::command(cmd_name, description.clone())
|
||||
.requires(move |src: &ProxyCommandSource| {
|
||||
if let Some(ref p) = perm_check {
|
||||
src.has_permission(p)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.executes(PluginCommandExecutor {
|
||||
command_id,
|
||||
plugin: plugin_cmd.clone(),
|
||||
})
|
||||
.then(
|
||||
pumpkin_command::argument_builder::argument(
|
||||
"args",
|
||||
pumpkin_command::argument_types::core::string::StringArgumentType::GreedyPhrase,
|
||||
)
|
||||
.suggests(PluginCommandSuggestionProvider {
|
||||
command_id,
|
||||
plugin: plugin_cmd.clone(),
|
||||
})
|
||||
.executes(PluginCommandExecutor {
|
||||
command_id,
|
||||
plugin: plugin_cmd,
|
||||
}),
|
||||
);
|
||||
proxy.command_dispatcher.register(cmd_builder);
|
||||
}
|
||||
info!(
|
||||
"Registered command '/{}' from plugin '{}'",
|
||||
name, plugin.metadata.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_plugins(&self) -> Result<usize, Box<dyn std::error::Error>> {
|
||||
if !self.plugins_dir.exists() {
|
||||
let _ = fs::create_dir_all(&self.plugins_dir);
|
||||
info!("Created plugins directory '{}'", self.plugins_dir.display());
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let mut count = 0;
|
||||
let start_time = std::time::Instant::now();
|
||||
let read_dir = match fs::read_dir(&self.plugins_dir) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
warn!("Failed to read plugins directory: {}", e);
|
||||
return Ok(0);
|
||||
}
|
||||
};
|
||||
|
||||
let proxy = self.proxy_ctx.get().cloned();
|
||||
|
||||
for entry in read_dir.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_file() && self.loader.can_load(&path) {
|
||||
info!("Loading plugin from '{}'...", path.display());
|
||||
match self.loader.load(&path, proxy.clone()).await {
|
||||
Ok((plugin, metadata)) => {
|
||||
let data_folder =
|
||||
PathBuf::from("plugins").join("data").join(&metadata.name);
|
||||
let context = Arc::new(PluginContext::new(metadata.clone(), data_folder));
|
||||
match plugin.on_load(context).await {
|
||||
Ok(()) => {
|
||||
info!(
|
||||
"Loaded plugin '{}' v{} by {}",
|
||||
metadata.name,
|
||||
metadata.version,
|
||||
metadata.authors.join(", ")
|
||||
);
|
||||
self.loaded_plugins.write().await.push(plugin);
|
||||
count += 1;
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Plugin '{}' failed to on_load: {}", metadata.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to load plugin '{}': {}", path.display(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Loaded {} plugin(s) in {:?}", count, start_time.elapsed());
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
pub async fn unload_plugins(&self) {
|
||||
let plugins = {
|
||||
let mut lock = self.loaded_plugins.write().await;
|
||||
std::mem::take(&mut *lock)
|
||||
};
|
||||
|
||||
for plugin in plugins {
|
||||
let data_folder = PathBuf::from("plugins")
|
||||
.join("data")
|
||||
.join(&plugin.metadata.name);
|
||||
let context = Arc::new(PluginContext::new(plugin.metadata.clone(), data_folder));
|
||||
if let Err(e) = plugin.on_unload(context).await {
|
||||
error!("Error unloading plugin '{}': {}", plugin.metadata.name, e);
|
||||
} else {
|
||||
info!("Unloaded plugin '{}'", plugin.metadata.name);
|
||||
}
|
||||
}
|
||||
|
||||
self.event_handlers.write().await.clear();
|
||||
}
|
||||
|
||||
// --- Event dispatchers ---
|
||||
|
||||
pub async fn fire_proxy_ping(&self, event: &mut ProxyPingEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::ProxyPing).cloned().unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::ProxyPing(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::ProxyPingEvent {
|
||||
client_ip: event.client_ip.clone(),
|
||||
protocol_version: event.protocol_version,
|
||||
virtual_host: event.virtual_host.clone(),
|
||||
motd: event.motd.clone(),
|
||||
max_players: event.max_players,
|
||||
online_players: event.online_players,
|
||||
version_name: event.version_name.clone(),
|
||||
},
|
||||
);
|
||||
match handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await
|
||||
{
|
||||
Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::ProxyPing(res)) => {
|
||||
event.motd = res.motd;
|
||||
event.max_players = res.max_players;
|
||||
event.online_players = res.online_players;
|
||||
event.version_name = res.version_name;
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error in plugin '{}' handling ProxyPing: {}",
|
||||
handler.plugin.metadata.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fire_player_pre_login(&self, event: &mut PlayerPreLoginEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::PlayerPreLogin)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerPreLogin(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::PlayerPreLoginEvent {
|
||||
client_ip: event.client_ip.clone(),
|
||||
username: event.username.clone(),
|
||||
protocol_version: event.protocol_version,
|
||||
virtual_host: event.virtual_host.clone(),
|
||||
cancelled: event.cancelled,
|
||||
cancel_reason: event.cancel_reason.clone(),
|
||||
},
|
||||
);
|
||||
match handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await
|
||||
{
|
||||
Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerPreLogin(
|
||||
res,
|
||||
)) => {
|
||||
event.cancelled = res.cancelled;
|
||||
event.cancel_reason = res.cancel_reason;
|
||||
if event.cancelled {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error in plugin '{}' handling PlayerPreLogin: {}",
|
||||
handler.plugin.metadata.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fire_player_login(&self, event: &mut PlayerLoginEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::PlayerLogin)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerLogin(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::PlayerLoginEvent {
|
||||
uuid: event.uuid.clone(),
|
||||
username: event.username.clone(),
|
||||
client_ip: event.client_ip.clone(),
|
||||
cancelled: event.cancelled,
|
||||
cancel_reason: event.cancel_reason.clone(),
|
||||
},
|
||||
);
|
||||
match handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await
|
||||
{
|
||||
Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerLogin(res)) => {
|
||||
event.cancelled = res.cancelled;
|
||||
event.cancel_reason = res.cancel_reason;
|
||||
if event.cancelled {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error in plugin '{}' handling PlayerLogin: {}",
|
||||
handler.plugin.metadata.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fire_player_join(&self, event: &PlayerJoinEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::PlayerJoin).cloned().unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerJoin(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::PlayerJoinEvent {
|
||||
uuid: event.uuid.clone(),
|
||||
username: event.username.clone(),
|
||||
client_ip: event.client_ip.clone(),
|
||||
initial_server: event.initial_server.clone(),
|
||||
},
|
||||
);
|
||||
let _ = handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fire_server_connect(&self, event: &mut ServerConnectEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::ServerConnect)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::ServerConnect(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::ServerConnectEvent {
|
||||
uuid: event.uuid.clone(),
|
||||
username: event.username.clone(),
|
||||
current_server: event.current_server.clone(),
|
||||
target_server: event.target_server.clone(),
|
||||
cancelled: event.cancelled,
|
||||
cancel_reason: event.cancel_reason.clone(),
|
||||
},
|
||||
);
|
||||
match handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await
|
||||
{
|
||||
Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::ServerConnect(
|
||||
res,
|
||||
)) => {
|
||||
event.target_server = res.target_server;
|
||||
event.cancelled = res.cancelled;
|
||||
event.cancel_reason = res.cancel_reason;
|
||||
if event.cancelled {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error in plugin '{}' handling ServerConnect: {}",
|
||||
handler.plugin.metadata.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fire_server_connected(&self, event: &ServerConnectedEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::ServerConnected)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::ServerConnected(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::ServerConnectedEvent {
|
||||
uuid: event.uuid.clone(),
|
||||
username: event.username.clone(),
|
||||
server_name: event.server_name.clone(),
|
||||
},
|
||||
);
|
||||
let _ = handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fire_player_disconnect(&self, event: &PlayerDisconnectEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::PlayerDisconnect)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt =
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerDisconnect(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::PlayerDisconnectEvent {
|
||||
uuid: event.uuid.clone(),
|
||||
username: event.username.clone(),
|
||||
last_server: event.last_server.clone(),
|
||||
},
|
||||
);
|
||||
let _ = handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fire_player_chat(&self, event: &mut PlayerChatEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::PlayerChat).cloned().unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerChat(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::PlayerChatEvent {
|
||||
uuid: event.uuid.clone(),
|
||||
username: event.username.clone(),
|
||||
message: event.message.clone(),
|
||||
cancelled: event.cancelled,
|
||||
},
|
||||
);
|
||||
match handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await
|
||||
{
|
||||
Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerChat(res)) => {
|
||||
event.message = res.message;
|
||||
event.cancelled = res.cancelled;
|
||||
if event.cancelled {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error in plugin '{}' handling PlayerChat: {}",
|
||||
handler.plugin.metadata.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fire_player_command(&self, event: &mut PlayerCommandEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::PlayerCommand)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerCommand(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::PlayerCommandEvent {
|
||||
uuid: event.uuid.clone(),
|
||||
username: event.username.clone(),
|
||||
command: event.command.clone(),
|
||||
cancelled: event.cancelled,
|
||||
},
|
||||
);
|
||||
match handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await
|
||||
{
|
||||
Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PlayerCommand(
|
||||
res,
|
||||
)) => {
|
||||
event.command = res.command;
|
||||
event.cancelled = res.cancelled;
|
||||
if event.cancelled {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error in plugin '{}' handling PlayerCommand: {}",
|
||||
handler.plugin.metadata.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fire_plugin_message(&self, event: &mut PluginMessageEvent) {
|
||||
let handlers = {
|
||||
let map = self.event_handlers.read().await;
|
||||
map.get(&EventType::PluginMessage)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
};
|
||||
for handler in handlers {
|
||||
let wit_evt = loader::wasm::wasm_host::wit::vine::plugin::event::Event::PluginMessage(
|
||||
loader::wasm::wasm_host::wit::vine::plugin::event::PluginMessageEvent {
|
||||
channel: event.channel.clone(),
|
||||
data: event.data.clone(),
|
||||
player_uuid: event.player_uuid.clone(),
|
||||
server_name: event.server_name.clone(),
|
||||
cancelled: event.cancelled,
|
||||
},
|
||||
);
|
||||
match handler
|
||||
.plugin
|
||||
.handle_event(handler.handler_id, wit_evt)
|
||||
.await
|
||||
{
|
||||
Ok(loader::wasm::wasm_host::wit::vine::plugin::event::Event::PluginMessage(
|
||||
res,
|
||||
)) => {
|
||||
event.data = res.data;
|
||||
event.cancelled = res.cancelled;
|
||||
if event.cancelled {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error in plugin '{}' handling PluginMessage: {}",
|
||||
handler.plugin.metadata.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PluginCommandExecutor {
|
||||
command_id: u32,
|
||||
plugin: Arc<WasmPlugin>,
|
||||
}
|
||||
|
||||
impl pumpkin_command::node::CommandExecutor<ProxyCommandSource> for PluginCommandExecutor {
|
||||
fn execute(
|
||||
&self,
|
||||
context: &pumpkin_command::context::command_context::CommandContext<ProxyCommandSource>,
|
||||
) -> pumpkin_command::node::CommandExecutorResult {
|
||||
let args: Vec<String> = if let Ok(phrase) =
|
||||
pumpkin_command::argument_types::core::string::StringArgumentType::get(context, "args")
|
||||
{
|
||||
phrase.split_whitespace().map(|s| s.to_string()).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let sender = match &context.source.sender {
|
||||
CommandSender::Console => loader::wasm::wasm_host::wit::vine::plugin::types::CommandSender {
|
||||
sender_type:
|
||||
loader::wasm::wasm_host::wit::vine::plugin::types::CommandSenderType::Console,
|
||||
name: "CONSOLE".to_string(),
|
||||
player_uuid: None,
|
||||
},
|
||||
CommandSender::Player { username, .. } => {
|
||||
let uuid = context
|
||||
.source
|
||||
.session_manager
|
||||
.get_session(username)
|
||||
.map(|s| s.uuid.to_string());
|
||||
loader::wasm::wasm_host::wit::vine::plugin::types::CommandSender {
|
||||
sender_type:
|
||||
loader::wasm::wasm_host::wit::vine::plugin::types::CommandSenderType::Player,
|
||||
name: username.clone(),
|
||||
player_uuid: uuid,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let plugin = self.plugin.clone();
|
||||
let command_id = self.command_id;
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
let result = tokio::task::block_in_place(|| {
|
||||
handle.block_on(async move { plugin.handle_command(command_id, sender, args).await })
|
||||
});
|
||||
|
||||
match result {
|
||||
Ok(Ok(code)) => Ok(code),
|
||||
Ok(Err(err_msg)) => {
|
||||
context
|
||||
.source
|
||||
.send_message(pumpkin_util::text::TextComponent::text(format!(
|
||||
"§cError: {err_msg}"
|
||||
)));
|
||||
Ok(0)
|
||||
}
|
||||
Err(e) => {
|
||||
context
|
||||
.source
|
||||
.send_message(pumpkin_util::text::TextComponent::text(format!(
|
||||
"§cPlugin command error: {e}"
|
||||
)));
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PluginCommandSuggestionProvider {
|
||||
command_id: u32,
|
||||
plugin: Arc<WasmPlugin>,
|
||||
}
|
||||
|
||||
impl pumpkin_command::suggestion::provider::SuggestionProvider<ProxyCommandSource>
|
||||
for PluginCommandSuggestionProvider
|
||||
{
|
||||
fn suggest(
|
||||
&self,
|
||||
context: &pumpkin_command::context::command_context::CommandContext<ProxyCommandSource>,
|
||||
builder: pumpkin_command::suggestion::suggestions::SuggestionsBuilder,
|
||||
) -> pumpkin_command::suggestion::provider::SuggestionProviderResult {
|
||||
let args: Vec<String> = if let Ok(phrase) =
|
||||
pumpkin_command::argument_types::core::string::StringArgumentType::get(context, "args")
|
||||
{
|
||||
phrase.split_whitespace().map(|s| s.to_string()).collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let sender = match &context.source.sender {
|
||||
CommandSender::Console => loader::wasm::wasm_host::wit::vine::plugin::types::CommandSender {
|
||||
sender_type:
|
||||
loader::wasm::wasm_host::wit::vine::plugin::types::CommandSenderType::Console,
|
||||
name: "CONSOLE".to_string(),
|
||||
player_uuid: None,
|
||||
},
|
||||
CommandSender::Player { username, .. } => {
|
||||
let uuid = context
|
||||
.source
|
||||
.session_manager
|
||||
.get_session(username)
|
||||
.map(|s| s.uuid.to_string());
|
||||
loader::wasm::wasm_host::wit::vine::plugin::types::CommandSender {
|
||||
sender_type:
|
||||
loader::wasm::wasm_host::wit::vine::plugin::types::CommandSenderType::Player,
|
||||
name: username.clone(),
|
||||
player_uuid: uuid,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let plugin = self.plugin.clone();
|
||||
let command_id = self.command_id;
|
||||
let handle = tokio::runtime::Handle::current();
|
||||
let suggestions_res = tokio::task::block_in_place(|| {
|
||||
handle.block_on(async move {
|
||||
plugin
|
||||
.handle_command_suggestion(command_id, sender, args)
|
||||
.await
|
||||
})
|
||||
});
|
||||
|
||||
let mut builder = builder;
|
||||
if let Ok(suggestions) = suggestions_res {
|
||||
for s in suggestions {
|
||||
builder = builder.suggest(s);
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::command::CommandDispatcher;
|
||||
use crate::config::Config;
|
||||
use crate::session::SessionManager;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_plugin_manager_empty_dir() {
|
||||
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
let config = Arc::new(Config::default());
|
||||
let dispatcher = Arc::new(CommandDispatcher::new());
|
||||
let session_manager = Arc::new(SessionManager::new());
|
||||
let manager =
|
||||
PluginManager::new(temp_dir.path().to_path_buf()).expect("failed to create manager");
|
||||
manager.init_proxy_context(config, session_manager, dispatcher);
|
||||
|
||||
let count = manager.load_plugins().await.expect("load failed");
|
||||
assert_eq!(count, 0);
|
||||
|
||||
manager.unload_plugins().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_plugin_manager_event_dispatch() {
|
||||
let temp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
let config = Arc::new(Config::default());
|
||||
let dispatcher = Arc::new(CommandDispatcher::new());
|
||||
let session_manager = Arc::new(SessionManager::new());
|
||||
let manager =
|
||||
PluginManager::new(temp_dir.path().to_path_buf()).expect("failed to create manager");
|
||||
manager.init_proxy_context(config, session_manager, dispatcher);
|
||||
|
||||
// Proxy ping
|
||||
let mut ping = ProxyPingEvent {
|
||||
client_ip: "127.0.0.1".to_string(),
|
||||
protocol_version: 765,
|
||||
virtual_host: "localhost".to_string(),
|
||||
motd: "A Minecraft Proxy".to_string(),
|
||||
max_players: 100,
|
||||
online_players: 0,
|
||||
version_name: "Vine 1.21".to_string(),
|
||||
};
|
||||
manager.fire_proxy_ping(&mut ping).await;
|
||||
assert_eq!(ping.motd, "A Minecraft Proxy");
|
||||
assert_eq!(ping.max_players, 100);
|
||||
|
||||
// Player pre-login
|
||||
let mut pre_login = PlayerPreLoginEvent {
|
||||
client_ip: "127.0.0.1".to_string(),
|
||||
username: "Player1".to_string(),
|
||||
protocol_version: 765,
|
||||
virtual_host: "localhost".to_string(),
|
||||
cancelled: false,
|
||||
cancel_reason: None,
|
||||
};
|
||||
manager.fire_player_pre_login(&mut pre_login).await;
|
||||
assert!(!pre_login.cancelled);
|
||||
|
||||
// Player login
|
||||
let mut login = PlayerLoginEvent {
|
||||
uuid: "00000000-0000-0000-0000-000000000001".to_string(),
|
||||
username: "Player1".to_string(),
|
||||
client_ip: "127.0.0.1".to_string(),
|
||||
cancelled: false,
|
||||
cancel_reason: None,
|
||||
};
|
||||
manager.fire_player_login(&mut login).await;
|
||||
assert!(!login.cancelled);
|
||||
|
||||
// Server connect
|
||||
let mut connect = ServerConnectEvent {
|
||||
uuid: "00000000-0000-0000-0000-000000000001".to_string(),
|
||||
username: "Player1".to_string(),
|
||||
current_server: None,
|
||||
target_server: "lobby".to_string(),
|
||||
cancelled: false,
|
||||
cancel_reason: None,
|
||||
};
|
||||
manager.fire_server_connect(&mut connect).await;
|
||||
assert_eq!(connect.target_server, "lobby");
|
||||
assert!(!connect.cancelled);
|
||||
|
||||
// Server connected
|
||||
let connected = ServerConnectedEvent {
|
||||
uuid: "00000000-0000-0000-0000-000000000001".to_string(),
|
||||
username: "Player1".to_string(),
|
||||
server_name: "lobby".to_string(),
|
||||
};
|
||||
manager.fire_server_connected(&connected).await;
|
||||
|
||||
// Player join
|
||||
let join = PlayerJoinEvent {
|
||||
uuid: "00000000-0000-0000-0000-000000000001".to_string(),
|
||||
username: "Player1".to_string(),
|
||||
client_ip: "127.0.0.1".to_string(),
|
||||
initial_server: "lobby".to_string(),
|
||||
};
|
||||
manager.fire_player_join(&join).await;
|
||||
|
||||
// Player chat
|
||||
let mut chat = PlayerChatEvent {
|
||||
uuid: "00000000-0000-0000-0000-000000000001".to_string(),
|
||||
username: "Player1".to_string(),
|
||||
message: "Hello proxy!".to_string(),
|
||||
cancelled: false,
|
||||
};
|
||||
manager.fire_player_chat(&mut chat).await;
|
||||
assert_eq!(chat.message, "Hello proxy!");
|
||||
assert!(!chat.cancelled);
|
||||
|
||||
// Player command
|
||||
let mut cmd = PlayerCommandEvent {
|
||||
uuid: "00000000-0000-0000-0000-000000000001".to_string(),
|
||||
username: "Player1".to_string(),
|
||||
command: "ping".to_string(),
|
||||
cancelled: false,
|
||||
};
|
||||
manager.fire_player_command(&mut cmd).await;
|
||||
assert!(!cmd.cancelled);
|
||||
|
||||
// Plugin message
|
||||
let mut msg = PluginMessageEvent {
|
||||
channel: "vine:test".to_string(),
|
||||
data: vec![1, 2, 3],
|
||||
player_uuid: Some("00000000-0000-0000-0000-000000000001".to_string()),
|
||||
server_name: Some("lobby".to_string()),
|
||||
cancelled: false,
|
||||
};
|
||||
manager.fire_plugin_message(&mut msg).await;
|
||||
assert!(!msg.cancelled);
|
||||
|
||||
// Player disconnect
|
||||
let disc = PlayerDisconnectEvent {
|
||||
uuid: "00000000-0000-0000-0000-000000000001".to_string(),
|
||||
username: "Player1".to_string(),
|
||||
last_server: Some("lobby".to_string()),
|
||||
};
|
||||
manager.fire_player_disconnect(&disc).await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event_priority_ordering() {
|
||||
assert!(EventPriority::Lowest < EventPriority::Low);
|
||||
assert!(EventPriority::Low < EventPriority::Normal);
|
||||
assert!(EventPriority::Normal < EventPriority::High);
|
||||
assert!(EventPriority::High < EventPriority::Highest);
|
||||
assert!(EventPriority::Highest < EventPriority::Monitor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plugin_metadata_serde() {
|
||||
let meta = PluginMetadata {
|
||||
name: "test-plugin".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
authors: vec!["Author1".to_string()],
|
||||
description: Some("A test plugin".to_string()),
|
||||
dependencies: vec!["other-plugin".to_string()],
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&meta).expect("serialization failed");
|
||||
let decoded: PluginMetadata = serde_json::from_str(&json).expect("deserialization failed");
|
||||
assert_eq!(decoded.name, "test-plugin");
|
||||
assert_eq!(decoded.version, "1.0.0");
|
||||
assert_eq!(decoded.authors, vec!["Author1".to_string()]);
|
||||
assert_eq!(decoded.dependencies, vec!["other-plugin".to_string()]);
|
||||
}
|
||||
}
|
||||
31
src/proxy.rs
31
src/proxy.rs
@ -27,6 +27,7 @@ pub struct ProxyServer {
|
||||
online_players: Arc<AtomicUsize>,
|
||||
session_manager: Arc<SessionManager>,
|
||||
command_dispatcher: Arc<CommandDispatcher>,
|
||||
plugin_manager: Arc<crate::plugin::PluginManager>,
|
||||
}
|
||||
|
||||
impl ProxyServer {
|
||||
@ -68,6 +69,14 @@ impl ProxyServer {
|
||||
|
||||
let router = Arc::new(Router::new(config.routing.clone(), config.servers.clone()));
|
||||
|
||||
let plugins_dir = std::path::PathBuf::from(&config.plugins.plugin_dir);
|
||||
let plugin_manager = crate::plugin::PluginManager::new(plugins_dir)?;
|
||||
plugin_manager.init_proxy_context(
|
||||
config.clone(),
|
||||
session_manager.clone(),
|
||||
command_dispatcher.clone(),
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
keystore,
|
||||
@ -78,6 +87,7 @@ impl ProxyServer {
|
||||
online_players,
|
||||
session_manager,
|
||||
command_dispatcher,
|
||||
plugin_manager,
|
||||
})
|
||||
}
|
||||
|
||||
@ -221,6 +231,16 @@ impl ProxyServer {
|
||||
}
|
||||
});
|
||||
|
||||
if self.config.plugins.enabled {
|
||||
info!(
|
||||
"Loading plugins from '{}'...",
|
||||
self.config.plugins.plugin_dir
|
||||
);
|
||||
if let Err(e) = self.plugin_manager.load_plugins().await {
|
||||
error!("Failed to load plugins: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
accept_res = listener.accept() => {
|
||||
@ -235,6 +255,7 @@ impl ProxyServer {
|
||||
let rate_limiter = self.rate_limiter.clone();
|
||||
let session_manager = self.session_manager.clone();
|
||||
let command_dispatcher = self.command_dispatcher.clone();
|
||||
let plugin_manager = self.plugin_manager.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = network::configure_client_socket(&stream, &config.network) {
|
||||
@ -249,8 +270,8 @@ impl ProxyServer {
|
||||
match network::parse_proxy_protocol(&mut stream, peer_addr).await {
|
||||
Ok(res) => {
|
||||
effective_addr = res.client_addr;
|
||||
leftover_bytes = res.leftover_bytes;
|
||||
debug!("[{}] PROXY protocol resolved client IP: {}", peer_addr, effective_addr);
|
||||
leftover_bytes = res.leftover_bytes;
|
||||
debug!("[{}] PROXY protocol resolved client IP: {}", peer_addr, effective_addr);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("[{}] Dropped connection: PROXY protocol error: {}", peer_addr, err);
|
||||
@ -274,6 +295,7 @@ impl ProxyServer {
|
||||
online_players,
|
||||
session_manager,
|
||||
command_dispatcher,
|
||||
plugin_manager,
|
||||
);
|
||||
|
||||
if let Err(err) = handler.handle(stream, leftover_bytes).await {
|
||||
@ -296,6 +318,11 @@ impl ProxyServer {
|
||||
|
||||
info!("Vine proxy has stopped accepting connections.");
|
||||
|
||||
if self.config.plugins.enabled {
|
||||
info!("Unloading plugins...");
|
||||
self.plugin_manager.unload_plugins().await;
|
||||
}
|
||||
|
||||
if let Some(handle) = telemetry_handle {
|
||||
let timeout_secs = self.config.server.shutdown_timeout_secs.max(1);
|
||||
let _ = tokio::time::timeout(Duration::from_secs(timeout_secs), handle).await;
|
||||
|
||||
@ -14,6 +14,10 @@ pub enum PlayerAction {
|
||||
Transfer { host: String, port: u16 },
|
||||
/// Send a system chat message to the player
|
||||
Message(TextComponent),
|
||||
/// Disconnect/kick the player with a reason
|
||||
Disconnect(String),
|
||||
/// Send a custom plugin message / payload to the player
|
||||
PluginMessage { channel: String, data: Vec<u8> },
|
||||
}
|
||||
|
||||
/// Represents an active connected player session on the proxy
|
||||
@ -23,6 +27,8 @@ pub struct PlayerSession {
|
||||
pub uuid: Uuid,
|
||||
pub current_server: String,
|
||||
pub action_tx: mpsc::UnboundedSender<PlayerAction>,
|
||||
pub client_ip: String,
|
||||
pub protocol_version: u32,
|
||||
}
|
||||
|
||||
/// Thread-safe registry of connected player sessions
|
||||
@ -111,6 +117,8 @@ mod tests {
|
||||
uuid: Uuid::new_v4(),
|
||||
current_server: "lobby".to_string(),
|
||||
action_tx: tx,
|
||||
client_ip: "127.0.0.1:12345".to_string(),
|
||||
protocol_version: 765,
|
||||
};
|
||||
|
||||
manager.register(session);
|
||||
@ -135,6 +143,8 @@ mod tests {
|
||||
uuid: Uuid::new_v4(),
|
||||
current_server: "lobby".to_string(),
|
||||
action_tx: tx,
|
||||
client_ip: "127.0.0.1:12346".to_string(),
|
||||
protocol_version: 765,
|
||||
};
|
||||
manager.register(session);
|
||||
assert_eq!(manager.get_session("Alex").unwrap().current_server, "lobby");
|
||||
|
||||
@ -122,7 +122,7 @@ impl StatusHandler {
|
||||
}
|
||||
|
||||
/// Resolves MOTD description for the virtual host or falls back to global default
|
||||
fn resolve_motd_text(&self, host: Option<&str>) -> String {
|
||||
pub fn resolve_motd_text(&self, host: Option<&str>) -> String {
|
||||
if let Some(h) = host {
|
||||
let clean_host = h.split(':').next().unwrap_or(h).trim();
|
||||
|
||||
@ -161,6 +161,58 @@ impl StatusHandler {
|
||||
CStatusResponse::new(json)
|
||||
}
|
||||
|
||||
/// Create custom CStatusResponse packet with overridden fields from plugins
|
||||
pub fn create_custom_status_packet(
|
||||
&self,
|
||||
client_version: JavaMinecraftVersion,
|
||||
motd: &str,
|
||||
max_players: u32,
|
||||
online_players: u32,
|
||||
version_name: &str,
|
||||
virtual_host: Option<&str>,
|
||||
) -> CStatusResponse {
|
||||
let protocol = if client_version == JavaMinecraftVersion::Unknown {
|
||||
self.motd.protocol_version
|
||||
} else {
|
||||
client_version.protocol_version()
|
||||
};
|
||||
|
||||
let icon_uri = self.icon_manager.get_icon_for_host(virtual_host);
|
||||
|
||||
let sample: Vec<PlayerSample> = if self.motd.show_sample_players {
|
||||
self.motd
|
||||
.sample_players
|
||||
.iter()
|
||||
.map(|name| PlayerSample {
|
||||
name,
|
||||
id: Uuid::nil().to_string(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let status = StatusJson {
|
||||
version: StatusVersion {
|
||||
name: version_name,
|
||||
protocol,
|
||||
},
|
||||
players: StatusPlayers {
|
||||
max: max_players,
|
||||
online: online_players as usize,
|
||||
sample,
|
||||
},
|
||||
description: StatusDescription {
|
||||
text: motd.to_string(),
|
||||
},
|
||||
enforce_secure_chat: false,
|
||||
favicon: icon_uri.as_deref(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&status).unwrap_or_else(|_| "{}".to_string());
|
||||
CStatusResponse::new(json)
|
||||
}
|
||||
|
||||
/// Create CPingResponse packet
|
||||
pub fn create_pong_packet(payload: i64) -> CPingResponse {
|
||||
CPingResponse::new(payload)
|
||||
|
||||
6
wit/command.wit
Normal file
6
wit/command.wit
Normal file
@ -0,0 +1,6 @@
|
||||
interface command {
|
||||
use types.{command-sender};
|
||||
|
||||
/// Reply to a command sender
|
||||
reply: func(sender: command-sender, message: string);
|
||||
}
|
||||
15
wit/context.wit
Normal file
15
wit/context.wit
Normal file
@ -0,0 +1,15 @@
|
||||
interface context {
|
||||
use types.{event-priority};
|
||||
use event.{event-type};
|
||||
|
||||
resource context {
|
||||
/// Register an event listener for an event type with priority
|
||||
register-event: func(handler-id: u32, event-type: event-type, priority: event-priority);
|
||||
|
||||
/// Register a proxy command
|
||||
register-command: func(command-id: u32, name: string, description: string, aliases: list<string>, permission: option<string>);
|
||||
|
||||
/// Get path to the plugin's data folder (e.g. plugins/data/<plugin-name>)
|
||||
get-data-folder: func() -> string;
|
||||
}
|
||||
}
|
||||
106
wit/event.wit
Normal file
106
wit/event.wit
Normal file
@ -0,0 +1,106 @@
|
||||
interface event {
|
||||
use types.{event-priority};
|
||||
|
||||
enum event-type {
|
||||
proxy-ping,
|
||||
player-pre-login,
|
||||
player-login,
|
||||
player-join,
|
||||
server-connect,
|
||||
server-connected,
|
||||
player-disconnect,
|
||||
player-chat,
|
||||
player-command,
|
||||
plugin-message,
|
||||
}
|
||||
|
||||
record proxy-ping-event {
|
||||
client-ip: string,
|
||||
protocol-version: u32,
|
||||
virtual-host: string,
|
||||
motd: string,
|
||||
max-players: u32,
|
||||
online-players: u32,
|
||||
version-name: string,
|
||||
}
|
||||
|
||||
record player-pre-login-event {
|
||||
client-ip: string,
|
||||
username: string,
|
||||
protocol-version: u32,
|
||||
virtual-host: string,
|
||||
cancelled: bool,
|
||||
cancel-reason: option<string>,
|
||||
}
|
||||
|
||||
record player-login-event {
|
||||
uuid: string,
|
||||
username: string,
|
||||
client-ip: string,
|
||||
cancelled: bool,
|
||||
cancel-reason: option<string>,
|
||||
}
|
||||
|
||||
record player-join-event {
|
||||
uuid: string,
|
||||
username: string,
|
||||
client-ip: string,
|
||||
initial-server: string,
|
||||
}
|
||||
|
||||
record server-connect-event {
|
||||
uuid: string,
|
||||
username: string,
|
||||
current-server: option<string>,
|
||||
target-server: string,
|
||||
cancelled: bool,
|
||||
cancel-reason: option<string>,
|
||||
}
|
||||
|
||||
record server-connected-event {
|
||||
uuid: string,
|
||||
username: string,
|
||||
server-name: string,
|
||||
}
|
||||
|
||||
record player-disconnect-event {
|
||||
uuid: string,
|
||||
username: string,
|
||||
last-server: option<string>,
|
||||
}
|
||||
|
||||
record player-chat-event {
|
||||
uuid: string,
|
||||
username: string,
|
||||
message: string,
|
||||
cancelled: bool,
|
||||
}
|
||||
|
||||
record player-command-event {
|
||||
uuid: string,
|
||||
username: string,
|
||||
command: string,
|
||||
cancelled: bool,
|
||||
}
|
||||
|
||||
record plugin-message-event {
|
||||
channel: string,
|
||||
data: list<u8>,
|
||||
player-uuid: option<string>,
|
||||
server-name: option<string>,
|
||||
cancelled: bool,
|
||||
}
|
||||
|
||||
variant event {
|
||||
proxy-ping(proxy-ping-event),
|
||||
player-pre-login(player-pre-login-event),
|
||||
player-login(player-login-event),
|
||||
player-join(player-join-event),
|
||||
server-connect(server-connect-event),
|
||||
server-connected(server-connected-event),
|
||||
player-disconnect(player-disconnect-event),
|
||||
player-chat(player-chat-event),
|
||||
player-command(player-command-event),
|
||||
plugin-message(plugin-message-event),
|
||||
}
|
||||
}
|
||||
11
wit/logging.wit
Normal file
11
wit/logging.wit
Normal file
@ -0,0 +1,11 @@
|
||||
interface logging {
|
||||
enum level {
|
||||
trace,
|
||||
debug,
|
||||
info,
|
||||
warn,
|
||||
error,
|
||||
}
|
||||
|
||||
log: func(level: level, message: string);
|
||||
}
|
||||
9
wit/metadata.wit
Normal file
9
wit/metadata.wit
Normal file
@ -0,0 +1,9 @@
|
||||
interface metadata {
|
||||
record plugin-metadata {
|
||||
name: string,
|
||||
version: string,
|
||||
authors: list<string>,
|
||||
description: option<string>,
|
||||
dependencies: list<string>,
|
||||
}
|
||||
}
|
||||
34
wit/plugin.wit
Normal file
34
wit/plugin.wit
Normal file
@ -0,0 +1,34 @@
|
||||
package vine:plugin@0.1.0;
|
||||
|
||||
world plugin {
|
||||
use context.{context};
|
||||
use event.{event};
|
||||
use types.{command-sender};
|
||||
use metadata.{plugin-metadata};
|
||||
|
||||
import logging;
|
||||
import proxy;
|
||||
import context;
|
||||
import command;
|
||||
|
||||
/// Called once by the host to initialize the plugin instance.
|
||||
export init-plugin: func();
|
||||
|
||||
/// Called when the plugin is loaded into the proxy.
|
||||
export on-load: func(ctx: context) -> result<_, string>;
|
||||
|
||||
/// Called when the plugin is being unloaded.
|
||||
export on-unload: func(ctx: context) -> result<_, string>;
|
||||
|
||||
/// Provides plugin metadata to the proxy host.
|
||||
export get-metadata: func() -> plugin-metadata;
|
||||
|
||||
/// Dispatches an event to the plugin handler.
|
||||
export handle-event: func(handler-id: u32, evt: event) -> event;
|
||||
|
||||
/// Dispatches a command execution to the plugin.
|
||||
export handle-command: func(command-id: u32, sender: command-sender, args: list<string>) -> result<s32, string>;
|
||||
|
||||
/// Dispatches command suggestion / tab completion to the plugin.
|
||||
export handle-command-suggestion: func(command-id: u32, sender: command-sender, args: list<string>) -> list<string>;
|
||||
}
|
||||
33
wit/proxy.wit
Normal file
33
wit/proxy.wit
Normal file
@ -0,0 +1,33 @@
|
||||
interface proxy {
|
||||
use types.{player-info, server-info};
|
||||
|
||||
/// Returns the total number of connected players.
|
||||
get-player-count: func() -> u32;
|
||||
|
||||
/// Returns a list of all connected players.
|
||||
get-players: func() -> list<player-info>;
|
||||
|
||||
/// Returns information for a specific player by username.
|
||||
get-player-by-name: func(username: string) -> option<player-info>;
|
||||
|
||||
/// Returns information for a specific player by UUID string.
|
||||
get-player-by-uuid: func(uuid: string) -> option<player-info>;
|
||||
|
||||
/// Returns a list of configured backend servers.
|
||||
get-servers: func() -> list<server-info>;
|
||||
|
||||
/// Broadcasts a system chat message to all connected players and console.
|
||||
broadcast: func(message: string);
|
||||
|
||||
/// Sends a system chat message to a specific player by UUID.
|
||||
send-message: func(player-uuid: string, message: string) -> bool;
|
||||
|
||||
/// Disconnects/kicks a player by UUID with the given reason.
|
||||
disconnect-player: func(player-uuid: string, reason: string) -> bool;
|
||||
|
||||
/// Requests switching/connecting a player to a backend server.
|
||||
connect-player: func(player-uuid: string, server-name: string) -> bool;
|
||||
|
||||
/// Sends a plugin message / custom payload to a player.
|
||||
send-plugin-message: func(player-uuid: string, channel: string, data: list<u8>) -> bool;
|
||||
}
|
||||
35
wit/types.wit
Normal file
35
wit/types.wit
Normal file
@ -0,0 +1,35 @@
|
||||
interface types {
|
||||
record player-info {
|
||||
uuid: string,
|
||||
username: string,
|
||||
current-server: string,
|
||||
client-ip: string,
|
||||
protocol-version: u32,
|
||||
}
|
||||
|
||||
record server-info {
|
||||
name: string,
|
||||
address: string,
|
||||
player-count: u32,
|
||||
}
|
||||
|
||||
enum event-priority {
|
||||
lowest,
|
||||
low,
|
||||
normal,
|
||||
high,
|
||||
highest,
|
||||
monitor,
|
||||
}
|
||||
|
||||
enum command-sender-type {
|
||||
console,
|
||||
player,
|
||||
}
|
||||
|
||||
record command-sender {
|
||||
sender-type: command-sender-type,
|
||||
name: string,
|
||||
player-uuid: option<string>,
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user