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