1use 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
44pub type JavaTransportRead = Box<dyn AsyncRead + Send + Unpin>;
46pub type JavaTransportWrite = Box<dyn AsyncWrite + Send + Unpin>;
48pub type JavaNetworkReader = TCPNetworkDecoder<BufReader<JavaTransportRead>>;
50pub type JavaNetworkWriter =
52 Arc<AsyncMutex<Option<TCPNetworkEncoder<BufWriter<JavaTransportWrite>>>>>;
53
54const DISCONNECT_FLUSH_TIMEOUT: Duration = Duration::from_secs(1);
55
56pub enum OutboundPacket {
58 Packet(EncodedPacket),
60 Disconnect(EncodedPacket),
62}
63
64pub(crate) struct ScheduledPlayPacket(ScheduledPlayPacketKind);
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub(crate) enum ScheduledPacketExecution {
70 PlayerLocal,
75 Serialized,
78 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 pub(crate) const fn is_domain_handshake_packet(&self) -> bool {
136 matches!(
137 self.0,
138 ScheduledPlayPacketKind::AcceptTeleportation(_) | ScheduledPlayPacketKind::PlayerLoaded
139 )
140 }
141
142 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 pub(crate) const fn execution(&self) -> ScheduledPacketExecution {
164 match &self.0 {
165 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 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 PlayerAction::ReleaseUseItem | PlayerAction::Stab => {
204 ScheduledPacketExecution::Exclusive
205 }
206 },
207 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 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
363pub struct BundleBuilder {
367 packets: Vec<EncodedPacket>,
368 compression: Option<CompressionInfo>,
369}
370
371impl BundleBuilder {
372 #[must_use]
374 pub const fn new(compression: Option<CompressionInfo>) -> Self {
375 Self {
376 packets: Vec::new(),
377 compression,
378 }
379 }
380
381 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 #[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
408pub 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 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 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 #[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 #[must_use]
529 pub fn latency(&self) -> i32 {
530 *self.latency.lock() as i32
531 }
532
533 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 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 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 pub fn close(&self) {
594 self.cancel_token.cancel();
595 }
596
597 #[must_use]
599 pub fn closed(&self) -> bool {
600 self.cancel_token.is_cancelled()
601 }
602
603 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 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 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 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 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 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}