diff --git a/crates/pumpkin-plugin-api/src/events/server/mod.rs b/crates/pumpkin-plugin-api/src/events/server/mod.rs index da103b94f..114157845 100644 --- a/crates/pumpkin-plugin-api/src/events/server/mod.rs +++ b/crates/pumpkin-plugin-api/src/events/server/mod.rs @@ -2,6 +2,8 @@ pub mod server_broadcast; /// Server command execution event. pub mod server_command; +/// Server list ping response event. +pub mod server_list_ping; /// Server initialization load event. pub mod server_load; /// Server tick completion event. @@ -13,6 +15,7 @@ pub mod spawn_change; pub use server_broadcast::*; pub use server_command::*; +pub use server_list_ping::*; pub use server_load::*; pub use server_tick_end::*; pub use server_tick_start::*; diff --git a/crates/pumpkin-plugin-api/src/events/server/server_list_ping.rs b/crates/pumpkin-plugin-api/src/events/server/server_list_ping.rs new file mode 100644 index 000000000..039d7304a --- /dev/null +++ b/crates/pumpkin-plugin-api/src/events/server/server_list_ping.rs @@ -0,0 +1,25 @@ +use crate::wit::pumpkin::plugin::event::{Event, EventType, ServerListPingEventData}; + +use super::super::FromIntoEvent; + +/// Fires when the server prepares a Java status/list ping response. +/// +/// Register this as a blocking event handler to customize the MOTD, favicon, +/// and reported player counts for WASM plugins. +pub struct ServerListPingEvent; + +impl FromIntoEvent for ServerListPingEvent { + const EVENT_TYPE: EventType = EventType::ServerListPingEvent; + type Data = ServerListPingEventData; + + fn data_from_event(event: Event) -> Self::Data { + match event { + Event::ServerListPingEvent(data) => data, + _ => panic!("expected ServerListPingEvent"), + } + } + + fn data_into_event(data: Self::Data) -> Event { + Event::ServerListPingEvent(data) + } +} diff --git a/crates/pumpkin-plugin-api/src/ext/mod.rs b/crates/pumpkin-plugin-api/src/ext/mod.rs index b099749ea..8c4c7bcab 100644 --- a/crates/pumpkin-plugin-api/src/ext/mod.rs +++ b/crates/pumpkin-plugin-api/src/ext/mod.rs @@ -1,3 +1,4 @@ //! WIT-generated plugin type extensions belong in here. For example [Display](std::fmt::Display) implementations. +mod server_list_ping; mod uuid; diff --git a/crates/pumpkin-plugin-api/src/ext/server_list_ping.rs b/crates/pumpkin-plugin-api/src/ext/server_list_ping.rs new file mode 100644 index 000000000..c5fa53406 --- /dev/null +++ b/crates/pumpkin-plugin-api/src/ext/server_list_ping.rs @@ -0,0 +1,38 @@ +use std::fmt; + +use crate::wit::pumpkin::plugin::event::ServerListPingAddress; + +impl fmt::Display for ServerListPingAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.host.contains(':') { + write!(f, "[{}]:{}", self.host, self.port) + } else { + write!(f, "{}:{}", self.host, self.port) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn formats_ipv4_address() { + let address = ServerListPingAddress { + host: "127.0.0.1".into(), + port: 25565, + }; + + assert_eq!(address.to_string(), "127.0.0.1:25565"); + } + + #[test] + fn formats_ipv6_address() { + let address = ServerListPingAddress { + host: "::1".into(), + port: 25565, + }; + + assert_eq!(address.to_string(), "[::1]:25565"); + } +} diff --git a/crates/pumpkin-plugin-wit b/crates/pumpkin-plugin-wit index ab193d116..50962bf63 160000 --- a/crates/pumpkin-plugin-wit +++ b/crates/pumpkin-plugin-wit @@ -1 +1 @@ -Subproject commit ab193d1160b90c446fb35d48e6c636d8f867c473 +Subproject commit 50962bf63943a48a5835a9f5db0eea4267c3035f diff --git a/crates/pumpkin/src/net/java/pending.rs b/crates/pumpkin/src/net/java/pending.rs index 929188448..6afce92d8 100644 --- a/crates/pumpkin/src/net/java/pending.rs +++ b/crates/pumpkin/src/net/java/pending.rs @@ -227,7 +227,7 @@ impl PendingConnection { async fn handle_status_packet( &mut self, - server: &Server, + server: &Arc, packet: &RawPacket, ) -> Result, ReadingError> { debug!("Handling status group"); diff --git a/crates/pumpkin/src/net/java/status.rs b/crates/pumpkin/src/net/java/status.rs index 6c9e25ccd..795e76621 100644 --- a/crates/pumpkin/src/net/java/status.rs +++ b/crates/pumpkin/src/net/java/status.rs @@ -1,21 +1,59 @@ use pumpkin_protocol::{ - java::client::status::CPingResponse, java::server::status::SStatusPingRequest, + Players, + java::client::status::{CPingResponse, CStatusResponse}, + java::server::status::SStatusPingRequest, }; -use crate::{net::java::pending::PendingConnection, server::Server}; +use std::sync::Arc; + +use crate::{ + net::java::pending::PendingConnection, plugin::server::list_ping::ServerListPingEvent, + server::Server, +}; use tracing::debug; impl PendingConnection { - pub async fn handle_status_request(&mut self, server: &Server) { + pub async fn handle_status_request(&mut self, server: &Arc) { debug!("Handling status request"); - let status = server.get_status(); - self.send_packet_now( - &status + let mut status_response = { + let status = server.get_status(); + status .lock() .await - .get_status_packet(self.version.load().protocol_version()), - ) - .await; + .get_status_response(self.version.load().protocol_version()) + }; + + let (max_players, num_players) = status_response + .players + .as_ref() + .map_or((0, 0), |players| (players.max, players.online)); + + let mut event = ServerListPingEvent::new( + self.server_address.clone(), + self.address, + status_response.description.clone(), + max_players, + num_players, + status_response.favicon.clone(), + ); + server.plugin_manager.fire(server, &mut event).await; + + status_response.description = event.motd; + status_response.favicon = event.favicon; + if let Some(players) = &mut status_response.players { + players.max = event.max_players; + players.online = event.num_players; + } else { + status_response.players = Some(Players { + max: event.max_players, + online: event.num_players, + sample: vec![], + }); + } + + let status_json = serde_json::to_string(&status_response).unwrap_or_default(); + self.send_packet_now(&CStatusResponse::new(status_json)) + .await; } pub async fn handle_ping_request(&mut self, ping_request: SStatusPingRequest) { diff --git a/crates/pumpkin/src/plugin/api/events/server/list_ping.rs b/crates/pumpkin/src/plugin/api/events/server/list_ping.rs new file mode 100644 index 000000000..8d71b47b5 --- /dev/null +++ b/crates/pumpkin/src/plugin/api/events/server/list_ping.rs @@ -0,0 +1,111 @@ +use std::{ + fmt, + net::{SocketAddr, ToSocketAddrs}, +}; + +use pumpkin_macros::Event; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ServerListPingAddress { + host: String, + port: u16, +} + +impl ServerListPingAddress { + #[must_use] + pub const fn new(host: String, port: u16) -> Self { + Self { host, port } + } + + #[must_use] + pub fn from_socket_addr(address: SocketAddr) -> Self { + Self { + host: address.ip().to_string(), + port: address.port(), + } + } + + #[must_use] + pub fn host(&self) -> &str { + &self.host + } + + #[must_use] + pub const fn port(&self) -> u16 { + self.port + } + + #[must_use] + pub fn as_socket_addr(&self) -> Option { + (self.host.as_str(), self.port) + .to_socket_addrs() + .ok() + .and_then(|mut addrs| addrs.next()) + } +} + +impl fmt::Display for ServerListPingAddress { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.host.contains(':') { + write!(f, "[{}]:{}", self.host, self.port) + } else { + write!(f, "{}:{}", self.host, self.port) + } + } +} + +/// An event that occurs when the server responds to a status ping. +#[derive(Event, Clone)] +pub struct ServerListPingEvent { + /// The hostname the client used to ping the server. + pub(crate) hostname: String, + + /// The address the ping came from. + pub(crate) address: ServerListPingAddress, + + /// The MOTD shown in the server list. + pub motd: String, + + /// The maximum player count. + pub max_players: u32, + + /// The current online player count. + pub num_players: u32, + + /// The favicon as a data URI (if any). + pub favicon: Option, +} + +impl ServerListPingEvent { + /// Creates a new `ServerListPingEvent`. + #[must_use] + pub fn new( + hostname: String, + address: SocketAddr, + motd: String, + max_players: u32, + num_players: u32, + favicon: Option, + ) -> Self { + Self { + hostname, + address: ServerListPingAddress::from_socket_addr(address), + motd, + max_players, + num_players, + favicon, + } + } + + /// The hostname provided by the client during the status handshake. + #[must_use] + pub fn hostname(&self) -> &str { + &self.hostname + } + + /// The remote socket address of the client requesting the status ping. + #[must_use] + pub const fn address(&self) -> &ServerListPingAddress { + &self.address + } +} diff --git a/crates/pumpkin/src/plugin/api/events/server/mod.rs b/crates/pumpkin/src/plugin/api/events/server/mod.rs index 6fb3d5568..fe797ddf9 100644 --- a/crates/pumpkin/src/plugin/api/events/server/mod.rs +++ b/crates/pumpkin/src/plugin/api/events/server/mod.rs @@ -1,3 +1,4 @@ +pub mod list_ping; pub mod packet; pub mod server_broadcast; pub mod server_command; diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs index ba827d746..f6929eabc 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/context.rs @@ -443,6 +443,7 @@ async fn register_server_event( event_type: EventType, ) { use crate::plugin::server::{ + list_ping::ServerListPingEvent, packet::{PacketReceivedEvent, PacketSentEvent}, server_broadcast::ServerBroadcastEvent, server_command::ServerCommandEvent, @@ -462,6 +463,10 @@ async fn register_server_event( EventType::ServerCommandEvent => { register_typed_event::(resource, handler, priority, blocking).await; } + EventType::ServerListPingEvent => { + register_typed_event::(resource, handler, priority, blocking) + .await; + } EventType::ServerBroadcastEvent => { register_typed_event::(resource, handler, priority, blocking) .await; @@ -544,6 +549,7 @@ impl pumpkin::plugin::context::HostContext for PluginHostState { event_type @ (EventType::PacketReceivedEvent | EventType::PacketSentEvent | EventType::ServerCommandEvent + | EventType::ServerListPingEvent | EventType::ServerBroadcastEvent | EventType::ServerLoadEvent | EventType::ServerTickEndEvent diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs index bdd7c44bf..220ef9091 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/mod.rs @@ -47,6 +47,16 @@ pub trait ToFromWasmEvent { event: wit::v0_1::pumpkin::plugin::event::Event, state: &mut PluginHostState, ) -> Self; + + fn apply_wasm_event( + &mut self, + event: wit::v0_1::pumpkin::plugin::event::Event, + state: &mut PluginHostState, + ) where + Self: Sized, + { + *self = Self::from_wasm_event(event, state); + } } pub(super) const fn to_wasm_position(position: Vector3) -> pumpkin::plugin::common::Position { @@ -244,7 +254,7 @@ impl EventHandler for WasmPluginEventHandler { .call_handle_event(&mut *store, self.handler_id, server, &wasm_event) .await { - *event = E::from_wasm_event(returned_event, store.data_mut()); + event.apply_wasm_event(returned_event, store.data_mut()); } } } diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/server.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/server.rs index 308abf715..1b30584e7 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/server.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/events/server.rs @@ -7,13 +7,14 @@ use crate::plugin::{ generated_packets, pumpkin::plugin::event::{ ClientboundPacket, Event, PacketReceivedEventData, PacketSentEventData, - ServerBroadcastEventData, ServerCommandEventData, ServerLoadEventData, - ServerLoadType, ServerTickEndEventData, ServerTickStartEventData, - ServerboundPacket, + ServerBroadcastEventData, ServerCommandEventData, ServerListPingAddress, + ServerListPingEventData, ServerLoadEventData, ServerLoadType, + ServerTickEndEventData, ServerTickStartEventData, ServerboundPacket, }, }, }, server::{ + list_ping::ServerListPingEvent, packet::{PacketReceivedEvent, PacketSentEvent}, server_broadcast::ServerBroadcastEvent, server_command::ServerCommandEvent, @@ -154,6 +155,51 @@ impl ToFromWasmEvent for ServerBroadcastEvent { } } +impl ToFromWasmEvent for ServerListPingEvent { + fn to_wasm_event(&self, _state: &mut PluginHostState) -> Event { + Event::ServerListPingEvent(ServerListPingEventData { + hostname: self.hostname().to_string(), + address: ServerListPingAddress { + host: self.address().host().to_string(), + port: self.address().port(), + }, + motd: self.motd.clone(), + max_players: self.max_players, + num_players: self.num_players, + favicon: self.favicon.clone(), + }) + } + + fn from_wasm_event(event: Event, _state: &mut PluginHostState) -> Self { + match event { + Event::ServerListPingEvent(data) => Self { + hostname: data.hostname, + address: crate::plugin::api::events::server::list_ping::ServerListPingAddress::new( + data.address.host, + data.address.port, + ), + motd: data.motd, + max_players: data.max_players, + num_players: data.num_players, + favicon: data.favicon, + }, + _ => panic!("unexpected event type"), + } + } + + fn apply_wasm_event(&mut self, event: Event, _state: &mut PluginHostState) { + match event { + Event::ServerListPingEvent(data) => { + self.motd = data.motd; + self.max_players = data.max_players; + self.num_players = data.num_players; + self.favicon = data.favicon; + } + _ => panic!("unexpected event type"), + } + } +} + impl ToFromWasmEvent for ServerLoadEvent { fn to_wasm_event(&self, _state: &mut PluginHostState) -> Event { Event::ServerLoadEvent(ServerLoadEventData { diff --git a/crates/pumpkin/src/server/connection_cache.rs b/crates/pumpkin/src/server/connection_cache.rs index a65e4624e..76f234b51 100644 --- a/crates/pumpkin/src/server/connection_cache.rs +++ b/crates/pumpkin/src/server/connection_cache.rs @@ -90,7 +90,7 @@ impl CachedStatus { } } - pub fn get_status_packet(&self, client_protocol: i32) -> CStatusResponse { + pub fn get_status_response(&self, client_protocol: i32) -> StatusResponse { let mut response = self.status_response.clone(); let supported_min = LOWEST_SUPPORTED_MC_VERSION.protocol_version(); @@ -103,8 +103,12 @@ impl CachedStatus { version.protocol = client_protocol as u32; } - let json = serde_json::to_string(&response).unwrap_or_default(); + response + } + pub fn get_status_packet(&self, client_protocol: i32) -> CStatusResponse { + let response = self.get_status_response(client_protocol); + let json = serde_json::to_string(&response).unwrap_or_default(); CStatusResponse::new(json) }