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(
236 self.gameprofile.id(),
237 show_hat,
238 ));
239 }
240
241 // Vanilla does not echo CSetChunkCacheRadius here; it is only broadcast
242 // when the server-wide view distance changes.
243 if old_view_distance != self.view_distance() {
244 self.get_world().chunk_map.update_player_status(self);
245 }
246 }
247
248 /// Returns the player's client information settings.
249 #[must_use]
250 pub fn client_information(&self) -> ClientInformation {
251 self.client_information.lock().clone()
252 }
253
254 /// Updates the player's client information settings.
255 pub fn set_client_information(&self, info: ClientInformation) {
256 Self::apply_client_information_to_entity_data(&mut self.entity_data.lock(), &info);
257 *self.client_information.lock() = info;
258 }
259
260 /// Returns the effective view distance for this player.
261 ///
262 /// This is the minimum of the client's requested view distance and
263 /// the server's configured maximum view distance.
264 #[must_use]
265 pub fn view_distance(&self) -> u8 {
266 let client_view_distance = self.client_information.lock().view_distance;
267 client_view_distance.min(self.world.load().view_distance)
268 }
269}