diff --git a/assets/bedrock/LICENSE-GEYSER b/assets/bedrock/LICENSE-GEYSER new file mode 100644 index 000000000..bec80d34c --- /dev/null +++ b/assets/bedrock/LICENSE-GEYSER @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2019-2026 GeyserMC. http://geysermc.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/assets/bedrock/README.md b/assets/bedrock/README.md index 68b953463..2bdda1c38 100644 --- a/assets/bedrock/README.md +++ b/assets/bedrock/README.md @@ -19,3 +19,9 @@ This directory contains a number of different data files used to help support co - `biomes.json` - downloaded from [GeyserMC/mappings](https://github.com/GeyserMC/mappings) - Maps Java Edition biome identifiers to their corresponding Bedrock Edition biome ID. Used in code generation to translate Java biomes to Bedrock counterparts. +- `player_geometry.json` + - adapted from [GeyserMC/Geyser](https://github.com/GeyserMC/Geyser/blob/master/core/src/main/resources/bedrock/geometries/geo.json) (MIT License) + - Provides valid standard wide and slim Bedrock player geometry for Java Edition player skins. + - See `LICENSE-GEYSER` for the source license and copyright notice. + +The Java-to-Bedrock skin conversion also uses Geyser's required-opacity masks under the same license. diff --git a/assets/bedrock/player_geometry.json b/assets/bedrock/player_geometry.json new file mode 100644 index 000000000..343989d72 --- /dev/null +++ b/assets/bedrock/player_geometry.json @@ -0,0 +1 @@ +{"format_version":"1.12.0","minecraft:geometry":[{"bones":[{"name":"body","parent":"waist","pivot":[0.0,24.0,0.0]},{"name":"waist","pivot":[0.0,12.0,0.0]},{"cubes":[{"origin":[-5.0,8.0,3.0],"size":[10,16,1],"uv":[0,0]}],"name":"cape","parent":"body","pivot":[0.0,24.0,3.0],"rotation":[0.0,180.0,0.0]}],"description":{"identifier":"geometry.cape","texture_height":32,"texture_width":64}},{"bones":[{"name":"root","pivot":[0.0,0.0,0.0]},{"cubes":[{"origin":[-4.0,12.0,-2.0],"size":[8,12,4],"uv":[16,16]}],"name":"body","parent":"waist","pivot":[0.0,24.0,0.0]},{"name":"waist","parent":"root","pivot":[0.0,12.0,0.0]},{"cubes":[{"origin":[-4.0,24.0,-4.0],"size":[8,8,8],"uv":[0,0]}],"name":"head","parent":"body","pivot":[0.0,24.0,0.0]},{"name":"cape","parent":"body","pivot":[0.0,24,3.0]},{"cubes":[{"inflate":0.50,"origin":[-4.0,24.0,-4.0],"size":[8,8,8],"uv":[32,0]}],"name":"hat","parent":"head","pivot":[0.0,24.0,0.0]},{"cubes":[{"origin":[4.0,12.0,-2.0],"size":[4,12,4],"uv":[32,48]}],"name":"leftArm","parent":"body","pivot":[5.0,22.0,0.0]},{"cubes":[{"inflate":0.250,"origin":[4.0,12.0,-2.0],"size":[4,12,4],"uv":[48,48]}],"name":"leftSleeve","parent":"leftArm","pivot":[5.0,22.0,0.0]},{"name":"leftItem","parent":"leftArm","pivot":[6.0,15.0,1.0]},{"cubes":[{"origin":[-8.0,12.0,-2.0],"size":[4,12,4],"uv":[40,16]}],"name":"rightArm","parent":"body","pivot":[-5.0,22.0,0.0]},{"cubes":[{"inflate":0.250,"origin":[-8.0,12.0,-2.0],"size":[4,12,4],"uv":[40,32]}],"name":"rightSleeve","parent":"rightArm","pivot":[-5.0,22.0,0.0]},{"locators":{"lead_hold":[-6,15,1]},"name":"rightItem","parent":"rightArm","pivot":[-6,15,1]},{"cubes":[{"origin":[-0.10,0.0,-2.0],"size":[4,12,4],"uv":[16,48]}],"name":"leftLeg","parent":"root","pivot":[1.90,12.0,0.0]},{"cubes":[{"inflate":0.250,"origin":[-0.10,0.0,-2.0],"size":[4,12,4],"uv":[0,48]}],"name":"leftPants","parent":"leftLeg","pivot":[1.90,12.0,0.0]},{"cubes":[{"origin":[-3.90,0.0,-2.0],"size":[4,12,4],"uv":[0,16]}],"name":"rightLeg","parent":"root","pivot":[-1.90,12.0,0.0]},{"cubes":[{"inflate":0.250,"origin":[-3.90,0.0,-2.0],"size":[4,12,4],"uv":[0,32]}],"name":"rightPants","parent":"rightLeg","pivot":[-1.90,12.0,0.0]},{"cubes":[{"inflate":0.250,"origin":[-4.0,12.0,-2.0],"size":[8,12,4],"uv":[16,32]}],"name":"jacket","parent":"body","pivot":[0.0,24.0,0.0]}],"description":{"identifier":"geometry.humanoid.custom","texture_height":64,"texture_width":64,"visible_bounds_height":2,"visible_bounds_offset":[0,1,0],"visible_bounds_width":1}},{"bones":[{"name":"root","pivot":[0.0,0.0,0.0]},{"name":"waist","parent":"root","pivot":[0.0,12.0,0.0]},{"cubes":[{"origin":[-4.0,12.0,-2.0],"size":[8,12,4],"uv":[16,16]}],"name":"body","parent":"waist","pivot":[0.0,24.0,0.0]},{"cubes":[{"origin":[-4.0,24.0,-4.0],"size":[8,8,8],"uv":[0,0]}],"name":"head","parent":"body","pivot":[0.0,24.0,0.0]},{"cubes":[{"inflate":0.50,"origin":[-4.0,24.0,-4.0],"size":[8,8,8],"uv":[32,0]}],"name":"hat","parent":"head","pivot":[0.0,24.0,0.0]},{"cubes":[{"origin":[-3.90,0.0,-2.0],"size":[4,12,4],"uv":[0,16]}],"name":"rightLeg","parent":"root","pivot":[-1.90,12.0,0.0]},{"cubes":[{"inflate":0.250,"origin":[-3.90,0.0,-2.0],"size":[4,12,4],"uv":[0,32]}],"name":"rightPants","parent":"rightLeg","pivot":[-1.90,12.0,0.0]},{"cubes":[{"origin":[-0.10,0.0,-2.0],"size":[4,12,4],"uv":[16,48]}],"name":"leftLeg","parent":"root","pivot":[1.90,12.0,0.0]},{"cubes":[{"inflate":0.250,"origin":[-0.10,0.0,-2.0],"size":[4,12,4],"uv":[0,48]}],"name":"leftPants","parent":"leftLeg","pivot":[1.90,12.0,0.0]},{"cubes":[{"origin":[4.0,11.50,-2.0],"size":[3,12,4],"uv":[32,48]}],"name":"leftArm","parent":"body","pivot":[5.0,21.50,0.0]},{"cubes":[{"inflate":0.250,"origin":[4.0,11.50,-2.0],"size":[3,12,4],"uv":[48,48]}],"name":"leftSleeve","parent":"leftArm","pivot":[5.0,21.50,0.0]},{"name":"leftItem","parent":"leftArm","pivot":[6,14.50,1]},{"cubes":[{"origin":[-7.0,11.50,-2.0],"size":[3,12,4],"uv":[40,16]}],"name":"rightArm","parent":"body","pivot":[-5.0,21.50,0.0]},{"cubes":[{"inflate":0.250,"origin":[-7.0,11.50,-2.0],"size":[3,12,4],"uv":[40,32]}],"name":"rightSleeve","parent":"rightArm","pivot":[-5.0,21.50,0.0]},{"locators":{"lead_hold":[-6,14.50,1]},"name":"rightItem","parent":"rightArm","pivot":[-6,14.50,1]},{"cubes":[{"inflate":0.250,"origin":[-4.0,12.0,-2.0],"size":[8,12,4],"uv":[16,32]}],"name":"jacket","parent":"body","pivot":[0.0,24.0,0.0]},{"name":"cape","parent":"body","pivot":[0.0,24,-3.0]}],"description":{"identifier":"geometry.humanoid.customSlim","texture_height":64,"texture_width":64,"visible_bounds_height":2,"visible_bounds_offset":[0,1,0],"visible_bounds_width":1}}]} diff --git a/crates/pumpkin-protocol/src/bedrock/client/add_player.rs b/crates/pumpkin-protocol/src/bedrock/client/add_player.rs index d3e1c6404..fae1d9d54 100644 --- a/crates/pumpkin-protocol/src/bedrock/client/add_player.rs +++ b/crates/pumpkin-protocol/src/bedrock/client/add_player.rs @@ -9,7 +9,7 @@ use std::io::{Error, Write}; use uuid::Uuid; use super::{ - common::{AbilityLayer, EntityLink}, + common::{AbilityLayer, BuildPlatform, EntityLink}, set_actor_data::EntityMetadata, }; @@ -31,7 +31,7 @@ pub struct CAddPlayer { pub ability_data: AbilityData, pub links: Vec, pub device_id: String, - pub build_platform: i32, + pub build_platform: BuildPlatform, } impl PacketWrite for CAddPlayer { @@ -79,7 +79,7 @@ impl CAddPlayer { ability_data: AbilityData, links: Vec, device_id: String, - build_platform: i32, + build_platform: BuildPlatform, ) -> Self { Self { uuid, diff --git a/crates/pumpkin-protocol/src/bedrock/client/common.rs b/crates/pumpkin-protocol/src/bedrock/client/common.rs index 3651e6fa6..74f9f1b37 100644 --- a/crates/pumpkin-protocol/src/bedrock/client/common.rs +++ b/crates/pumpkin-protocol/src/bedrock/client/common.rs @@ -1,5 +1,34 @@ +use std::io::{Error, Write}; + use crate::{codec::var_long::VarLong, serial::PacketWrite}; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(i32)] +pub enum BuildPlatform { + Unknown = -1, + Google = 1, + Ios = 2, + Osx = 3, + Amazon = 4, + GearVr = 5, + Hololens = 6, + Uwp = 7, + Win32 = 8, + Dedicated = 9, + TvOs = 10, + Sony = 11, + Nx = 12, + Xbox = 13, + WindowsPhone = 14, + Linux = 15, +} + +impl PacketWrite for BuildPlatform { + fn write(&self, writer: &mut W) -> Result<(), Error> { + (*self as i32).write(writer) + } +} + #[derive(Default, Clone, PacketWrite)] pub struct AbilityLayer { pub serialized_layer: u16, diff --git a/crates/pumpkin-protocol/src/bedrock/client/player_list.rs b/crates/pumpkin-protocol/src/bedrock/client/player_list.rs index 99ea6ceef..83768e5b3 100644 --- a/crates/pumpkin-protocol/src/bedrock/client/player_list.rs +++ b/crates/pumpkin-protocol/src/bedrock/client/player_list.rs @@ -6,6 +6,16 @@ use pumpkin_macros::packet; use std::io::{Error, Write}; use uuid::Uuid; +use super::common::BuildPlatform; + +const WIDE_SKIN_RESOURCE_PATCH: &[u8] = br#"{"geometry":{"default":"geometry.humanoid.custom"}}"#; +const SLIM_SKIN_RESOURCE_PATCH: &[u8] = + br#"{"geometry":{"default":"geometry.humanoid.customSlim"}}"#; +const DEFAULT_SKIN_GEOMETRY: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../assets/bedrock/player_geometry.json" +)); + #[packet(63)] pub struct CPlayerList { pub action: u8, @@ -45,7 +55,7 @@ pub struct PlayerListEntry { pub username: String, pub xuid: String, pub platform_chat_id: String, - pub build_platform: i32, + pub build_platform: BuildPlatform, pub skin: Skin, pub is_teacher: bool, pub is_host: bool, @@ -105,7 +115,7 @@ impl Skin { Self { skin_id: "Standard_Custom".to_string(), play_fab_id: String::new(), - resource_patch: r#"{"geometry":{"default":"geometry.humanoid"}}"#.into(), + resource_patch: WIDE_SKIN_RESOURCE_PATCH.to_vec(), image_width: 64, image_height: 64, // 64 * 64 * 4 = 16384 bytes of raw RGBA data @@ -115,24 +125,36 @@ impl Skin { cape_width: 0, cape_height: 0, cape_data: Vec::new(), - geometry_data: Vec::new(), + geometry_data: DEFAULT_SKIN_GEOMETRY.to_vec(), animation_data: Vec::new(), - geometry_data_engine_version: Vec::new(), + geometry_data_engine_version: b"1.26.40".to_vec(), cape_id: String::new(), full_id: "Standard_Custom".to_string(), arm_size: "wide".to_string(), skin_color: "#0".to_string(), persona_pieces: Vec::new(), piece_tint_colors: Vec::new(), - is_premium: false, + is_premium: true, is_persona: false, persona_cape_on_classic: false, is_primary_user: false, - override_appearance: false, + override_appearance: true, is_trusted: true, profile_hash: String::new(), } } + + /// Selects the standard wide or slim player geometry while preserving the + /// rest of the serialized skin. + pub fn set_slim(&mut self, slim: bool) { + self.arm_size = if slim { "slim" } else { "wide" }.to_string(); + self.resource_patch = if slim { + SLIM_SKIN_RESOURCE_PATCH + } else { + WIDE_SKIN_RESOURCE_PATCH + } + .to_vec(); + } } impl PacketWrite for Skin { @@ -239,3 +261,35 @@ fn parse_color(color: &str) -> i32 { let value = color.trim_start_matches('#'); u32::from_str_radix(value, 16).unwrap_or_default() as i32 } + +#[cfg(test)] +mod tests { + use super::{DEFAULT_SKIN_GEOMETRY, SLIM_SKIN_RESOURCE_PATCH, Skin, WIDE_SKIN_RESOURCE_PATCH}; + + #[test] + fn fallback_skin_contains_the_geometry_it_references() { + let skin = Skin::steve(); + + assert_eq!(skin.resource_patch, WIDE_SKIN_RESOURCE_PATCH); + assert_eq!(skin.geometry_data, DEFAULT_SKIN_GEOMETRY); + assert!( + String::from_utf8_lossy(&skin.geometry_data) + .contains(r#""identifier":"geometry.humanoid.custom""#) + ); + assert!(skin.override_appearance); + assert!(skin.is_trusted); + } + + #[test] + fn slim_fallback_skin_references_the_slim_geometry() { + let mut skin = Skin::steve(); + skin.set_slim(true); + + assert_eq!(skin.arm_size, "slim"); + assert_eq!(skin.resource_patch, SLIM_SKIN_RESOURCE_PATCH); + assert!( + String::from_utf8_lossy(&skin.geometry_data) + .contains(r#""identifier":"geometry.humanoid.customSlim""#) + ); + } +} diff --git a/crates/pumpkin-protocol/src/bedrock/server/mod.rs b/crates/pumpkin-protocol/src/bedrock/server/mod.rs index 4733f8fb7..ab9fa52e3 100644 --- a/crates/pumpkin-protocol/src/bedrock/server/mod.rs +++ b/crates/pumpkin-protocol/src/bedrock/server/mod.rs @@ -13,6 +13,7 @@ pub mod loading_screen; pub mod login; pub mod mob_equipment; pub mod modal_form_response; +pub mod packet_violation_warning; pub mod player_action; pub mod player_auth_input; pub mod player_hotbar; @@ -39,6 +40,7 @@ pub use loading_screen::*; pub use login::*; pub use mob_equipment::*; pub use modal_form_response::*; +pub use packet_violation_warning::*; pub use player_action::{Action as PlayerActionType, SPlayerAction}; pub use player_auth_input::*; pub use player_hotbar::*; diff --git a/crates/pumpkin-protocol/src/bedrock/server/packet_violation_warning.rs b/crates/pumpkin-protocol/src/bedrock/server/packet_violation_warning.rs new file mode 100644 index 000000000..a9417e31c --- /dev/null +++ b/crates/pumpkin-protocol/src/bedrock/server/packet_violation_warning.rs @@ -0,0 +1,25 @@ +use std::io::{Error, Read}; + +use pumpkin_macros::packet; + +use crate::{codec::var_int::VarInt, serial::PacketRead}; + +#[derive(Debug)] +#[packet(156)] +pub struct SPacketViolationWarning { + pub violation_type: VarInt, + pub severity: VarInt, + pub packet_id: VarInt, + pub context: String, +} + +impl PacketRead for SPacketViolationWarning { + fn read(reader: &mut R) -> Result { + Ok(Self { + violation_type: VarInt::read(reader)?, + severity: VarInt::read(reader)?, + packet_id: VarInt::read(reader)?, + context: String::read(reader)?, + }) + } +} diff --git a/crates/pumpkin/src/entity/player.rs b/crates/pumpkin/src/entity/player.rs index 039478fc4..8418ca1a0 100644 --- a/crates/pumpkin/src/entity/player.rs +++ b/crates/pumpkin/src/entity/player.rs @@ -560,6 +560,11 @@ use pumpkin_protocol::Property; use serde::Deserialize; use std::io::Read; +// Bit masks for the Java skin pixels that Bedrock requires to be opaque. +// Adapted from Geyser's SkinProvider under the MIT License. +const SKIN_OPAQUE_MASK: &str = "AP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAADwD/D/D/8AAPAP8P8P/wAA8A/w/w//AADwD/D/D/8AAP///////w8A////////DwD///////8PAP///////w8A////////DwD///////8PAP///////w8A////////DwD///////8PAP///////w8A////////DwD///////8PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwD/APAAAAAPAP8A8AAAAA8A/wDwAAAADwD/APAAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAA="; +const LEGACY_SKIN_OPAQUE_MASK: &str = "AP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AAAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAAD/////AAAAAP////8AAAAA/////wAAAADwD/D/D/APAPAP8P8P8A8A8A/w/w/wDwDwD/D/D/APAP////////8A/////////wD/////////AP////////8A/////////wD/////////AP////////8A/////////wD/////////AP////////8A/////////wD/////////AA=="; + #[derive(Deserialize)] struct TexturesProperty { textures: Textures, @@ -606,7 +611,7 @@ impl Player { let img = image::load_from_memory(&buf).ok()?; let width = img.width(); - let mut height = img.height(); + let height = img.height(); if width != 64 || (height != 32 && height != 64) { return None; @@ -614,18 +619,26 @@ impl Player { let mut rgba = img.into_rgba8().into_raw(); - if height == 32 { - rgba.resize(64 * 64 * 4, 0); - height = 64; + let opaque_mask = BASE64_STANDARD + .decode(if height == 32 { + LEGACY_SKIN_OPAQUE_MASK + } else { + SKIN_OPAQUE_MASK + }) + .ok()?; + for pixel_index in 0..(width * height) as usize { + if opaque_mask[pixel_index >> 3] & (1 << (pixel_index & 7)) != 0 { + rgba[pixel_index * 4 + 3] = u8::MAX; + } } let mut skin = pumpkin_protocol::bedrock::client::Skin::steve(); - if is_slim { - skin.arm_size = "slim".to_string(); - } + skin.set_slim(is_slim); skin.image_width = width; skin.image_height = height; skin.skin_data = rgba; + skin.skin_id.clone_from(&url); + skin.full_id = url; Some(skin) } @@ -676,13 +689,21 @@ impl Player { abilities.set_for_gamemode(gamemode); let properties = gameprofile.properties.load().clone(); - let bedrock_skin = tokio::task::spawn_blocking(move || { + let mut bedrock_skin = tokio::task::spawn_blocking(move || { Self::fetch_skin(&properties) .unwrap_or_else(pumpkin_protocol::bedrock::client::Skin::steve) }) .await .unwrap_or_else(|_| pumpkin_protocol::bedrock::client::Skin::steve()); + // Standard_Custom is a shared placeholder. Give fallback skins a stable, + // per-player identity so Bedrock never sees duplicate skin IDs. + if bedrock_skin.skin_id == "Standard_Custom" { + let skin_id = format!("pumpkin:{player_uuid}"); + bedrock_skin.skin_id.clone_from(&skin_id); + bedrock_skin.full_id = skin_id; + } + Self { living_entity, config: ArcSwap::new(Arc::new(config)), diff --git a/crates/pumpkin/src/net/bedrock/mod.rs b/crates/pumpkin/src/net/bedrock/mod.rs index 7caa20c31..9bceec227 100644 --- a/crates/pumpkin/src/net/bedrock/mod.rs +++ b/crates/pumpkin/src/net/bedrock/mod.rs @@ -28,7 +28,8 @@ use pumpkin_protocol::{ client_cache_status::SClientCacheStatus, command_request::SCommandRequest, container_close::SContainerClose, emote::SEmote, interaction::SInteraction, inventory_transaction::SInventoryTransaction, loading_screen::SLoadingScreen, - login::SLogin, mob_equipment::SMobEquipment, player_action::SPlayerAction, + login::SLogin, mob_equipment::SMobEquipment, + packet_violation_warning::SPacketViolationWarning, player_action::SPlayerAction, player_auth_input::SPlayerAuthInput, request_ability::SRequestAbility, request_chunk_radius::SRequestChunkRadius, request_network_settings::SRequestNetworkSettings, @@ -717,6 +718,16 @@ impl BedrockClient { self.handle_mob_equipment(server, player, SMobEquipment::read(reader)?) .await; } + SPacketViolationWarning::PACKET_ID => { + let warning = SPacketViolationWarning::read(reader)?; + warn!( + violation_type = warning.violation_type.0, + severity = warning.severity.0, + packet_id = warning.packet_id.0, + context = %warning.context, + "Bedrock client rejected a server packet" + ); + } _ => { warn!("Bedrock: Received Unknown Game packet: {}", packet.id); } diff --git a/crates/pumpkin/src/net/bedrock/nethernet.rs b/crates/pumpkin/src/net/bedrock/nethernet.rs index 277c9106c..9d459eec9 100644 --- a/crates/pumpkin/src/net/bedrock/nethernet.rs +++ b/crates/pumpkin/src/net/bedrock/nethernet.rs @@ -50,7 +50,9 @@ use crate::STOP_INTERRUPT; const RELIABLE_CHANNEL: &str = "ReliableDataChannel"; const UNRELIABLE_CHANNEL: &str = "UnreliableDataChannel"; -const MAX_FRAGMENT_SIZE: usize = 262_143; +// NetherNet splits encoded packets that exceed 10,000 bytes into application-level +// segments. Larger SCTP messages are rejected by some Bedrock clients. +const MAX_FRAGMENT_SIZE: usize = 10_000; const MAX_SDP_SIZE: usize = 1 << 20; type IncomingSession = (Arc, SocketAddr); @@ -727,6 +729,16 @@ mod tests { assert_eq!(fragments.push(0, b"three").unwrap().unwrap(), "onetwothree"); } + #[test] + fn outbound_payloads_are_split_at_the_nethernet_limit() { + let payload = vec![0; MAX_FRAGMENT_SIZE + 1]; + let chunks = payload.chunks(MAX_FRAGMENT_SIZE).collect::>(); + + assert_eq!(chunks.len(), 2); + assert_eq!(chunks[0].len(), 10_000); + assert_eq!(chunks[1].len(), 1); + } + #[test] fn rejects_out_of_order_fragments_and_recovers() { let mut fragments = FragmentBuffer::default(); diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index aa8358cec..3ac89448f 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -90,6 +90,7 @@ use pumpkin_protocol::{ bedrock::{ client::{ add_player::CAddPlayer, + common::BuildPlatform, creative_content::{CCreativeContent, CreativeCategory, Entry, Group}, gamerules_changed::GameRules, player_list::{CPlayerList, PlayerListEntry, Skin}, @@ -2415,7 +2416,7 @@ impl World { username: gameprofile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), - build_platform: 0, + build_platform: BuildPlatform::Unknown, skin: (**player.bedrock_skin.load()).clone(), is_teacher: false, is_host: false, @@ -2487,7 +2488,7 @@ impl World { }, links: Vec::new(), device_id: String::new(), - build_platform: 0, + build_platform: BuildPlatform::Unknown, }; self.broadcast_packet_except_editioned_sync( @@ -2536,7 +2537,7 @@ impl World { username: ex_profile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), - build_platform: 0, + build_platform: BuildPlatform::Unknown, skin: (**existing_player.bedrock_skin.load()).clone(), is_teacher: false, is_host: false, @@ -2581,7 +2582,7 @@ impl World { }, links: Vec::new(), device_id: String::new(), - build_platform: 0, + build_platform: BuildPlatform::Unknown, }; client.send_game_packet(&ex_add_player).await; @@ -2752,7 +2753,7 @@ impl World { username: gameprofile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), - build_platform: 0, + build_platform: BuildPlatform::Unknown, skin: (**player.bedrock_skin.load()).clone(), is_teacher: false, is_host: false, @@ -2827,11 +2828,16 @@ impl World { }]; if base_config.allow_chat_reports { - player_actions.push(PlayerAction::InitializeChat(Some(InitChat { - session_id: chat_session.session_id, - expires_at: chat_session.expires_at, - public_key: chat_session.public_key.clone(), - signature: chat_session.signature.clone(), + let initialized = chat_session.session_id != uuid::Uuid::nil() + && !chat_session.public_key.is_empty() + && !chat_session.signature.is_empty(); + player_actions.push(PlayerAction::InitializeChat(initialized.then(|| { + InitChat { + session_id: chat_session.session_id, + expires_at: chat_session.expires_at, + public_key: chat_session.public_key.clone(), + signature: chat_session.signature.clone(), + } }))); } @@ -2933,7 +2939,7 @@ impl World { }, links: Vec::new(), device_id: String::new(), - build_platform: 0, + build_platform: BuildPlatform::Unknown, }; // Spawn the player for every client. @@ -3022,7 +3028,7 @@ impl World { }, links: Vec::new(), device_id: String::new(), - build_platform: 0, + build_platform: BuildPlatform::Unknown, }; let bedrock_player_list = CPlayerList { @@ -3033,7 +3039,7 @@ impl World { username: gameprofile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), - build_platform: 0, + build_platform: BuildPlatform::Unknown, skin: (**existing_player.bedrock_skin.load()).clone(), is_teacher: false, is_host: false, @@ -4156,7 +4162,7 @@ impl World { username: player.gameprofile.name.clone(), xuid: String::new(), platform_chat_id: String::new(), - build_platform: 0, + build_platform: BuildPlatform::Unknown, skin: Skin::steve(), is_teacher: false, is_host: false,