Add ServerListPingEvent (#2736)

* feat(events): add ServerListPingEvent

* Update pumpkin-plugin-wit to 50962bf
This commit is contained in:
Noël Hagestein
2026-08-10 11:32:02 +02:00
committed by GitHub
parent 604d597709
commit fcb26179d3
13 changed files with 300 additions and 17 deletions

View File

@@ -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::*;

View File

@@ -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)
}
}

View File

@@ -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;

View File

@@ -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");
}
}

View File

@@ -227,7 +227,7 @@ impl PendingConnection {
async fn handle_status_packet(
&mut self,
server: &Server,
server: &Arc<Server>,
packet: &RawPacket,
) -> Result<Option<PacketHandlerResult>, ReadingError> {
debug!("Handling status group");

View File

@@ -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<Server>) {
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) {

View File

@@ -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<SocketAddr> {
(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<String>,
}
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<String>,
) -> 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
}
}

View File

@@ -1,3 +1,4 @@
pub mod list_ping;
pub mod packet;
pub mod server_broadcast;
pub mod server_command;

View File

@@ -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::<ServerCommandEvent>(resource, handler, priority, blocking).await;
}
EventType::ServerListPingEvent => {
register_typed_event::<ServerListPingEvent>(resource, handler, priority, blocking)
.await;
}
EventType::ServerBroadcastEvent => {
register_typed_event::<ServerBroadcastEvent>(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

View File

@@ -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<f64>) -> pumpkin::plugin::common::Position {
@@ -244,7 +254,7 @@ impl<E: Payload + ToFromWasmEvent> EventHandler<E> 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());
}
}
}

View File

@@ -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 {

View File

@@ -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)
}