mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
fix: bedrock and java players disconnecting when joining the same server (#2844)
* WIP: improve Bedrock player skin compatibility * Fix cross-edition player list synchronization * Fix legacy Java skins on Bedrock * Refactor Bedrock build platform encoding
This commit is contained in:
21
assets/bedrock/LICENSE-GEYSER
Normal file
21
assets/bedrock/LICENSE-GEYSER
Normal file
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
1
assets/bedrock/player_geometry.json
Normal file
1
assets/bedrock/player_geometry.json
Normal file
@@ -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}}]}
|
||||
@@ -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<EntityLink>,
|
||||
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<EntityLink>,
|
||||
device_id: String,
|
||||
build_platform: i32,
|
||||
build_platform: BuildPlatform,
|
||||
) -> Self {
|
||||
Self {
|
||||
uuid,
|
||||
|
||||
@@ -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<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
|
||||
(*self as i32).write(writer)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, PacketWrite)]
|
||||
pub struct AbilityLayer {
|
||||
pub serialized_layer: u16,
|
||||
|
||||
@@ -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""#)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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<R: Read>(reader: &mut R) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
violation_type: VarInt::read(reader)?,
|
||||
severity: VarInt::read(reader)?,
|
||||
packet_id: VarInt::read(reader)?,
|
||||
context: String::read(reader)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<NetherNetSession>, 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::<Vec<_>>();
|
||||
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user