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::{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}