From a8ea55f39a4db1ed736b02b085223752e6018ca7 Mon Sep 17 00:00:00 2001 From: Hiuoy <119734912+Hiuoy@users.noreply.github.com> Date: Thu, 5 Nov 2026 20:15:14 +0400 Subject: [PATCH] Native hooks: ClientPlayerNetworkEvent.LoggingIn, and LoggingOut's label A client mod hears the window's player log in to a server, single player's own included: native_client_player_network_logging_in(), at the end of the Login arm (ClientPacketListener.handleLogin), after the new client level. Respawns and dimension changes don't log in again. Horizon makes the player with the server's first position, so native_player() is None there; native_send_packet reaches the server. client_disconnecting, which main fires before a connection is discarded (Minecraft.disconnect), gets the catalog label ClientPlayerNetworkEvent.LoggingOut, so horizon-port-pack events finds it. The docs describe the pair. Checked in game with horizon-ctl and a local probe mod: one login on opening a world and one on reopening it, none on a trip to the Nether or a respawn, and a logout on Save and Quit and on quitting the game. Fixes #10008 Co-Authored-By: Claude Opus 5.5 --- crates/horizon-client/src/game/native_hooks.rs | 16 ++++++++++++++++ crates/horizon-client/src/game/remote.rs | 2 ++ docs/native-mods.md | 13 +++++++++++++ native-hooks.json | 2 ++ .../client_player_network_logging_in.json | 16 ++++++++++++++++ 5 files changed, 49 insertions(+) create mode 100644 tools/native-hook-fragments/client_player_network_logging_in.json diff --git a/crates/horizon-client/src/game/native_hooks.rs b/crates/horizon-client/src/game/native_hooks.rs index a469da3cb8..3c6c4d1b4a 100644 --- a/crates/horizon-client/src/game/native_hooks.rs +++ b/crates/horizon-client/src/game/native_hooks.rs @@ -2194,9 +2194,25 @@ impl Game<'_> { mod_api::dispatch_client_start(self, mod_api::mods()); } /// Before an actual connection is discarded, not on a dimension change. + /// NeoForge's `ClientPlayerNetworkEvent.LoggingOut`, told here after + /// `native_client_level_unload` and only when a connection ends. pub(super) fn native_client_disconnecting(&self) { mod_api::dispatch_client_disconnecting(self, mod_api::mods()); } + /// The window's player logs in to a server (NeoForge's + /// `ClientPlayerNetworkEvent.LoggingIn`, in + /// `ClientPacketListener.handleLogin`), single player's own server or a + /// remote one: once per login packet, after the new client level + /// (`native_client_level_load`). Respawns and dimension changes don't log + /// in again; a server that sends its player back through configuration + /// does, without a `native_client_disconnecting` between. Horizon makes + /// the player with the server's first position, so `native_player` is + /// None here. A mod may send the server its own packets + /// (`native_send_packet`). Client only. + /// API @API:client_player_network_logging_in@. + pub(super) fn native_client_player_network_logging_in(&mut self) { + mod_api::dispatch_client_player_network_logging_in(self, mod_api::mods()); + } /// Once before this client's final save/exit. A mod can flush and join its worker here. pub(super) fn native_client_stopping(&self) { mod_api::dispatch_client_stopping(self, mod_api::mods()); diff --git a/crates/horizon-client/src/game/remote.rs b/crates/horizon-client/src/game/remote.rs index b7c7eb200a..da3a7aa04b 100644 --- a/crates/horizon-client/src/game/remote.rs +++ b/crates/horizon-client/src/game/remote.rs @@ -3252,6 +3252,8 @@ impl Game<'_> { false, horizon_client::level_loading::Reason::Other, ); + // handleLogin: ClientPlayerNetworkEvent.LoggingIn, the new level made. + self.native_client_player_network_logging_in(); // No ClientInformation here: the configuration phase sent it, // and ClientPacketListener.broadcastClientInformation sends it // again only when an option changes it. diff --git a/docs/native-mods.md b/docs/native-mods.md index 51acd502ab..197244ade9 100644 --- a/docs/native-mods.md +++ b/docs/native-mods.md @@ -2933,6 +2933,19 @@ game runs, for seasons (a golden autumn, a pale winter) or a world that greens a - A season's mood in a shader pack (warmer grading, a lower sun, more haze) comes from the same mod through `native_shader_params` (`frame.mods`); its server half can send the season to each client with mod messages. +### Logging in and out + +`native_client_player_network_logging_in()` (client) is NeoForge's `ClientPlayerNetworkEvent.LoggingIn`: the window's +player logs in to a server, single player's own included. It fires once per login packet +(`ClientPacketListener.handleLogin`), after the new client level (`native_client_level_load`). Respawns and dimension +changes don't log in again; a server that sends its player back through configuration logs it in again, with no logout +between. Horizon makes the player with the server's first position, so `native_player()` is None here, but the +connection is up: `native_send_packet` reaches the server. The counterpart is `native_client_disconnecting()`, +NeoForge's `ClientPlayerNetworkEvent.LoggingOut`. It is told before a connection is discarded, after +`native_client_level_unload`, and only when a connection ends: NeoForge's documentation says its event may also fire as +a new integrated server is created, and Horizon's doesn't. A mod keeps its per-server state between the two: caches, a +handshake with its server half, a welcome message. Bundles need `"hooks": @API:client_player_network_logging_in@`. + ### Sounds a client mod plays `native_play_local_sound(sound, at, volume, pitch, delay)` (client) plays sound event `sound` at `at` for this player only, diff --git a/native-hooks.json b/native-hooks.json index 453d8a80e3..45dee04fab 100644 --- a/native-hooks.json +++ b/native-hooks.json @@ -5048,7 +5048,9 @@ "name": "client_disconnecting", "version": 1, "target": "horizon_client::game::native_hooks::Game<'_>::native_client_disconnecting", + "neoforge": "ClientPlayerNetworkEvent.LoggingOut (only when a connection ends)", "side": "client", + "cancellable": false, "mod_api": { "contract": "listen" } diff --git a/tools/native-hook-fragments/client_player_network_logging_in.json b/tools/native-hook-fragments/client_player_network_logging_in.json new file mode 100644 index 0000000000..caef62da42 --- /dev/null +++ b/tools/native-hook-fragments/client_player_network_logging_in.json @@ -0,0 +1,16 @@ +{ + "hooks": [ + { + "name": "client_player_network_logging_in", + "target": "horizon_client::game::native_hooks::Game<'_>::native_client_player_network_logging_in", + "neoforge": "ClientPlayerNetworkEvent.LoggingIn", + "side": "client", + "cancellable": false, + "mod_api": { + "contract": "listen" + } + } + ], + "rust_summary": "the window's player logging in to a server (`native_client_player_network_logging_in`).", + "doc_summary": "the window's player logging in to a server, single player's own included: `native_client_player_network_logging_in()`, NeoForge's `ClientPlayerNetworkEvent.LoggingIn`, once per login packet after the new client level, not on respawns or dimension changes. Its counterpart for logging out is `native_client_disconnecting` ([Logging in and out](#logging-in-and-out)). Bundles need \"hooks\": @API:client_player_network_logging_in@." +}