Native hooks: a player's plugin channel message (custom_payload_from_client, #10461)

native_custom_payload_from_client(player, channel, data) hears a guest's
ServerboundCustomPayloadPacket in play on any channel that isn't Horizon's own
(horizon:mod_net, horizon:quiet_commands, horizon:lod), which the server drops
today. Like mod messages it waits for the tick's end, with the hub back, so
every helper works; it is heard from a dead player too, as vanilla's
handleCustomPayload doesn't look at health. Horizon's own channels are handled
as before. The client side is #6774.
This commit is contained in:
Detimes committed 2026-11-06 06:51:55 +00:00
1 parent 01fec61d2d
commit 27200f64cd
5 files changed
+76 -1

No files matched your search

@@ -1943,6 +1943,20 @@ impl Game<'_> {
let Some(gi) = host.hub.guests.iter().position(|g| g.conn == conn) else {
return;
};
// A plugin channel's message on a channel that isn't one of Horizon's own (the match below
// takes those) is for the mods, from a dead player too: vanilla's handleCustomPayload doesn't
// look at health.
let packet = match packet {
S::CustomPayload(p)
if p.channel != horizon_protocol::quiet::CHANNEL
&& p.channel != horizon_protocol::mod_net::CHANNEL
&& p.channel != horizon_client::lod_net::CHANNEL =>
{
self.guest_custom_payload(host, conn, p.channel, p.data);
return;
}
packet => packet,
};
// A dead player acts no more, but the death screen still respawns and chats: its
// commands run as anyone's (ServerGamePacketListenerImpl.handleChatCommand doesn't
// look at the player's health).
@@ -6512,6 +6512,25 @@ impl Game<'_> {
) {
mod_api::dispatch_player_level_change(self, mod_api::mods(), player, name, from, to);
}
/// A plugin channel message from a player's client
/// (`ServerboundCustomPayloadPacket` in play) on any channel that isn't
/// Horizon's own (`horizon:mod_net`, `horizon:quiet_commands`,
/// `horizon:lod`), which vanilla drops: `channel` as the client sent it
/// (`voicechat:request_secret`), `data` its bytes, checked by nothing.
/// Runs in the tick after it arrives, in the order the messages came,
/// with the hub back and no player's state swapped in, so every helper
/// works; `native_send_custom_payload_to_player` answers on a channel of
/// any name. Heard from a dead player too. Server-side, for every guest
/// (a single player world's own player sends none). The client side is
/// #6774. API @API:custom_payload_from_client@.
pub(super) fn native_custom_payload_from_client(
&mut self,
player: u128,
channel: &str,
data: &[u8],
) {
mod_api::dispatch_custom_payload_from_client(self, mod_api::mods(), player, channel, data);
}
/// A click in a menu a mod opened (`native_open_menu`), which moved
/// nothing: the menu's `slot` (its own first, then the player's
/// inventory, `native_menu_inventory_slot`; -999 outside), the `button`
+21 -1
View File
@@ -28,6 +28,9 @@ pub(super) struct NativeNet {
to_players: Vec<(u128, Vec<u8>)>,
to_counts: HashMap<u128, u32>,
pub(super) to_server: u32,
/// Plugin channel messages from players on channels that aren't Horizon's own, for
/// `native_custom_payload_from_client`: player, channel, data.
payloads_from: Vec<(u128, String, Vec<u8>)>,
}
/// A message's payload as `native_net_send_*` check it.
@@ -59,9 +62,23 @@ impl Game<'_> {
}
}
/// A plugin channel message from `conn` on a channel that isn't Horizon's own: queued for
/// `run_native_net`, which hands it to `native_custom_payload_from_client`.
pub(super) fn guest_custom_payload(
&mut self,
host: &LanHost,
conn: u32,
channel: String,
data: Vec<u8>,
) {
if let Some(player) = host.hub.guest(conn).map(|g| g.uuid) {
self.native_net.payloads_from.push((player, channel, data));
}
}
/// Whether messages from players wait for `run_native_net`.
pub(super) fn native_net_waiting(&self) -> bool {
!self.native_net.from_players.is_empty()
!self.native_net.from_players.is_empty() || !self.native_net.payloads_from.is_empty()
}
/// The waiting messages reach the mods, with the hub back in the game.
@@ -69,6 +86,9 @@ impl Game<'_> {
for (player, m) in std::mem::take(&mut self.native_net.from_players) {
self.native_net_from_client(player, &m.channel, &m.data);
}
for (player, channel, data) in std::mem::take(&mut self.native_net.payloads_from) {
self.native_custom_payload_from_client(player, &channel, &data);
}
}
/// What the mods sent this tick leaves; the tick's allowances start over.
+7
View File
@@ -2501,6 +2501,13 @@ before (#10461).
With proxy forwarding on (`forwarding.secret`, offline mode) the server reads the handshake's host up to 32767
characters, as Paper does behind a proxy, so a Floodgate player's long data reaches the hook; without forwarding the
limit stays vanilla's 255.
- **`native_custom_payload_from_client(player, channel, data)`**: a player's plugin channel message
(`ServerboundCustomPayloadPacket` in play) on any channel that isn't Horizon's own (`horizon:mod_net`,
`horizon:quiet_commands`, `horizon:lod`), which vanilla drops: a Simple Voice Chat client's `voicechat:*` requests,
a proxy's or a client mod's channels. `data` is the client's bytes, checked by nothing. It runs in the tick after the
message arrived, in arrival order, with the hub back and no player's state swapped in, so every helper works; it is
heard from a dead player too. Every guest is heard (a single player world's own player sends none). The client side,
a window's own plugin channels, is #6774. Bundles need `"hooks": @API:custom_payload_from_client@`.
- **`native_perform_command(player, command, ran)`**: a player typed a registered command (without its slash) and
their level allows it, whether the world allows commands or not. The mod parses the line and sets `ran`. The
@@ -0,0 +1,15 @@
{
"hooks": [
{
"name": "custom_payload_from_client",
"target": "horizon_client::game::native_hooks::Game<'_>::native_custom_payload_from_client",
"neoforge": "none (NeoForge registers payload handlers per type; Bukkit's PluginMessageListener)",
"side": "server",
"mod_api": {
"contract": "listen"
}
}
],
"rust_summary": "a player's plugin channel message on a channel that isn't Horizon's own (`native_custom_payload_from_client`).",
"doc_summary": "`native_custom_payload_from_client(player, channel, data)` hears a player's `ServerboundCustomPayloadPacket` in play on any channel that isn't Horizon's own, which vanilla drops: in the tick after it arrives, with no player's state swapped in, from a dead player too. Bundles need \"hooks\": @API:custom_payload_from_client@."
}