Skip to main content

steel_core/player/connection/
java.rs

1//! This module contains the `JavaConnection` struct, which is used to represent a connection to a Java client.
2use std::io::Cursor;
3use std::sync::Arc;
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use steel_protocol::packet_reader::TCPNetworkDecoder;
7use steel_protocol::packet_traits::{ClientPacket, CompressionInfo, EncodedPacket, ServerPacket};
8use steel_protocol::packet_writer::TCPNetworkEncoder;
9use steel_protocol::packets::common::{
10    CDisconnect, CKeepAlive, CPongResponse, SClientInformation, SCustomPayload, SKeepAlive,
11    SPingRequest,
12};
13use steel_protocol::packets::game::{
14    CBundleDelimiter, CCommandSuggestions, ClientCommandAction, PlayerAction, PlayerCommandAction,
15    SAcceptTeleportation, SAttack, SChangeDifficulty, SChangeGameMode, SChat, SChatAck,
16    SChatCommand, SChatSessionUpdate, SChunkBatchReceived, SClientCommand, SClientTickEnd,
17    SCommandSuggestion, SContainerButtonClick, SContainerClick, SContainerClose,
18    SContainerSlotStateChanged, SInteract, SMovePlayer, SMovePlayerPos, SMovePlayerPosRot,
19    SMovePlayerRot, SMovePlayerStatusOnly, SMoveVehicle, SPickItemFromBlock, SPlayerAbilities,
20    SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoad, SPunch, SRenameItem,
21    SSeenAdvancement, SSetBeacon, SSetCarriedItem, SSetCreativeModeSlot, SSignUpdate,
22    SSpectatorAction, SUseItem, SUseItemOn,
23};
24
25use crate::command::{handle_client_request, sender::CommandSender};
26use steel_protocol::utils::{ConnectionProtocol, PacketError, RawPacket};
27use steel_registry::packets::play;
28use steel_utils::locks::{AsyncMutex, SyncMutex};
29use steel_utils::translations;
30use text_components::content::Resolvable;
31use text_components::custom::CustomData;
32use text_components::resolving::TextResolutor;
33use text_components::{Modifier, TextComponent, format::Color};
34use tokio::io::{AsyncRead, AsyncWrite, BufReader, BufWriter};
35use tokio::select;
36use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, error::TryRecvError};
37use tokio::time::timeout;
38use tokio_util::sync::CancellationToken;
39
40use crate::player::connection::NetworkConnection;
41use crate::player::{Player, PlayerSession};
42use crate::server::Server;
43
44/// Boxed read half of a Java client transport (a TCP socket or an in-memory pipe).
45pub type JavaTransportRead = Box<dyn AsyncRead + Send + Unpin>;
46/// Boxed write half of a Java client transport.
47pub type JavaTransportWrite = Box<dyn AsyncWrite + Send + Unpin>;
48/// Packet decoder over the read half of a Java client transport.
49pub type JavaNetworkReader = TCPNetworkDecoder<BufReader<JavaTransportRead>>;
50/// Shared Java socket writer.
51pub type JavaNetworkWriter =
52    Arc<AsyncMutex<Option<TCPNetworkEncoder<BufWriter<JavaTransportWrite>>>>>;
53
54const DISCONNECT_FLUSH_TIMEOUT: Duration = Duration::from_secs(1);
55
56/// Outbound packet queue message for Java connections.
57pub enum OutboundPacket {
58    /// Normal packet write that may be interrupted by connection shutdown.
59    Packet(EncodedPacket),
60    /// Final disconnect packet that is flushed on a bounded best-effort basis.
61    Disconnect(EncodedPacket),
62}
63
64/// A decoded play packet whose handler runs in the server's inter-tick packet phase.
65pub(crate) struct ScheduledPlayPacket(ScheduledPlayPacketKind);
66
67/// Cross-player concurrency permitted for a scheduled packet handler.
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub(crate) enum ScheduledPacketExecution {
70    /// The handler may overlap handlers for other players, but never its own player lane.
71    ///
72    /// Shared mutations must be fully linearized by their resource locks, and the handler must
73    /// tolerate cross-player execution order differing from packet submission order.
74    PlayerLocal,
75    /// The handler may overlap player-local work, but not another serialized handler. Serialized
76    /// handlers start in global packet submission order.
77    Serialized,
78    /// The handler is a global submission-order barrier and must not overlap scheduled work.
79    Exclusive,
80}
81
82enum ScheduledPlayPacketKind {
83    AcceptTeleportation(SAcceptTeleportation),
84    Attack(SAttack),
85    Interact(SInteract),
86    CustomPayload(SCustomPayload),
87    Chat(Box<SChat>),
88    ChatAck(SChatAck),
89    ChatSessionUpdate(SChatSessionUpdate),
90    ClientInformation(SClientInformation),
91    ClientTickEnd,
92    MovePlayer(SMovePlayer),
93    MoveVehicle(SMoveVehicle),
94    PlayerLoaded,
95    ChatCommand(SChatCommand),
96    CommandSuggestion(SCommandSuggestion),
97    ContainerButtonClick(SContainerButtonClick),
98    ContainerClick(SContainerClick),
99    ContainerClose(SContainerClose),
100    ContainerSlotStateChanged(SContainerSlotStateChanged),
101    SetCreativeModeSlot(SSetCreativeModeSlot),
102    PlayerInput(SPlayerInput),
103    PlayerCommand(SPlayerCommand),
104    PlayerAbilities(SPlayerAbilities),
105    RenameItem(SRenameItem),
106    UseItemOn(SUseItemOn),
107    UseItem(SUseItem),
108    SetBeacon(SSetBeacon),
109    SetCarriedItem(SSetCarriedItem),
110    Punch(SPunch),
111    PlayerAction(SPlayerAction),
112    PickItemFromBlock(SPickItemFromBlock),
113    SignUpdate(SSignUpdate),
114    SpectatorAction(SSpectatorAction),
115    ClientCommand(SClientCommand),
116    ChangeGameMode(SChangeGameMode),
117    ChangeDifficulty(SChangeDifficulty),
118    UpdateSelectedTab(SSeenAdvancement),
119}
120
121enum ImmediatePlayPacket {
122    KeepAlive(SKeepAlive),
123    PingRequest(SPingRequest),
124    ChunkBatchReceived(SChunkBatchReceived),
125    Unknown(i32),
126}
127
128enum DecodedPlayPacket {
129    Scheduled(ScheduledPlayPacket),
130    Immediate(ImmediatePlayPacket),
131}
132
133impl ScheduledPlayPacket {
134    /// Returns whether this packet acknowledges target-world synchronization.
135    pub(crate) const fn is_domain_handshake_packet(&self) -> bool {
136        matches!(
137            self.0,
138            ScheduledPlayPacketKind::AcceptTeleportation(_) | ScheduledPlayPacketKind::PlayerLoaded
139        )
140    }
141
142    /// Returns whether this is the death screen's one-shot respawn request.
143    pub(crate) const fn is_perform_respawn(&self) -> bool {
144        matches!(
145            self.0,
146            ScheduledPlayPacketKind::ClientCommand(SClientCommand {
147                action: ClientCommandAction::PerformRespawn,
148            })
149        )
150    }
151
152    #[cfg(test)]
153    pub(crate) const fn perform_respawn_for_test() -> Self {
154        Self(ScheduledPlayPacketKind::ClientCommand(SClientCommand {
155            action: ClientCommandAction::PerformRespawn,
156        }))
157    }
158
159    /// Returns the handler's audited cross-player concurrency class.
160    ///
161    /// This match is intentionally exhaustive so every newly implemented packet requires an
162    /// explicit concurrency decision.
163    pub(crate) const fn execution(&self) -> ScheduledPacketExecution {
164        match &self.0 {
165            // These handlers touch player-owned state or use an individually linearizable shared
166            // operation. The player lane preserves same-player order.
167            ScheduledPlayPacketKind::ChatSessionUpdate(_)
168            | ScheduledPlayPacketKind::ClientInformation(_)
169            | ScheduledPlayPacketKind::ClientTickEnd
170            | ScheduledPlayPacketKind::PlayerLoaded
171            | ScheduledPlayPacketKind::ChatCommand(_)
172            | ScheduledPlayPacketKind::CommandSuggestion(_)
173            | ScheduledPlayPacketKind::ContainerClose(_)
174            | ScheduledPlayPacketKind::SetCreativeModeSlot(_)
175            | ScheduledPlayPacketKind::PlayerInput(_)
176            | ScheduledPlayPacketKind::PlayerAbilities(_)
177            | ScheduledPlayPacketKind::SetCarriedItem(_)
178            | ScheduledPlayPacketKind::Punch(_)
179            | ScheduledPlayPacketKind::PickItemFromBlock(_)
180            | ScheduledPlayPacketKind::ClientCommand(_) => ScheduledPacketExecution::PlayerLocal,
181            ScheduledPlayPacketKind::PlayerCommand(packet) => match packet.action {
182                PlayerCommandAction::StartSprinting
183                | PlayerCommandAction::StopSprinting
184                | PlayerCommandAction::StartFallFlying => ScheduledPacketExecution::PlayerLocal,
185                PlayerCommandAction::LeaveBed => ScheduledPacketExecution::Serialized,
186                // These handlers are not implemented, so their eventual vehicle transaction
187                // cannot yet be audited against concurrently player-local work.
188                PlayerCommandAction::StartRidingJump
189                | PlayerCommandAction::StopRidingJump
190                | PlayerCommandAction::OpenVehicleInventory => ScheduledPacketExecution::Exclusive,
191            },
192            ScheduledPlayPacketKind::PlayerAction(packet) => match packet.action {
193                PlayerAction::AbortDestroyBlock | PlayerAction::SwapItemWithOffhand => {
194                    ScheduledPacketExecution::PlayerLocal
195                }
196                PlayerAction::StartDestroyBlock
197                | PlayerAction::ChangeDestroyDirection
198                | PlayerAction::StopDestroyBlock
199                | PlayerAction::DropAllItems
200                | PlayerAction::DropItem => ScheduledPacketExecution::Serialized,
201                // Active-use release may invoke item behavior and mutate inventory, while stab
202                // spans independently locked targets; neither can overlap player-local work.
203                PlayerAction::ReleaseUseItem | PlayerAction::Stab => {
204                    ScheduledPacketExecution::Exclusive
205                }
206            },
207            // Position, world, menu, chat, and domain mutations may overlap player-local work but
208            // retain one global mutation order matching the packet submission order.
209            ScheduledPlayPacketKind::AcceptTeleportation(_)
210            | ScheduledPlayPacketKind::Chat(_)
211            | ScheduledPlayPacketKind::ChatAck(_)
212            | ScheduledPlayPacketKind::MovePlayer(_)
213            | ScheduledPlayPacketKind::MoveVehicle(_)
214            | ScheduledPlayPacketKind::ContainerClick(_)
215            | ScheduledPlayPacketKind::RenameItem(_)
216            | ScheduledPlayPacketKind::UseItemOn(_)
217            | ScheduledPlayPacketKind::UseItem(_)
218            | ScheduledPlayPacketKind::SignUpdate(_)
219            | ScheduledPlayPacketKind::SpectatorAction(_)
220            | ScheduledPlayPacketKind::ChangeGameMode(_)
221            | ScheduledPlayPacketKind::ChangeDifficulty(_)
222            | ScheduledPlayPacketKind::UpdateSelectedTab(_) => ScheduledPacketExecution::Serialized,
223            // Combat spans source and target state, custom payloads have no constrained resource
224            // contract, and the unimplemented menu handlers have no auditable transaction yet.
225            ScheduledPlayPacketKind::Attack(_)
226            | ScheduledPlayPacketKind::Interact(_)
227            | ScheduledPlayPacketKind::CustomPayload(_)
228            | ScheduledPlayPacketKind::ContainerButtonClick(_)
229            | ScheduledPlayPacketKind::SetBeacon(_)
230            | ScheduledPlayPacketKind::ContainerSlotStateChanged(_) => {
231                ScheduledPacketExecution::Exclusive
232            }
233        }
234    }
235
236    pub(crate) const fn can_process_before_join(&self) -> bool {
237        matches!(
238            &self.0,
239            ScheduledPlayPacketKind::AcceptTeleportation(_)
240                | ScheduledPlayPacketKind::ClientInformation(_)
241                | ScheduledPlayPacketKind::ClientTickEnd
242                | ScheduledPlayPacketKind::CustomPayload(_)
243                | ScheduledPlayPacketKind::ChatAck(_)
244                | ScheduledPlayPacketKind::ChatSessionUpdate(_)
245                | ScheduledPlayPacketKind::PlayerLoaded
246        )
247    }
248
249    #[expect(
250        clippy::too_many_lines,
251        reason = "flat dispatch over every scheduled packet kind"
252    )]
253    pub(crate) fn handle(self, player: Arc<Player>, server: &Arc<Server>) {
254        if !player.has_joined_world() && !self.can_process_before_join() {
255            return;
256        }
257
258        match self.0 {
259            ScheduledPlayPacketKind::AcceptTeleportation(packet) => {
260                player.handle_accept_teleportation(packet);
261            }
262            ScheduledPlayPacketKind::Attack(packet) => player.handle_attack(packet),
263            ScheduledPlayPacketKind::Interact(packet) => player.handle_interact(packet),
264            ScheduledPlayPacketKind::CustomPayload(packet) => {
265                player.handle_custom_payload(packet);
266            }
267            ScheduledPlayPacketKind::Chat(packet) => {
268                player.handle_chat(*packet);
269            }
270            ScheduledPlayPacketKind::ChatAck(packet) => player.handle_chat_ack(packet),
271            ScheduledPlayPacketKind::ChatSessionUpdate(packet) => {
272                player.handle_chat_session_update(packet);
273            }
274            ScheduledPlayPacketKind::ClientInformation(packet) => {
275                player.handle_client_information(packet);
276            }
277            ScheduledPlayPacketKind::ClientTickEnd => player.handle_client_tick_end(),
278            ScheduledPlayPacketKind::MovePlayer(packet) => player.handle_move_player(packet),
279            ScheduledPlayPacketKind::MoveVehicle(packet) => player.handle_move_vehicle(packet),
280            ScheduledPlayPacketKind::PlayerLoaded => {
281                if player.mark_client_loaded_from_network() {
282                    player.send_inventory_to_remote();
283                }
284            }
285            ScheduledPlayPacketKind::ChatCommand(packet) => {
286                player.reset_last_action_time();
287                if server
288                    .submit_command(CommandSender::Player(Arc::clone(&player)), packet.command)
289                    .is_err()
290                {
291                    player.send_message(
292                        &TextComponent::const_plain("Command queue is full").color(Color::Red),
293                    );
294                }
295                player.detect_command_rate_spam();
296            }
297            ScheduledPlayPacketKind::CommandSuggestion(packet) => {
298                if server
299                    .submit_command_suggestions(Arc::clone(&player), packet.id, packet.command)
300                    .is_err()
301                {
302                    player.send_packet(CCommandSuggestions::new(packet.id, 0, 0, Vec::new()));
303                }
304            }
305            ScheduledPlayPacketKind::ContainerButtonClick(packet) => {
306                player.handle_container_button_click(packet);
307            }
308            ScheduledPlayPacketKind::SetBeacon(packet) => {
309                player.handle_set_beacon_packet(packet);
310            }
311            ScheduledPlayPacketKind::ContainerClick(packet) => {
312                player.handle_container_click(packet);
313            }
314            ScheduledPlayPacketKind::ContainerClose(packet) => {
315                player.handle_container_close(packet);
316            }
317            ScheduledPlayPacketKind::ContainerSlotStateChanged(packet) => {
318                player.handle_container_slot_state_changed(packet);
319            }
320            ScheduledPlayPacketKind::SetCreativeModeSlot(packet) => {
321                player.handle_set_creative_mode_slot(packet);
322            }
323            ScheduledPlayPacketKind::PlayerInput(packet) => player.handle_player_input(packet),
324            ScheduledPlayPacketKind::PlayerCommand(packet) => {
325                player.handle_player_command(packet);
326            }
327            ScheduledPlayPacketKind::PlayerAbilities(packet) => {
328                player.handle_player_abilities(packet);
329            }
330            ScheduledPlayPacketKind::RenameItem(packet) => player.handle_rename_item(packet),
331            ScheduledPlayPacketKind::UseItemOn(packet) => player.handle_use_item_on(packet),
332            ScheduledPlayPacketKind::UseItem(packet) => player.handle_use_item(packet),
333            ScheduledPlayPacketKind::SetCarriedItem(packet) => {
334                player.handle_set_carried_item(packet);
335            }
336            ScheduledPlayPacketKind::Punch(packet) => player.handle_punch(&packet),
337            ScheduledPlayPacketKind::PlayerAction(packet) => {
338                player.handle_player_action(packet);
339            }
340            ScheduledPlayPacketKind::PickItemFromBlock(packet) => {
341                player.handle_pick_item_from_block(packet);
342            }
343            ScheduledPlayPacketKind::SignUpdate(packet) => player.handle_sign_update(packet),
344            ScheduledPlayPacketKind::SpectatorAction(packet) => {
345                player.handle_spectator_action(packet);
346            }
347            ScheduledPlayPacketKind::ClientCommand(packet) => {
348                player.handle_client_command(packet.action);
349            }
350            ScheduledPlayPacketKind::ChangeGameMode(packet) => {
351                handle_client_request(&player, server, packet.gamemode);
352            }
353            ScheduledPlayPacketKind::ChangeDifficulty(packet) => {
354                player.handle_change_difficulty(packet.difficulty);
355            }
356            ScheduledPlayPacketKind::UpdateSelectedTab(packet) => {
357                player.handle_seen_advancement(packet);
358            }
359        }
360    }
361}
362
363/// Builder for creating packet bundles.
364///
365/// Used with [`JavaConnection::send_bundle`] to send multiple packets atomically.
366pub struct BundleBuilder {
367    packets: Vec<EncodedPacket>,
368    compression: Option<CompressionInfo>,
369}
370
371impl BundleBuilder {
372    /// Creates a new `BundleBuilder` with the given compression settings.
373    #[must_use]
374    pub const fn new(compression: Option<CompressionInfo>) -> Self {
375        Self {
376            packets: Vec::new(),
377            compression,
378        }
379    }
380
381    /// Adds a packet to the bundle.
382    ///
383    /// # Panics
384    /// Panics if the packet fails to encode.
385    pub fn add<P: ClientPacket>(&mut self, packet: P) {
386        let encoded = EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
387            .expect("Failed to encode packet");
388        self.packets.push(encoded);
389    }
390
391    /// Consumes the builder and returns the collected encoded packets.
392    #[must_use]
393    pub fn into_packets(self) -> Vec<EncodedPacket> {
394        self.packets
395    }
396}
397
398#[expect(
399    clippy::struct_field_names,
400    reason = "alive_ prefix is intentional to group related keep-alive fields"
401)]
402struct KeepAliveTracker {
403    alive_time: u64,
404    alive_pending: bool,
405    alive_id: u64,
406}
407
408/// A connection to a Java client.
409pub struct JavaConnection {
410    outgoing_packets: UnboundedSender<OutboundPacket>,
411    cancel_token: CancellationToken,
412    compression: Option<CompressionInfo>,
413    network_writer: JavaNetworkWriter,
414    id: u64,
415
416    session: Arc<PlayerSession>,
417    keep_alive_tracker: SyncMutex<KeepAliveTracker>,
418    latency: SyncMutex<u32>,
419}
420
421impl JavaConnection {
422    /// Creates a new `JavaConnection`.
423    pub const fn new(
424        outgoing_packets: UnboundedSender<OutboundPacket>,
425        cancel_token: CancellationToken,
426        compression: Option<CompressionInfo>,
427        network_writer: JavaNetworkWriter,
428        id: u64,
429        session: Arc<PlayerSession>,
430    ) -> Self {
431        Self {
432            outgoing_packets,
433            cancel_token,
434            compression,
435            network_writer,
436            id,
437            session,
438            keep_alive_tracker: SyncMutex::new(KeepAliveTracker {
439                alive_time: 0,
440                alive_pending: false,
441                alive_id: 0,
442            }),
443            latency: SyncMutex::new(0),
444        }
445    }
446
447    async fn write_packet_now(&self, packet: &EncodedPacket) -> Result<(), PacketError> {
448        let mut network_writer = self.network_writer.lock().await;
449        let Some(network_writer) = network_writer.as_mut() else {
450            return Err(PacketError::ConnectionClosed);
451        };
452        network_writer.write_packet(packet).await
453    }
454
455    async fn finish_disconnect(&self, disconnect_packet: Option<EncodedPacket>) {
456        let finish = async {
457            let Some(mut network_writer) = self.network_writer.lock().await.take() else {
458                return Ok(());
459            };
460            let Some(packet) = disconnect_packet else {
461                return Ok(());
462            };
463            network_writer.write_packet(&packet).await
464        };
465
466        match timeout(DISCONNECT_FLUSH_TIMEOUT, finish).await {
467            Ok(Ok(())) => {}
468            Ok(Err(error)) => log::debug!(
469                "Best-effort disconnect write for client {} failed: {error}",
470                self.id
471            ),
472            Err(_) => log::debug!(
473                "Best-effort disconnect write for client {} timed out",
474                self.id
475            ),
476        }
477    }
478
479    /// Ticks the connection.
480    pub fn tick(&self) {
481        self.keep_connection_alive();
482    }
483
484    fn keep_connection_alive(&self) {
485        let mut tracker = self.keep_alive_tracker.lock();
486        let now = SystemTime::now()
487            .duration_since(UNIX_EPOCH)
488            .expect("System time before UNIX EPOCH")
489            .as_millis() as u64;
490
491        if now - tracker.alive_time >= 15000 {
492            if tracker.alive_pending {
493                self.disconnect(translations::DISCONNECT_TIMEOUT.msg());
494            } else {
495                tracker.alive_pending = true;
496                tracker.alive_id = now;
497                tracker.alive_time = now;
498                self.send_packet(CKeepAlive::new(tracker.alive_id as i64));
499            }
500        }
501    }
502
503    /// Handles a keep alive packet.
504    #[expect(
505        clippy::cast_possible_truncation,
506        reason = "latency saturates at u32::MAX ms (~49 days), which is unreachable in practice"
507    )]
508    fn handle_keep_alive(&self, packet: SKeepAlive) {
509        let mut tracker = self.keep_alive_tracker.lock();
510        if tracker.alive_pending && packet.id as u64 == tracker.alive_id {
511            let now = SystemTime::now()
512                .duration_since(UNIX_EPOCH)
513                .expect("System time before UNIX EPOCH")
514                .as_millis() as u64;
515
516            let time = now.saturating_sub(tracker.alive_time) as u32;
517            tracker.alive_pending = false;
518            drop(tracker);
519            let mut latency = self.latency.lock();
520            *latency = (*latency * 3 + time) / 4;
521        } else {
522            self.disconnect(translations::DISCONNECT_TIMEOUT.msg());
523        }
524    }
525
526    /// Returns the current latency in milliseconds.
527    /// This is a smoothed average calculated from keep-alive round-trip times.
528    #[must_use]
529    pub fn latency(&self) -> i32 {
530        *self.latency.lock() as i32
531    }
532
533    /// Disconnects the client.
534    pub fn disconnect(&self, reason: impl Into<TextComponent>) {
535        let packet = match EncodedPacket::from_bare(
536            CDisconnect::new(&reason.into(), self),
537            self.compression,
538            ConnectionProtocol::Play,
539        ) {
540            Ok(packet) => packet,
541            Err(err) => {
542                log::warn!(
543                    "Failed to encode disconnect packet for client {}: {err}",
544                    self.id
545                );
546                self.close();
547                return;
548            }
549        };
550        if self
551            .outgoing_packets
552            .send(OutboundPacket::Disconnect(packet))
553            .is_err()
554        {
555            self.close();
556            return;
557        }
558        self.close();
559    }
560
561    /// Sends a packet to the client.
562    ///
563    /// # Panics
564    /// - If the packet fails to be encoded.
565    /// - If the packet fails to be sent through the channel.
566    pub fn send_packet<P: ClientPacket>(&self, packet: P) {
567        let packet = EncodedPacket::from_bare(packet, self.compression, ConnectionProtocol::Play)
568            .expect("Failed to encode packet");
569        if self
570            .outgoing_packets
571            .send(OutboundPacket::Packet(packet))
572            .is_err()
573        {
574            self.close();
575        }
576    }
577
578    /// Sends an encoded packet to the client.
579    ///
580    /// # Panics
581    /// - If the packet fails to be sent through the channel.
582    pub fn send_encoded_packet(&self, packet: EncodedPacket) {
583        if self
584            .outgoing_packets
585            .send(OutboundPacket::Packet(packet))
586            .is_err()
587        {
588            self.close();
589        }
590    }
591
592    /// Closes the connection.
593    pub fn close(&self) {
594        self.cancel_token.cancel();
595    }
596
597    /// Returns whether the connection is closed.
598    #[must_use]
599    pub fn closed(&self) -> bool {
600        self.cancel_token.is_cancelled()
601    }
602
603    /// Waits for the connection to be closed.
604    pub async fn wait_for_close(&self) {
605        self.cancel_token.cancelled().await;
606    }
607
608    const fn can_process_before_join(packet_id: i32) -> bool {
609        matches!(
610            packet_id,
611            play::S_ACCEPT_TELEPORTATION
612                | play::S_KEEP_ALIVE
613                | play::S_PING_REQUEST
614                | play::S_CLIENT_INFORMATION
615                | play::S_CUSTOM_PAYLOAD
616                | play::S_CHUNK_BATCH_RECEIVED
617                | play::S_CHAT_SESSION_UPDATE
618                | play::S_CHAT_ACK
619                | play::S_CLIENT_TICK_END
620                | play::S_PLAYER_LOADED
621        )
622    }
623
624    const fn can_process_during_domain_handshake(packet_id: i32) -> bool {
625        matches!(
626            packet_id,
627            play::S_ACCEPT_TELEPORTATION | play::S_CHUNK_BATCH_RECEIVED | play::S_PLAYER_LOADED
628        )
629    }
630
631    /// Decodes and dispatches one packet received from the client.
632    fn process_packet(
633        &self,
634        packet: RawPacket,
635        player: Arc<Player>,
636        server: &Server,
637    ) -> Result<(), PacketError> {
638        if !player.has_joined_world() && !Self::can_process_before_join(packet.id) {
639            return Ok(());
640        }
641
642        let payload_bytes = packet.payload().len();
643        let Some(packet) = Self::decode_domain_gated_packet(packet, &player)? else {
644            return Ok(());
645        };
646
647        match packet {
648            DecodedPlayPacket::Scheduled(packet) => {
649                server.schedule_play_packet(player, packet, payload_bytes);
650            }
651            DecodedPlayPacket::Immediate(packet) => {
652                self.handle_immediate_packet(packet);
653            }
654        }
655        Ok(())
656    }
657
658    fn decode_domain_gated_packet(
659        packet: RawPacket,
660        player: &Player,
661    ) -> Result<Option<DecodedPlayPacket>, PacketError> {
662        let maintenance_packet = matches!(packet.id, play::S_KEEP_ALIVE | play::S_PING_REQUEST);
663        let handshake_packet = Self::can_process_during_domain_handshake(packet.id);
664        if packet.id == play::S_CLIENT_COMMAND {
665            let decoded = Self::decode_play_packet(packet)?;
666            let perform_respawn = matches!(
667                &decoded,
668                DecodedPlayPacket::Scheduled(packet) if packet.is_perform_respawn()
669            );
670            if player.gate_domain_switch_packet(false, perform_respawn) {
671                return Ok(Some(decoded));
672            }
673            return Ok(None);
674        }
675
676        if !maintenance_packet && !player.gate_domain_switch_packet(handshake_packet, false) {
677            return Ok(None);
678        }
679
680        Self::decode_play_packet(packet).map(Some)
681    }
682
683    #[expect(
684        clippy::too_many_lines,
685        reason = "single match decode over all implemented play packets keeps protocol routing auditable"
686    )]
687    fn decode_play_packet(packet: RawPacket) -> Result<DecodedPlayPacket, PacketError> {
688        let data = &mut Cursor::new(packet.payload());
689        let scheduled = |packet| DecodedPlayPacket::Scheduled(ScheduledPlayPacket(packet));
690
691        Ok(match packet.id {
692            play::S_ACCEPT_TELEPORTATION => {
693                scheduled(ScheduledPlayPacketKind::AcceptTeleportation(
694                    SAcceptTeleportation::read_packet(data)?,
695                ))
696            }
697            play::S_ATTACK => {
698                scheduled(ScheduledPlayPacketKind::Attack(SAttack::read_packet(data)?))
699            }
700            play::S_INTERACT => scheduled(ScheduledPlayPacketKind::Interact(
701                SInteract::read_packet(data)?,
702            )),
703            play::S_CUSTOM_PAYLOAD => scheduled(ScheduledPlayPacketKind::CustomPayload(
704                SCustomPayload::read_packet(data)?,
705            )),
706            play::S_CHAT => scheduled(ScheduledPlayPacketKind::Chat(Box::new(SChat::read_packet(
707                data,
708            )?))),
709            play::S_CHAT_SESSION_UPDATE => scheduled(ScheduledPlayPacketKind::ChatSessionUpdate(
710                SChatSessionUpdate::read_packet(data)?,
711            )),
712            play::S_CHAT_ACK => scheduled(ScheduledPlayPacketKind::ChatAck(SChatAck::read_packet(
713                data,
714            )?)),
715            play::S_CLIENT_INFORMATION => scheduled(ScheduledPlayPacketKind::ClientInformation(
716                SClientInformation::read_packet(data)?,
717            )),
718            play::S_CLIENT_TICK_END => {
719                let _ = SClientTickEnd::read_packet(data)?;
720                scheduled(ScheduledPlayPacketKind::ClientTickEnd)
721            }
722            play::S_CHUNK_BATCH_RECEIVED => DecodedPlayPacket::Immediate(
723                ImmediatePlayPacket::ChunkBatchReceived(SChunkBatchReceived::read_packet(data)?),
724            ),
725            play::S_KEEP_ALIVE => DecodedPlayPacket::Immediate(ImmediatePlayPacket::KeepAlive(
726                SKeepAlive::read_packet(data)?,
727            )),
728            play::S_MOVE_PLAYER_POS => scheduled(ScheduledPlayPacketKind::MovePlayer(
729                SMovePlayerPos::read_packet(data)?.into(),
730            )),
731            play::S_MOVE_PLAYER_POS_ROT => scheduled(ScheduledPlayPacketKind::MovePlayer(
732                SMovePlayerPosRot::read_packet(data)?.into(),
733            )),
734            play::S_MOVE_PLAYER_ROT => scheduled(ScheduledPlayPacketKind::MovePlayer(
735                SMovePlayerRot::read_packet(data)?.into(),
736            )),
737            play::S_MOVE_PLAYER_STATUS_ONLY => scheduled(ScheduledPlayPacketKind::MovePlayer(
738                SMovePlayerStatusOnly::read_packet(data)?.into(),
739            )),
740            play::S_MOVE_VEHICLE => scheduled(ScheduledPlayPacketKind::MoveVehicle(
741                SMoveVehicle::read_packet(data)?,
742            )),
743            play::S_PLAYER_LOADED => {
744                let _ = SPlayerLoad::read_packet(data)?;
745                scheduled(ScheduledPlayPacketKind::PlayerLoaded)
746            }
747            play::S_CHAT_COMMAND => scheduled(ScheduledPlayPacketKind::ChatCommand(
748                SChatCommand::read_packet(data)?,
749            )),
750            play::S_COMMAND_SUGGESTION => scheduled(ScheduledPlayPacketKind::CommandSuggestion(
751                SCommandSuggestion::read_packet(data)?,
752            )),
753            play::S_CONTAINER_BUTTON_CLICK => {
754                scheduled(ScheduledPlayPacketKind::ContainerButtonClick(
755                    SContainerButtonClick::read_packet(data)?,
756                ))
757            }
758            play::S_CONTAINER_CLICK => scheduled(ScheduledPlayPacketKind::ContainerClick(
759                SContainerClick::read_packet(data)?,
760            )),
761            play::S_CONTAINER_CLOSE => scheduled(ScheduledPlayPacketKind::ContainerClose(
762                SContainerClose::read_packet(data)?,
763            )),
764            play::S_CONTAINER_SLOT_STATE_CHANGED => {
765                scheduled(ScheduledPlayPacketKind::ContainerSlotStateChanged(
766                    SContainerSlotStateChanged::read_packet(data)?,
767                ))
768            }
769            play::S_SET_CREATIVE_MODE_SLOT => {
770                scheduled(ScheduledPlayPacketKind::SetCreativeModeSlot(
771                    SSetCreativeModeSlot::read_packet(data)?,
772                ))
773            }
774            play::S_SET_BEACON => scheduled(ScheduledPlayPacketKind::SetBeacon(
775                SSetBeacon::read_packet(data)?,
776            )),
777            play::S_PLAYER_INPUT => scheduled(ScheduledPlayPacketKind::PlayerInput(
778                SPlayerInput::read_packet(data)?,
779            )),
780            play::S_PLAYER_COMMAND => scheduled(ScheduledPlayPacketKind::PlayerCommand(
781                SPlayerCommand::read_packet(data)?,
782            )),
783            play::S_PLAYER_ABILITIES => scheduled(ScheduledPlayPacketKind::PlayerAbilities(
784                SPlayerAbilities::read_packet(data)?,
785            )),
786            play::S_RENAME_ITEM => scheduled(ScheduledPlayPacketKind::RenameItem(
787                SRenameItem::read_packet(data)?,
788            )),
789            play::S_USE_ITEM_ON => scheduled(ScheduledPlayPacketKind::UseItemOn(
790                SUseItemOn::read_packet(data)?,
791            )),
792            play::S_USE_ITEM => scheduled(ScheduledPlayPacketKind::UseItem(SUseItem::read_packet(
793                data,
794            )?)),
795            play::S_SET_CARRIED_ITEM => scheduled(ScheduledPlayPacketKind::SetCarriedItem(
796                SSetCarriedItem::read_packet(data)?,
797            )),
798            play::S_PUNCH => scheduled(ScheduledPlayPacketKind::Punch(SPunch::read_packet(data)?)),
799            play::S_PLAYER_ACTION => scheduled(ScheduledPlayPacketKind::PlayerAction(
800                SPlayerAction::read_packet(data)?,
801            )),
802            play::S_PICK_ITEM_FROM_BLOCK => scheduled(ScheduledPlayPacketKind::PickItemFromBlock(
803                SPickItemFromBlock::read_packet(data)?,
804            )),
805            play::S_SIGN_UPDATE => scheduled(ScheduledPlayPacketKind::SignUpdate(
806                SSignUpdate::read_packet(data)?,
807            )),
808            play::S_SPECTATOR_ACTION => scheduled(ScheduledPlayPacketKind::SpectatorAction(
809                SSpectatorAction::read_packet(data)?,
810            )),
811            play::S_CLIENT_COMMAND => scheduled(ScheduledPlayPacketKind::ClientCommand(
812                SClientCommand::read_packet(data)?,
813            )),
814            play::S_PING_REQUEST => DecodedPlayPacket::Immediate(ImmediatePlayPacket::PingRequest(
815                SPingRequest::read_packet(data)?,
816            )),
817            play::S_CHANGE_GAME_MODE => scheduled(ScheduledPlayPacketKind::ChangeGameMode(
818                SChangeGameMode::read_packet(data)?,
819            )),
820            play::S_CHANGE_DIFFICULTY => scheduled(ScheduledPlayPacketKind::ChangeDifficulty(
821                SChangeDifficulty::read_packet(data)?,
822            )),
823            play::S_SEEN_ADVANCEMENTS => scheduled(ScheduledPlayPacketKind::UpdateSelectedTab(
824                SSeenAdvancement::read_packet(data)?,
825            )),
826            id => DecodedPlayPacket::Immediate(ImmediatePlayPacket::Unknown(id)),
827        })
828    }
829
830    fn handle_immediate_packet(&self, packet: ImmediatePlayPacket) {
831        match packet {
832            ImmediatePlayPacket::KeepAlive(packet) => self.handle_keep_alive(packet),
833            ImmediatePlayPacket::PingRequest(packet) => {
834                self.send_packet(CPongResponse::new(packet.time));
835            }
836            ImmediatePlayPacket::ChunkBatchReceived(packet) => {
837                self.session
838                    .chunk_sender()
839                    .lock()
840                    .on_chunk_batch_received_by_client(packet.desired_chunks_per_tick);
841            }
842            ImmediatePlayPacket::Unknown(id) => log::info!("play packet id {id} is not known"),
843        }
844    }
845
846    /// Listens for packets from the client.
847    pub async fn listener(&self, mut reader: JavaNetworkReader, server: Arc<Server>) {
848        loop {
849            select! {
850                () = self.wait_for_close() => {
851                    break;
852                }
853                packet = reader.get_raw_packet() => {
854                    match packet {
855                        Ok(packet) => {
856                            if let Some(player) = self.session.current_player()
857                                && let Err(err) = self.process_packet(packet, player, &server) {
858                                log::warn!(
859                                    "Failed to get packet from client {}: {err}",
860                                    self.id
861                                );
862                            }
863                        }
864                        Err(err) => {
865                            log::debug!("Failed to get raw packet from client {}: {err}", self.id);
866                            self.close();
867                        }
868                    }
869                }
870            }
871        }
872    }
873
874    /// Sends packets to the client.
875    ///
876    pub async fn sender(&self, mut sender_recv: UnboundedReceiver<OutboundPacket>) {
877        let disconnect_packet = loop {
878            select! {
879                biased;
880                () = self.wait_for_close() => {
881                    break Self::take_queued_disconnect(&mut sender_recv);
882                }
883                outbound = sender_recv.recv() => {
884                    if let Some(outbound) = outbound {
885                        let (packet, close_after_write) = match outbound {
886                            OutboundPacket::Packet(packet) => (packet, false),
887                            OutboundPacket::Disconnect(packet) => (packet, true),
888                        };
889
890                        if close_after_write {
891                            self.close();
892                            break Some(packet);
893                        }
894
895                        let write_result = self.write_packet_now(&packet);
896                        select! {
897                            biased;
898                            () = self.wait_for_close() => {
899                                break Self::take_queued_disconnect(&mut sender_recv);
900                            },
901                            result = write_result => {
902                                if let Err(err) = result {
903                                    log::warn!("Failed to send packet to client {}: {err}", self.id);
904                                    self.close();
905                                    break None;
906                                }
907                            }
908                        }
909                    } else {
910                        //log::warn!(
911                        //    "Internal packet_sender_recv channel closed for client {}",
912                        //    self.id
913                        //);
914                        self.close();
915                        break None;
916                    }
917                }
918            }
919        };
920
921        self.finish_disconnect(disconnect_packet).await;
922    }
923
924    fn take_queued_disconnect(
925        sender_recv: &mut UnboundedReceiver<OutboundPacket>,
926    ) -> Option<EncodedPacket> {
927        let mut disconnect_packet = None;
928        loop {
929            match sender_recv.try_recv() {
930                Ok(OutboundPacket::Packet(_)) => {}
931                Ok(OutboundPacket::Disconnect(packet)) => disconnect_packet = Some(packet),
932                Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
933            }
934        }
935        disconnect_packet
936    }
937}
938
939impl TextResolutor for JavaConnection {
940    fn resolve_content(&self, _resolvable: &Resolvable) -> TextComponent {
941        TextComponent::new()
942    }
943
944    fn resolve_custom(&self, _data: &CustomData) -> Option<TextComponent> {
945        None
946    }
947
948    fn translate(&self, _key: &str) -> Option<String> {
949        None
950    }
951}
952
953impl NetworkConnection for JavaConnection {
954    fn compression(&self) -> Option<CompressionInfo> {
955        self.compression
956    }
957
958    fn send_encoded(&self, packet: EncodedPacket) {
959        self.send_encoded_packet(packet);
960    }
961
962    fn send_encoded_bundle(&self, packets: Vec<EncodedPacket>) {
963        self.send_packet(CBundleDelimiter);
964        for packet in packets {
965            self.send_encoded_packet(packet);
966        }
967        self.send_packet(CBundleDelimiter);
968    }
969
970    fn disconnect_with_reason(&self, reason: TextComponent) {
971        self.disconnect(reason);
972    }
973
974    fn tick(&self) {
975        self.keep_connection_alive();
976    }
977
978    fn latency(&self) -> i32 {
979        *self.latency.lock() as i32
980    }
981
982    fn close(&self) {
983        JavaConnection::close(self);
984    }
985
986    fn closed(&self) -> bool {
987        self.cancel_token.is_cancelled()
988    }
989}
990
991#[cfg(test)]
992mod tests {
993    use std::array;
994
995    use crate::{
996        entity::{Entity as _, LivingEntity as _},
997        test_support::{TestPlayerBuilder, fresh_test_world},
998    };
999    use rustc_hash::FxHashMap;
1000    use steel_protocol::packets::common::{ChatVisibility, HumanoidArm, ParticleStatus};
1001    use steel_protocol::packets::game::{ClickType, ClientCommandAction, HashedStack};
1002    use steel_registry::{blocks::properties::Direction, item_stack::ItemStack};
1003    use steel_utils::{BlockPos, codec::VarInt, types::SignTextSlot};
1004    use tokio::{
1005        io::{AsyncReadExt as _, duplex},
1006        sync::mpsc,
1007        task,
1008    };
1009    use uuid::Uuid;
1010
1011    use super::*;
1012
1013    fn decode(packet: RawPacket) -> DecodedPlayPacket {
1014        let Ok(decoded) = JavaConnection::decode_play_packet(packet) else {
1015            panic!("test play packet should decode");
1016        };
1017        decoded
1018    }
1019
1020    fn execution(kind: ScheduledPlayPacketKind) -> ScheduledPacketExecution {
1021        ScheduledPlayPacket(kind).execution()
1022    }
1023
1024    #[test]
1025    fn pre_join_custom_payload_uses_serverbound_play_packet_id() {
1026        assert!(JavaConnection::can_process_before_join(
1027            play::S_CUSTOM_PAYLOAD
1028        ));
1029        assert!(!JavaConnection::can_process_before_join(
1030            play::C_CUSTOM_PAYLOAD
1031        ));
1032    }
1033
1034    #[test]
1035    fn queued_domain_switch_records_only_perform_respawn_at_connection_gate() {
1036        let world_fixture = fresh_test_world("queued_domain_switch_respawn_packet");
1037        let world = &world_fixture.world;
1038        let player = TestPlayerBuilder::new(Arc::clone(world), "RespawnTester", 1).build();
1039        let Some(token) = player.begin_pending_world_change() else {
1040            panic!("test player should acquire a world-change token");
1041        };
1042        assert!(player.begin_domain_switch(token));
1043        player.set_health(0.0);
1044
1045        let request_stats = JavaConnection::decode_domain_gated_packet(
1046            RawPacket::new(
1047                play::S_CLIENT_COMMAND,
1048                vec![ClientCommandAction::RequestStats as u8],
1049            ),
1050            &player,
1051        );
1052        assert!(matches!(request_stats, Ok(None)));
1053        assert!(!player.has_deferred_death_respawn_for_test());
1054
1055        let perform_respawn = JavaConnection::decode_domain_gated_packet(
1056            RawPacket::new(
1057                play::S_CLIENT_COMMAND,
1058                vec![ClientCommandAction::PerformRespawn as u8],
1059            ),
1060            &player,
1061        );
1062        assert!(matches!(perform_respawn, Ok(None)));
1063        assert!(player.has_deferred_death_respawn_for_test());
1064
1065        assert!(player.finish_domain_switch(token));
1066        assert!(player.finish_pending_world_change(token));
1067    }
1068
1069    #[test]
1070    fn custom_payload_defaults_to_global_exclusive_scheduling() {
1071        let channel = b"minecraft:brand";
1072        let mut payload = vec![channel.len() as u8];
1073        payload.extend_from_slice(channel);
1074        payload.extend_from_slice(b"steel");
1075        let decoded = decode(RawPacket::new(play::S_CUSTOM_PAYLOAD, payload));
1076        let DecodedPlayPacket::Scheduled(
1077            packet @ ScheduledPlayPacket(ScheduledPlayPacketKind::CustomPayload(_)),
1078        ) = decoded
1079        else {
1080            panic!("custom payload should use the scheduled packet path");
1081        };
1082
1083        assert_eq!(packet.execution(), ScheduledPacketExecution::Exclusive);
1084    }
1085
1086    #[test]
1087    fn pre_join_allows_initial_play_acknowledgements() {
1088        assert!(JavaConnection::can_process_before_join(
1089            play::S_ACCEPT_TELEPORTATION
1090        ));
1091        assert!(JavaConnection::can_process_before_join(
1092            play::S_CHUNK_BATCH_RECEIVED
1093        ));
1094        assert!(JavaConnection::can_process_before_join(
1095            play::S_PLAYER_LOADED
1096        ));
1097        assert!(JavaConnection::can_process_during_domain_handshake(
1098            play::S_ACCEPT_TELEPORTATION
1099        ));
1100        assert!(JavaConnection::can_process_during_domain_handshake(
1101            play::S_CHUNK_BATCH_RECEIVED
1102        ));
1103        assert!(JavaConnection::can_process_during_domain_handshake(
1104            play::S_PLAYER_LOADED
1105        ));
1106        assert!(!JavaConnection::can_process_during_domain_handshake(
1107            play::S_MOVE_PLAYER_POS
1108        ));
1109    }
1110
1111    #[test]
1112    fn scheduled_domain_handshake_classification_is_narrow() {
1113        // Teleport id, then the acknowledged position and rotation.
1114        let mut accept_payload = vec![0];
1115        accept_payload.extend_from_slice(&[0; size_of::<f64>() * 3 + size_of::<f32>() * 2]);
1116        let accept = decode(RawPacket::new(play::S_ACCEPT_TELEPORTATION, accept_payload));
1117        let DecodedPlayPacket::Scheduled(accept) = accept else {
1118            panic!("teleport acknowledgement should be scheduled");
1119        };
1120        assert!(accept.is_domain_handshake_packet());
1121
1122        let client_tick_end = decode(RawPacket::new(play::S_CLIENT_TICK_END, Vec::new()));
1123        let DecodedPlayPacket::Scheduled(client_tick_end) = client_tick_end else {
1124            panic!("client tick end should be scheduled");
1125        };
1126        assert!(!client_tick_end.is_domain_handshake_packet());
1127    }
1128
1129    #[test]
1130    fn client_tick_end_is_scheduled_for_the_inter_tick_phase() {
1131        let decoded = decode(RawPacket::new(play::S_CLIENT_TICK_END, Vec::new()));
1132
1133        assert!(matches!(
1134            decoded,
1135            DecodedPlayPacket::Scheduled(ScheduledPlayPacket(
1136                ScheduledPlayPacketKind::ClientTickEnd
1137            ))
1138        ));
1139    }
1140
1141    #[test]
1142    fn packet_execution_classification_separates_local_and_serialized_work() {
1143        assert_eq!(
1144            execution(ScheduledPlayPacketKind::PlayerAbilities(SPlayerAbilities {
1145                flags: 0
1146            },)),
1147            ScheduledPacketExecution::PlayerLocal
1148        );
1149        assert_eq!(
1150            execution(ScheduledPlayPacketKind::MovePlayer(
1151                SMovePlayerStatusOnly { packed_byte: 0 }.into(),
1152            )),
1153            ScheduledPacketExecution::Serialized
1154        );
1155    }
1156
1157    #[test]
1158    fn inventory_execution_reflects_complete_transaction_boundaries() {
1159        let click = SContainerClick {
1160            container_id: 0,
1161            state_id: 0,
1162            slot_num: 0,
1163            button_num: 0,
1164            click_type: ClickType::Pickup,
1165            changed_slots: FxHashMap::default(),
1166            carried_item: HashedStack::Empty,
1167        };
1168
1169        assert_eq!(
1170            execution(ScheduledPlayPacketKind::ContainerClick(click)),
1171            ScheduledPacketExecution::Serialized
1172        );
1173        assert_eq!(
1174            execution(ScheduledPlayPacketKind::ContainerClose(SContainerClose {
1175                container_id: 0,
1176            })),
1177            ScheduledPacketExecution::PlayerLocal
1178        );
1179        assert_eq!(
1180            execution(ScheduledPlayPacketKind::SetCreativeModeSlot(
1181                SSetCreativeModeSlot {
1182                    slot_num: 1,
1183                    item_stack: ItemStack::empty(),
1184                },
1185            )),
1186            ScheduledPacketExecution::PlayerLocal
1187        );
1188    }
1189
1190    #[test]
1191    fn player_command_execution_is_action_sensitive() {
1192        let command = |action| {
1193            execution(ScheduledPlayPacketKind::PlayerCommand(SPlayerCommand {
1194                entity_id: 1,
1195                action,
1196                data: 0,
1197            }))
1198        };
1199
1200        assert_eq!(
1201            command(PlayerCommandAction::StartSprinting),
1202            ScheduledPacketExecution::PlayerLocal
1203        );
1204        assert_eq!(
1205            command(PlayerCommandAction::StartFallFlying),
1206            ScheduledPacketExecution::PlayerLocal
1207        );
1208        assert_eq!(
1209            command(PlayerCommandAction::LeaveBed),
1210            ScheduledPacketExecution::Serialized
1211        );
1212        assert_eq!(
1213            command(PlayerCommandAction::OpenVehicleInventory),
1214            ScheduledPacketExecution::Exclusive
1215        );
1216    }
1217
1218    #[test]
1219    fn player_action_execution_is_action_sensitive() {
1220        let action = |action| {
1221            execution(ScheduledPlayPacketKind::PlayerAction(SPlayerAction {
1222                action,
1223                pos: BlockPos::new(0, 64, 0),
1224                direction: Direction::Down,
1225                sequence: 0,
1226            }))
1227        };
1228
1229        assert_eq!(
1230            action(PlayerAction::AbortDestroyBlock),
1231            ScheduledPacketExecution::PlayerLocal
1232        );
1233        assert_eq!(
1234            action(PlayerAction::SwapItemWithOffhand),
1235            ScheduledPacketExecution::PlayerLocal
1236        );
1237        assert_eq!(
1238            action(PlayerAction::StartDestroyBlock),
1239            ScheduledPacketExecution::Serialized
1240        );
1241        assert_eq!(
1242            action(PlayerAction::Stab),
1243            ScheduledPacketExecution::Exclusive
1244        );
1245    }
1246
1247    #[test]
1248    fn chat_message_and_ack_share_the_serialized_commit_lane() {
1249        assert_eq!(
1250            execution(ScheduledPlayPacketKind::Chat(Box::new(SChat {
1251                message: "hello".to_owned(),
1252                timestamp: 0,
1253                salt: 0,
1254                signature: None,
1255                offset: 0,
1256                acknowledged: [0; 3],
1257                checksum: 0,
1258            }))),
1259            ScheduledPacketExecution::Serialized
1260        );
1261        assert_eq!(
1262            execution(ScheduledPlayPacketKind::ChatAck(SChatAck {
1263                offset: VarInt(0),
1264            })),
1265            ScheduledPacketExecution::Serialized
1266        );
1267    }
1268
1269    #[test]
1270    fn cross_player_and_unimplemented_handlers_remain_global_barriers() {
1271        assert_eq!(
1272            execution(ScheduledPlayPacketKind::Attack(SAttack { entity_id: 1 })),
1273            ScheduledPacketExecution::Exclusive
1274        );
1275        assert_eq!(
1276            execution(ScheduledPlayPacketKind::ContainerButtonClick(
1277                SContainerButtonClick {
1278                    container_id: 1,
1279                    button_id: 0,
1280                },
1281            )),
1282            ScheduledPacketExecution::Exclusive
1283        );
1284        assert_eq!(
1285            execution(ScheduledPlayPacketKind::PlayerAction(SPlayerAction {
1286                action: PlayerAction::ReleaseUseItem,
1287                pos: BlockPos::new(0, 64, 0),
1288                direction: Direction::Down,
1289                sequence: 0,
1290            })),
1291            ScheduledPacketExecution::Exclusive
1292        );
1293    }
1294
1295    #[test]
1296    fn audited_handlers_use_the_narrowest_safe_execution_class() {
1297        assert_eq!(
1298            execution(ScheduledPlayPacketKind::AcceptTeleportation(
1299                SAcceptTeleportation {
1300                    teleport_id: 1,
1301                    x: 0.0,
1302                    y: 64.0,
1303                    z: 0.0,
1304                    y_rot: 0.0,
1305                    x_rot: 0.0,
1306                },
1307            )),
1308            ScheduledPacketExecution::Serialized
1309        );
1310        assert_eq!(
1311            execution(ScheduledPlayPacketKind::PlayerInput(SPlayerInput {
1312                flags: 0,
1313            })),
1314            ScheduledPacketExecution::PlayerLocal
1315        );
1316        assert_eq!(
1317            execution(ScheduledPlayPacketKind::ChatSessionUpdate(
1318                SChatSessionUpdate {
1319                    session_id: Uuid::nil(),
1320                    expires_at: 0,
1321                    public_key: Vec::new(),
1322                    key_signature: Vec::new(),
1323                },
1324            )),
1325            ScheduledPacketExecution::PlayerLocal
1326        );
1327        assert_eq!(
1328            execution(ScheduledPlayPacketKind::ClientInformation(
1329                SClientInformation {
1330                    language: "en_us".to_owned(),
1331                    view_distance: 8,
1332                    chat_visibility: ChatVisibility::Full,
1333                    chat_colors: true,
1334                    model_customization: 0,
1335                    main_hand: HumanoidArm::Right,
1336                    text_filtering_enabled: false,
1337                    allows_listing: true,
1338                    particle_status: ParticleStatus::All,
1339                },
1340            )),
1341            ScheduledPacketExecution::PlayerLocal
1342        );
1343        assert_eq!(
1344            execution(ScheduledPlayPacketKind::ChatCommand(SChatCommand {
1345                command: "help".to_owned(),
1346            })),
1347            ScheduledPacketExecution::PlayerLocal
1348        );
1349        assert_eq!(
1350            execution(ScheduledPlayPacketKind::PickItemFromBlock(
1351                SPickItemFromBlock {
1352                    pos: BlockPos::new(0, 64, 0),
1353                    include_data: false,
1354                },
1355            )),
1356            ScheduledPacketExecution::PlayerLocal
1357        );
1358        assert_eq!(
1359            execution(ScheduledPlayPacketKind::SignUpdate(SSignUpdate {
1360                pos: BlockPos::new(0, 64, 0),
1361                lines: array::from_fn(|_| String::new()),
1362                slot: SignTextSlot::Front,
1363            })),
1364            ScheduledPacketExecution::Serialized
1365        );
1366        assert_eq!(
1367            execution(ScheduledPlayPacketKind::Punch(SPunch {})),
1368            ScheduledPacketExecution::PlayerLocal
1369        );
1370        assert_eq!(
1371            execution(ScheduledPlayPacketKind::ClientCommand(SClientCommand {
1372                action: ClientCommandAction::PerformRespawn,
1373            })),
1374            ScheduledPacketExecution::PlayerLocal
1375        );
1376    }
1377
1378    #[test]
1379    fn keep_alive_remains_on_the_immediate_connection_path() {
1380        let decoded = decode(RawPacket::new(
1381            play::S_KEEP_ALIVE,
1382            42_i64.to_be_bytes().to_vec(),
1383        ));
1384
1385        assert!(matches!(
1386            decoded,
1387            DecodedPlayPacket::Immediate(ImmediatePlayPacket::KeepAlive(SKeepAlive { id: 42 }))
1388        ));
1389    }
1390
1391    #[test]
1392    fn chunk_batch_ack_uses_the_immediate_connection_path() {
1393        let decoded = decode(RawPacket::new(
1394            play::S_CHUNK_BATCH_RECEIVED,
1395            12.5_f32.to_be_bytes().to_vec(),
1396        ));
1397
1398        assert!(matches!(
1399            decoded,
1400            DecodedPlayPacket::Immediate(ImmediatePlayPacket::ChunkBatchReceived(
1401                SChunkBatchReceived {
1402                    desired_chunks_per_tick: 12.5
1403                }
1404            ))
1405        ));
1406    }
1407
1408    #[tokio::test]
1409    async fn sender_writes_packets_through_a_boxed_transport() {
1410        let (server_end, mut client_end) = duplex(1024);
1411        let transport: JavaTransportWrite = Box::new(server_end);
1412        let network_writer: JavaNetworkWriter = Arc::new(AsyncMutex::new(Some(
1413            TCPNetworkEncoder::new(BufWriter::new(transport)),
1414        )));
1415        let (outgoing_packets, outgoing_receiver) = mpsc::unbounded_channel();
1416        let cancel_token = CancellationToken::new();
1417        let connection = Arc::new(JavaConnection::new(
1418            outgoing_packets,
1419            cancel_token.clone(),
1420            None,
1421            network_writer,
1422            1,
1423            Arc::new(PlayerSession::new(10, 10)),
1424        ));
1425        let sender = task::spawn({
1426            let connection = Arc::clone(&connection);
1427            async move { connection.sender(outgoing_receiver).await }
1428        });
1429
1430        let Ok(packet) =
1431            EncodedPacket::from_bare(CKeepAlive { id: 7 }, None, ConnectionProtocol::Play)
1432        else {
1433            panic!("keep alive should encode");
1434        };
1435        let expected = packet.encoded_data.as_slice().to_vec();
1436        connection.send_encoded(packet);
1437
1438        let mut received = vec![0; expected.len()];
1439        let Ok(_) = client_end.read_exact(&mut received).await else {
1440            panic!("client end should receive the framed packet");
1441        };
1442        assert_eq!(received, expected);
1443
1444        cancel_token.cancel();
1445        let Ok(()) = sender.await else {
1446            panic!("sender task should finish after cancellation");
1447        };
1448    }
1449}