Skip to main content

steel_core/player/connection/
mod.rs

1//! This module contains the `PlayerConnection` trait that abstracts network connections.
2//!
3//! The trait is object-safe to allow using `dyn PlayerConnection` for both real network
4//! connections (`JavaConnection`) and test connections (`FlintConnection`).
5
6mod java;
7
8pub use java::{
9    BundleBuilder, JavaConnection, JavaNetworkReader, JavaNetworkWriter, JavaTransportRead,
10    JavaTransportWrite, OutboundPacket,
11};
12pub(crate) use java::{ScheduledPacketExecution, ScheduledPlayPacket};
13
14use enum_dispatch::enum_dispatch;
15use steel_protocol::packet_traits::{ClientPacket, CompressionInfo, EncodedPacket};
16use steel_protocol::packets::common::{
17    ChatVisibility, HumanoidArm, ParticleStatus, SClientInformation,
18};
19use steel_protocol::packets::game::CPlayerInfoUpdate;
20use steel_protocol::utils::ConnectionProtocol;
21use text_components::TextComponent;
22
23use crate::player::Player;
24
25/// Client-side settings sent via `SClientInformation` packet.
26/// This is stored separately from the packet struct to allow default initialization.
27#[derive(Debug, Clone)]
28pub struct ClientInformation {
29    /// The client's language (e.g., "`en_us"`).
30    pub language: String,
31    /// The client's requested view distance in chunks.
32    pub view_distance: u8,
33    /// Chat visibility setting.
34    pub chat_visibility: ChatVisibility,
35    /// Whether chat colors are enabled.
36    pub chat_colors: bool,
37    /// Bitmask for displayed skin parts.
38    pub model_customization: u8,
39    /// The player's main hand (left or right).
40    pub main_hand: HumanoidArm,
41    /// Whether text filtering is enabled.
42    pub text_filtering_enabled: bool,
43    /// Whether the player appears in the server list.
44    pub allows_listing: bool,
45    /// Particle rendering setting.
46    pub particle_status: ParticleStatus,
47}
48
49impl Default for ClientInformation {
50    fn default() -> Self {
51        Self {
52            language: "en_us".to_string(),
53            view_distance: 8,
54            chat_visibility: ChatVisibility::Full,
55            chat_colors: true,
56            model_customization: 0,
57            main_hand: HumanoidArm::Right,
58            text_filtering_enabled: false,
59            allows_listing: true,
60            particle_status: ParticleStatus::All,
61        }
62    }
63}
64
65/// An object-safe trait for player connections.
66///
67/// This abstracts the connection layer so that:
68/// - `JavaConnection` can handle real network traffic
69/// - Test connections (like `FlintConnection`) can record events for assertions
70///
71/// # Object Safety
72///
73/// This trait uses type erasure for packet sending - packets must be pre-encoded
74/// into `EncodedPacket` before being sent. The `Player` struct provides a generic
75/// `send_packet<P: ClientPacket>()` helper that handles encoding.
76#[enum_dispatch]
77pub trait NetworkConnection: Send + Sync {
78    /// Returns compression info for packet encoding.
79    ///
80    /// Returns `None` if compression is disabled (e.g., for test connections).
81    fn compression(&self) -> Option<CompressionInfo>;
82
83    /// Sends a pre-encoded packet.
84    ///
85    /// This is the object-safe method that accepts already-encoded packets.
86    /// Use `Player::send_packet()` for the generic version that handles encoding.
87    fn send_encoded(&self, packet: EncodedPacket);
88
89    /// Sends multiple pre-encoded packets as an atomic bundle.
90    ///
91    /// The implementation wraps the packets with bundle delimiter packets so
92    /// the client processes them together in a single game tick.
93    /// Use `Player::send_bundle()` for the generic version that handles encoding.
94    fn send_encoded_bundle(&self, packets: Vec<EncodedPacket>);
95
96    /// Disconnects the player with a reason.
97    fn disconnect_with_reason(&self, reason: TextComponent);
98
99    /// Performs per-tick connection maintenance (e.g., keep-alive).
100    fn tick(&self);
101
102    /// Returns the current latency in milliseconds.
103    fn latency(&self) -> i32;
104
105    /// Closes the connection.
106    fn close(&self);
107
108    /// Returns whether the connection is closed.
109    fn closed(&self) -> bool;
110}
111
112/// Concrete player connection type using `enum_dispatch` for zero-cost dispatch.
113///
114/// The `Java` variant handles real Java connections while `Other` supports tests
115/// and alternative backends.
116#[enum_dispatch(NetworkConnection)]
117pub enum PlayerConnection {
118    /// A real Java client connection.
119    Java(JavaConnection),
120    /// A dynamic connection for tests or other backends.
121    Other(Box<dyn NetworkConnection>),
122}
123
124impl NetworkConnection for Box<dyn NetworkConnection> {
125    fn compression(&self) -> Option<CompressionInfo> {
126        (**self).compression()
127    }
128
129    fn send_encoded(&self, packet: EncodedPacket) {
130        (**self).send_encoded(packet);
131    }
132
133    fn send_encoded_bundle(&self, packets: Vec<EncodedPacket>) {
134        (**self).send_encoded_bundle(packets);
135    }
136
137    fn disconnect_with_reason(&self, reason: TextComponent) {
138        (**self).disconnect_with_reason(reason);
139    }
140
141    fn tick(&self) {
142        (**self).tick();
143    }
144
145    fn latency(&self) -> i32 {
146        (**self).latency()
147    }
148
149    fn close(&self) {
150        (**self).close();
151    }
152
153    fn closed(&self) -> bool {
154        (**self).closed()
155    }
156}
157
158impl Player {
159    /// Sends a packet to the player's connection.
160    ///
161    /// This is a generic helper that encodes the packet and delegates to the
162    /// connection's `send_encoded` method, enabling object-safe packet sending.
163    ///
164    /// # Panics
165    ///
166    /// Panics if the packet fails to encode.
167    pub fn send_packet<P: ClientPacket>(&self, packet: P) {
168        let encoded = EncodedPacket::from_bare(
169            packet,
170            self.connection.compression(),
171            ConnectionProtocol::Play,
172        )
173        .expect("Failed to encode packet");
174        self.connection.send_encoded(encoded);
175    }
176
177    /// Sends multiple packets as an atomic bundle.
178    ///
179    /// The closure receives a [`BundleBuilder`] to add packets to.
180    /// All packets are encoded, then sent wrapped in bundle delimiters so the
181    /// client processes them together in a single game tick.
182    pub fn send_bundle<F>(&self, f: F)
183    where
184        F: FnOnce(&mut BundleBuilder),
185    {
186        let mut builder = BundleBuilder::new(self.connection.compression());
187        f(&mut builder);
188        let packets = builder.into_packets();
189        if !packets.is_empty() {
190            self.connection.send_encoded_bundle(packets);
191        }
192    }
193
194    /// Disconnects the player with a reason message.
195    pub fn disconnect(&self, reason: impl Into<TextComponent>) {
196        self.connection.disconnect_with_reason(reason.into());
197    }
198
199    /// Marks the player's connection as closed without sending a disconnect
200    /// packet.
201    ///
202    /// Used during shutdown so that container-close logic treats the player as
203    /// disconnected — dropping open-menu contents into the world instead of the
204    /// saved inventory, matching vanilla's removal-on-shutdown behavior.
205    pub fn close_connection(&self) {
206        self.connection.close();
207    }
208
209    /// Handles client information updates during play phase.
210    pub fn handle_client_information(&self, packet: SClientInformation) {
211        let old_view_distance = self.view_distance();
212        let was_hat_shown = self.shows_hat();
213
214        // TODO: Centralize the minimum with config validation when zero view distance is supported.
215        let info = ClientInformation {
216            language: packet.language,
217            view_distance: packet
218                .view_distance
219                .max(2)
220                .cast_unsigned()
221                .min(self.config.view_distance.max(2)),
222            chat_visibility: packet.chat_visibility,
223            chat_colors: packet.chat_colors,
224            model_customization: packet.model_customization,
225            main_hand: packet.main_hand,
226            text_filtering_enabled: packet.text_filtering_enabled,
227            allows_listing: packet.allows_listing,
228            particle_status: packet.particle_status,
229        };
230        self.set_client_information(info);
231
232        let show_hat = self.shows_hat();
233        if show_hat != was_hat_shown {
234            self.server()
235                .broadcast_to_online(CPlayerInfoUpdate::update_hat(self.gameprofile.id, show_hat));
236        }
237
238        // Vanilla does not echo CSetChunkCacheRadius here; it is only broadcast
239        // when the server-wide view distance changes.
240        if old_view_distance != self.view_distance() {
241            self.get_world().chunk_map.update_player_status(self);
242        }
243    }
244
245    /// Returns the player's client information settings.
246    #[must_use]
247    pub fn client_information(&self) -> ClientInformation {
248        self.client_information.lock().clone()
249    }
250
251    /// Updates the player's client information settings.
252    pub fn set_client_information(&self, info: ClientInformation) {
253        Self::apply_client_information_to_entity_data(&mut self.entity_data.lock(), &info);
254        *self.client_information.lock() = info;
255    }
256
257    /// Returns the effective view distance for this player.
258    ///
259    /// This is the minimum of the client's requested view distance and
260    /// the server's configured maximum view distance.
261    #[must_use]
262    pub fn view_distance(&self) -> u8 {
263        let client_view_distance = self.client_information.lock().view_distance;
264        client_view_distance.min(self.world.load().view_distance)
265    }
266}