chore: some more bedrock work

This commit is contained in:
Alexander Medvedev
2026-05-03 19:16:21 +02:00
parent af10507137
commit fb5ede696c
22 changed files with 13798 additions and 90 deletions

12927
assets/en_us_bedrock.lang Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,20 @@
use pumpkin_macros::packet;
use pumpkin_util::math::vector3::Vector3;
use crate::{codec::var_int::VarInt, serial::PacketWrite};
#[derive(PacketWrite)]
#[packet(25)]
pub struct CLevelEvent {
pub event_id: VarInt,
pub position: Vector3<f32>,
pub data: VarInt,
}
#[repr(i32)]
pub enum LevelEvent {
// There are hundreds of these, adding only what we need for now
BlockStartBreak = 3600,
BlockStopBreak = 3601,
BlockUpdateBreak = 3602,
}

View File

@@ -7,6 +7,7 @@ pub mod gamerules_changed;
pub mod handshake; pub mod handshake;
pub mod inventory_content; pub mod inventory_content;
pub mod level_chunk; pub mod level_chunk;
pub mod level_event;
pub mod move_player; pub mod move_player;
pub mod network_chunk_publisher_update; pub mod network_chunk_publisher_update;
pub mod network_settings; pub mod network_settings;

View File

@@ -45,21 +45,29 @@ impl EntityMetadata {
self.0.insert(key, value); self.0.insert(key, value);
} }
pub fn set_flag(&mut self, key: u32, index: u8) { pub fn set_flag(&mut self, key: u32, index: u8, value: bool) {
if key == entity_data_key::PLAYER_FLAGS { if key == entity_data_key::PLAYER_FLAGS {
let current_value = match self.0.get(&key) { let current_value = match self.0.get(&key) {
Some(MetadataValue::Byte(v)) => *v, Some(MetadataValue::Byte(v)) => *v,
_ => 0, _ => 0,
}; };
self.0 let new_value = if value {
.insert(key, MetadataValue::Byte(current_value | (1i8 << index))); current_value | (1i8 << index)
} else {
current_value & !(1i8 << index)
};
self.0.insert(key, MetadataValue::Byte(new_value));
} else { } else {
let current_value = match self.0.get(&key) { let current_value = match self.0.get(&key) {
Some(MetadataValue::Long(v)) => *v, Some(MetadataValue::Long(v)) => *v,
_ => 0, _ => 0,
}; };
self.0 let new_value = if value {
.insert(key, MetadataValue::Long(current_value | (1i64 << index))); current_value | (1i64 << index)
} else {
current_value & !(1i64 << index)
};
self.0.insert(key, MetadataValue::Long(new_value));
} }
} }
} }

View File

@@ -0,0 +1,84 @@
use std::io::{Error, Read, Write};
use pumpkin_macros::packet;
use crate::{
codec::{var_int::VarInt, var_ulong::VarULong},
serial::{PacketRead, PacketWrite},
};
#[derive(Debug)]
#[packet(44)]
pub struct SAnimate {
pub action: AnimateAction,
pub runtime_entity_id: VarULong,
pub boat_rowing_time: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimateAction {
NoAction = 0,
SwingArm = 1,
WakeUp = 2,
CriticalHit = 3,
MagicCriticalHit = 4,
RowRight = 128,
RowLeft = 129,
}
impl PacketRead for AnimateAction {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let action = u8::read(reader)?;
match action {
0 => Ok(Self::NoAction),
1 => Ok(Self::SwingArm),
2 => Ok(Self::WakeUp),
3 => Ok(Self::CriticalHit),
4 => Ok(Self::MagicCriticalHit),
128 => Ok(Self::RowRight),
129 => Ok(Self::RowLeft),
_ => Err(Error::other(format!(
"Invalid animate action ID: {}",
action
))),
}
}
}
impl PacketWrite for AnimateAction {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
(*self as u8).write(writer)
}
}
impl PacketRead for SAnimate {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let action = AnimateAction::read(reader)?;
let runtime_entity_id = VarULong::read(reader)?;
let boat_rowing_time =
if action == AnimateAction::RowRight || action == AnimateAction::RowLeft {
Some(f32::read(reader)?)
} else {
None
};
let _swing_source = bool::read(reader)?;
Ok(Self {
action,
runtime_entity_id,
boat_rowing_time,
})
}
}
impl PacketWrite for SAnimate {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.action.write(writer)?;
self.runtime_entity_id.write(writer)?;
if let Some(rowing_time) = self.boat_rowing_time {
rowing_time.write(writer)?;
}
// swingSource
false.write(writer)?;
Ok(())
}
}

View File

@@ -1,19 +1,19 @@
use pumpkin_macros::packet; use pumpkin_macros::packet;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{codec::var_uint::VarUInt, serial::PacketRead};
codec::{var_int::VarInt, var_uint::VarUInt},
serial::PacketRead,
};
#[derive(Debug, PacketRead)] #[derive(Debug, PacketRead)]
#[packet(77)] #[packet(77)]
pub struct SCommandRequest { pub struct SCommandRequest {
// https://mojang.github.io/bedrock-protocol-docs/html/CommandRequestPacket.html
pub command: String, pub command: String,
pub command_type: VarUInt,
// Command Origin
pub command_type: String,
pub command_uuid: Uuid, pub command_uuid: Uuid,
pub request_id: String, pub request_id: String,
pub player_actor_unique_id: i64,
pub is_internal_source: bool, pub is_internal_source: bool,
pub version: VarInt, pub version: String,
} }

View File

@@ -1,4 +1,5 @@
use pumpkin_macros::packet; use pumpkin_macros::packet;
use serde::Deserialize;
use std::io::{Error, ErrorKind, Read}; use std::io::{Error, ErrorKind, Read};
use crate::{codec::var_uint::VarUInt, serial::PacketRead}; use crate::{codec::var_uint::VarUInt, serial::PacketRead};
@@ -46,3 +47,133 @@ impl PacketRead for SLogin {
}) })
} }
} }
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct SkinAnimation {
pub frames: f64,
pub image: String,
pub image_height: i32,
pub image_width: i32,
#[serde(rename = "Type")]
pub animation_type: i32, // 'type' is a reserved keyword in Rust
pub animation_expression: i32,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct PersonaPiece {
#[serde(rename = "IsDefault")]
pub is_default: bool,
#[serde(rename = "PackId")]
pub pack_id: String,
#[serde(rename = "PieceId")]
pub piece_id: String,
pub piece_type: String,
#[serde(rename = "ProductId")]
pub product_id: String,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct PersonaPieceTintColour {
#[serde(rename = "Colors")]
pub colours: [String; 4],
pub piece_type: String,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "PascalCase")]
pub struct ClientData {
pub client_random_id: i64,
#[serde(rename = "DeviceOS")]
pub device_os: i32,
#[serde(rename = "DeviceId")]
pub device_id: String,
pub game_version: String,
pub language_code: String,
pub current_input_mode: i32,
pub default_input_mode: i32,
#[serde(rename = "UIProfile")]
pub ui_profile: i32,
pub server_address: String,
#[serde(default)]
pub device_model: String,
#[serde(rename = "GuiScale", default)]
pub gui_scale: i32,
#[serde(default)]
pub is_editor_mode: bool,
#[serde(default)]
pub max_view_distance: i32,
#[serde(default)]
pub memory_tier: i32,
#[serde(default)]
pub platform_type: i32,
#[serde(default)]
pub graphics_mode: i32,
#[serde(default)]
pub compatible_with_client_side_chunk_gen: bool,
#[serde(rename = "PlatformOfflineId", default)]
pub platform_offline_id: String,
#[serde(rename = "PlatformOnlineId", default)]
pub platform_online_id: String,
#[serde(rename = "PlatformUserId", default)]
pub platform_user_id: String,
#[serde(rename = "SelfSignedId", default)]
pub self_signed_id: String,
#[serde(rename = "PlayFabId", default)]
pub play_fab_id: String,
#[serde(default)]
pub third_party_name: String,
#[serde(default)]
pub third_party_name_only: bool,
#[serde(rename = "SkinId", default)]
pub skin_id: String,
#[serde(default)]
pub skin_data: String,
#[serde(default)]
pub skin_image_height: i32,
#[serde(default)]
pub skin_image_width: i32,
#[serde(rename = "SkinColor", default)]
pub skin_colour: String,
#[serde(default)]
pub arm_size: String,
#[serde(default)]
pub persona_skin: bool,
#[serde(default)]
pub premium_skin: bool,
#[serde(default)]
pub trusted_skin: bool,
#[serde(default)]
pub override_skin: bool,
#[serde(default)]
pub cape_data: String,
#[serde(rename = "CapeId", default)]
pub cape_id: String,
#[serde(default)]
pub cape_image_height: i32,
#[serde(default)]
pub cape_image_width: i32,
#[serde(default)]
pub cape_on_classic_skin: bool,
#[serde(rename = "SkinGeometryData", default)]
pub skin_geometry: String,
#[serde(rename = "SkinGeometryDataEngineVersion", default)]
pub skin_geometry_version: String,
#[serde(default)]
pub skin_resource_patch: String,
#[serde(default)]
pub animated_image_data: Vec<SkinAnimation>,
#[serde(default)]
pub skin_animation_data: String,
#[serde(default)]
pub persona_pieces: Vec<PersonaPiece>,
#[serde(rename = "PieceTintColors", default)]
pub piece_tint_colours: Vec<PersonaPieceTintColour>,
}

View File

@@ -1,3 +1,4 @@
pub mod animate;
pub mod client_cache_status; pub mod client_cache_status;
pub mod command_request; pub mod command_request;
pub mod container_close; pub mod container_close;

View File

@@ -57,10 +57,11 @@ pub enum Action {
ClientAckServerData = 36, ClientAckServerData = 36,
} }
impl PacketRead for Action { impl TryFrom<i32> for Action {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> { type Error = String;
let action = VarInt::read(reader)?;
match action.0 { fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::StartBreak), 0 => Ok(Self::StartBreak),
1 => Ok(Self::AbortBreak), 1 => Ok(Self::AbortBreak),
2 => Ok(Self::StopBreak), 2 => Ok(Self::StopBreak),
@@ -98,7 +99,15 @@ impl PacketRead for Action {
34 => Ok(Self::StartFlying), 34 => Ok(Self::StartFlying),
35 => Ok(Self::StopFlying), 35 => Ok(Self::StopFlying),
36 => Ok(Self::ClientAckServerData), 36 => Ok(Self::ClientAckServerData),
_ => Err(Error::other(format!("Invalid action ID: {}", action.0))), _ => Err(format!("Invalid action ID: {}", value)),
} }
} }
} }
impl PacketRead for Action {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let action = VarInt::read(reader)?;
Action::try_from(action.0).map_err(Error::other)
}
}

View File

@@ -1,37 +1,166 @@
use std::io::{Error, Read};
use pumpkin_macros::packet; use pumpkin_macros::packet;
use pumpkin_util::math::{vector2::Vector2, vector3::Vector3}; use pumpkin_util::math::{position::BlockPos, vector2::Vector2, vector3::Vector3};
use crate::{ use crate::{
codec::{bitset::Bitset, var_uint::VarUInt, var_ulong::VarULong}, codec::{
bitset::Bitset,
var_int::VarInt,
var_long::{VarLong, VarLongType},
var_uint::VarUInt,
var_ulong::VarULong,
},
packet::{self, Packet},
serial::PacketRead, serial::PacketRead,
}; };
#[derive(Debug, PacketRead)] #[derive(Debug)]
#[packet(144)] #[packet(144)]
pub struct SPlayerAuthInput { pub struct SPlayerAuthInput {
// https://mojang.github.io/bedrock-protocol-docs/html/PlayerAuthInputPacket.html
pub pitch: f32, pub pitch: f32,
pub yaw: f32, pub yaw: f32,
pub position: Vector3<f32>, pub position: Vector3<f32>,
pub move_vec: Vector2<f32>, pub move_vec: Vector2<f32>,
pub head_rotation: f32, pub head_yaw: f32,
pub input_data: Bitset<65>, pub input_data: Bitset<65>,
pub input_mode: VarUInt, pub input_mode: VarUInt,
pub play_mode: VarUInt, pub play_mode: VarUInt,
pub new_interaction_model: VarUInt, pub interaction_model: VarUInt,
pub interact_rotation: Vector2<f32>, pub interact_pitch: f32,
pub client_tick: VarULong, pub interact_yaw: f32,
pub pos_delta: Vector3<f32>, pub tick: VarULong,
pub delta: Vector3<f32>,
pub block_actions: Option<Vec<PlayerBlockAction>>,
pub vehicle_rotation: Option<Vector2<f32>>,
pub vehicle_unique_id: Option<VarLong>,
pub analog_move: Vector2<f32>, pub analog_move: Vector2<f32>,
pub camera_orientation: Vector3<f32>, pub camera_orientation: Vector3<f32>,
pub raw_move_vec: Vector2<f32>, pub raw_move: Vector2<f32>,
} }
#[derive(Clone, Copy)] impl PacketRead for SPlayerAuthInput {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let pitch = f32::read(reader)?;
let yaw = f32::read(reader)?;
let position = Vector3::<f32>::read(reader)?;
let move_vec = Vector2::<f32>::read(reader)?;
let head_yaw = f32::read(reader)?;
let input_data = Bitset::<65>::read(reader)?;
let input_mode = VarUInt::read(reader)?;
let play_mode = VarUInt::read(reader)?;
let interaction_model = VarUInt::read(reader)?;
let interact_pitch = f32::read(reader)?;
let interact_yaw = f32::read(reader)?;
let tick = VarULong::read(reader)?;
let delta = Vector3::<f32>::read(reader)?;
// 1. Perform Item Interaction
if input_data.get(InputData::PerformItemInteraction as usize) {
// protocol.UseItemTransactionData (Simplified skip)
let _action_type = VarUInt::read(reader)?;
let action_count = VarUInt::read(reader)?.0;
for _ in 0..action_count {
let _type = VarUInt::read(reader)?;
let _pos = BlockPos::read(reader)?;
let _face = VarInt::read(reader)?;
}
}
// 2. Item Stack Request
if input_data.get(InputData::PerformItemStackRequest as usize) {
// protocol.Single ItemStackRequest
return Err(Error::other("ItemStackRequest decoding not implemented"));
}
// 3. Block Actions
let block_actions = if input_data.get(InputData::PerformBlockActions as usize) {
let count = VarInt::read(reader)?.0 as usize;
let mut actions = Vec::with_capacity(count);
for _ in 0..count {
actions.push(PlayerBlockAction::read(reader)?);
}
Some(actions)
} else {
None
};
// 4. Vehicle Info (Matches Go logic)
let mut vehicle_rotation = None;
let mut vehicle_unique_id = None;
if input_data.get(InputData::ClientPredictedVehicle as usize) {
vehicle_rotation = Some(Vector2::<f32>::read(reader)?);
vehicle_unique_id = Some(VarLong::read(reader)?);
}
// 5. Trailing Data
let analog_move = Vector2::<f32>::read(reader)?;
let camera_orientation = Vector3::<f32>::read(reader)?;
let raw_move = Vector2::<f32>::read(reader)?;
Ok(Self {
pitch,
yaw,
position,
move_vec,
head_yaw,
input_data,
input_mode,
play_mode,
interaction_model,
interact_pitch,
interact_yaw,
tick,
delta,
block_actions,
vehicle_rotation,
vehicle_unique_id,
analog_move,
camera_orientation,
raw_move,
})
}
}
#[derive(Debug, PacketRead)]
pub struct PlayerBlockAction {
pub action: VarInt,
pub block_pos: BlockPos,
pub face: VarInt,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(u32)]
pub enum InputMode {
Mouse = 1,
Touch = 2,
GamePad = 3,
MotionController = 4,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(u32)]
pub enum PlayMode {
Normal = 0,
Teaser = 1,
Screen = 2,
ExitLevel = 7,
NumModes = 9,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(u32)]
pub enum InteractionModel {
Touch = 0,
Crosshair = 1,
Classic = 2,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum InputData { pub enum InputData {
// https://mojang.github.io/bedrock-protocol-docs/html/enums.html#PlayerAuthInputPacket::InputData
Ascend = 0, Ascend = 0,
Descend = 1, Descend = 1,
NorthJump = 2,
JumpDown = 3, JumpDown = 3,
SprintDown = 4, SprintDown = 4,
ChangeHeight = 5, ChangeHeight = 5,
@@ -74,7 +203,7 @@ pub enum InputData {
StartFlying = 42, StartFlying = 42,
StopFlying = 43, StopFlying = 43,
ClientAckServerData = 44, ClientAckServerData = 44,
IsInClientPredictedVehicle = 45, ClientPredictedVehicle = 45, // Renamed to match Go
PaddlingLeft = 46, PaddlingLeft = 46,
PaddlingRight = 47, PaddlingRight = 47,
BlockBreakingDelayEnabled = 48, BlockBreakingDelayEnabled = 48,
@@ -83,30 +212,15 @@ pub enum InputData {
DownLeft = 51, DownLeft = 51,
DownRight = 52, DownRight = 52,
StartUsingItem = 53, StartUsingItem = 53,
IsCameraRelativeMovementEnabled = 54, CameraRelativeMovementEnabled = 54,
IsRotControlledByMoveDirection = 55, RotControlledByMoveDirection = 55,
StartSpinAttack = 56, StartSpinAttack = 56,
StopSpinAttack = 57, StopSpinAttack = 57,
IsHotbarOnlyTouch = 58, IsHotbarTouchOnly = 58,
JumpReleasedRaw = 59, JumpReleasedRaw = 59,
JumpPressedRaw = 60, JumpPressedRaw = 60,
JumpCurrentRaw = 61, JumpCurrentRaw = 61,
SneakReleasedRaw = 62, SneakReleasedRaw = 62,
SneakPressedRaw = 63, SneakPressedRaw = 63,
SneakCurrentRaw = 64, SneakCurrentRaw = 64,
InputNum = 65,
}
impl From<InputData> for usize {
fn from(value: InputData) -> Self {
value as Self
}
}
pub enum InputMode {
Undefined = 0,
Mouse = 1,
Touch = 2,
GamePad = 3,
MotionController = 4,
} }

View File

@@ -37,6 +37,20 @@ impl SText {
} }
} }
#[must_use]
pub fn translation(message: String, parameters: Vec<String>) -> Self {
Self {
needs_translation: true,
r#type: TextPacketType::Translation,
source_name: String::new(),
message,
parameters,
xuid: String::new(),
platform_chat_id: String::new(),
filtered_message: None,
}
}
#[must_use] #[must_use]
pub fn system_message(message: String) -> Self { pub fn system_message(message: String) -> Self {
Self { Self {

View File

@@ -12,7 +12,7 @@ use serde::{
use crate::{ use crate::{
WritingError, WritingError,
ser::{NetworkReadExt, NetworkWriteExt, ReadingError}, ser::{NetworkReadExt, NetworkWriteExt, ReadingError},
serial::PacketWrite, serial::{PacketRead, PacketWrite},
}; };
pub type VarLongType = i64; pub type VarLongType = i64;
@@ -144,6 +144,34 @@ impl<'de> Deserialize<'de> for VarLong {
} }
} }
impl PacketRead for VarLong {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let mut val: u64 = 0;
let mut shift = 0;
loop {
let byte = u8::read(reader)?;
val |= ((byte & 0x7F) as u64) << shift;
if (byte & 0x80) == 0 {
break;
}
shift += 7;
if shift >= 64 {
return Err(Error::new(
std::io::ErrorKind::InvalidData,
"VarLong is too big (overflow)",
));
}
}
let decoded = ((val >> 1) as i64) ^ -((val & 1) as i64);
Ok(VarLong(decoded))
}
}
impl PacketWrite for VarLong { impl PacketWrite for VarLong {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> { fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
let mut val = ((self.0 << 1) ^ (self.0 >> 63)) as u64; let mut val = ((self.0 << 1) ^ (self.0 >> 63)) as u64;

View File

@@ -138,7 +138,7 @@ impl PacketRead for String {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> { fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
const MAX_STRING_LENGTH: usize = 32767; const MAX_STRING_LENGTH: usize = 32767;
let len = u32::read(reader)? as usize; let len = VarUInt::read(reader)?.0 as usize;
if len > MAX_STRING_LENGTH { if len > MAX_STRING_LENGTH {
return Err(Error::new( return Err(Error::new(

View File

@@ -307,6 +307,28 @@ impl NamedColor {
Self::White => RGBColor::new(255, 255, 255), Self::White => RGBColor::new(255, 255, 255),
} }
} }
#[must_use]
pub const fn to_legacy_char(&self) -> char {
match self {
Self::Black => '0',
Self::DarkBlue => '1',
Self::DarkGreen => '2',
Self::DarkAqua => '3',
Self::DarkRed => '4',
Self::DarkPurple => '5',
Self::Gold => '6',
Self::Gray => '7',
Self::DarkGray => '8',
Self::Blue => '9',
Self::Green => 'a',
Self::Aqua => 'b',
Self::Red => 'c',
Self::LightPurple => 'd',
Self::Yellow => 'e',
Self::White => 'f',
}
}
} }
impl TryFrom<&str> for NamedColor { impl TryFrom<&str> for NamedColor {

View File

@@ -136,6 +136,70 @@ impl TextComponentBase {
text text
} }
#[must_use]
pub fn to_bedrock_legacy(&self, locale: Locale) -> String {
let mut text = String::new();
// 1. Inject Bedrock formatting codes
if let Some(color) = &self.style.color {
match color {
Color::Named(named) => {
text.push_str(&format!("§{}", named.to_legacy_char()));
}
Color::Rgb(_rgb) => {
// Bedrock doesn't strictly support Java's §x hex format.
// Most Bedrock implementations fallback to Gray or ignore it.
}
Color::Reset => {
// Explicitly handle the Reset variant
text.push_str("§r");
}
}
}
if self.style.bold == Some(true) {
text.push_str("§l");
}
if self.style.italic == Some(true) {
text.push_str("§o");
}
if self.style.underlined == Some(true) {
text.push_str("§n");
}
if self.style.obfuscated == Some(true) {
text.push_str("§k");
}
// Note: Bedrock does not support strikethrough natively without resource packs.
// 2. Resolve Content
match &*self.content {
TextContent::Text { text: t } => text.push_str(t),
TextContent::Translate { translate, with } => {
// TODO
text.push_str(&get_translation_text(
translate.to_string(),
locale,
with.clone(),
));
}
TextContent::EntityNames { selector, .. } => text.push_str(selector),
TextContent::Keybind { keybind } => text.push_str(keybind),
TextContent::Custom { key, with, .. } => {
text.push_str(&get_translation_text(key.clone(), locale, with.clone()));
}
}
// 3. Recursively append extra components
for child in &self.extra {
text.push_str(&child.to_bedrock_legacy(locale));
// Bedrock styles bleed into subsequent text. We append a reset code
// to ensure child styles are properly isolated from one another.
text.push_str("§r");
}
text
}
/// Extracts the raw text content of this component for the given locale. /// Extracts the raw text content of this component for the given locale.
/// ///
/// # Arguments /// # Arguments
@@ -145,7 +209,7 @@ impl TextComponentBase {
/// The plain text content of the component. /// The plain text content of the component.
#[must_use] #[must_use]
pub fn get_text(self, locale: Locale) -> String { pub fn get_text(self, locale: Locale) -> String {
match *self.content { let mut text = match *self.content {
TextContent::Text { text } => text.into_owned(), TextContent::Text { text } => text.into_owned(),
TextContent::Translate { translate, with } => { TextContent::Translate { translate, with } => {
get_translation_text(format!("minecraft:{translate}"), locale, with) get_translation_text(format!("minecraft:{translate}"), locale, with)
@@ -156,7 +220,14 @@ impl TextComponentBase {
} => selector.into_owned(), } => selector.into_owned(),
TextContent::Keybind { keybind } => keybind.into_owned(), TextContent::Keybind { keybind } => keybind.into_owned(),
TextContent::Custom { key, with, .. } => get_translation_text(key, locale, with), TextContent::Custom { key, with, .. } => get_translation_text(key, locale, with),
};
// Recursively append the text of all child components
for child in self.extra {
text += &child.get_text(locale);
} }
text
} }
/// Converts this component by resolving all translations. /// Converts this component by resolving all translations.

View File

@@ -34,7 +34,12 @@ use pumpkin_data::{
use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot}; use pumpkin_protocol::java::client::play::{CUpdateEntityPos, CUpdateEntityPosRot};
use pumpkin_protocol::{ use pumpkin_protocol::{
PositionFlag, PositionFlag,
bedrock::client::set_actor_data::{
CSetActorData, EntityMetadata, MetadataValue, PropertySyncData, entity_data_flag,
entity_data_key,
},
codec::var_int::VarInt, codec::var_int::VarInt,
codec::var_ulong::VarULong,
java::client::play::{ java::client::play::{
CEntityPositionSync, CEntityVelocity, CHeadRot, CPlayerPosition, CSetEntityMetadata, CEntityPositionSync, CEntityVelocity, CHeadRot, CPlayerPosition, CSetEntityMetadata,
CSetPassengers, CSpawnEntity, CUpdateEntityRot, Metadata, CSetPassengers, CSpawnEntity, CUpdateEntityRot, Metadata,
@@ -508,6 +513,10 @@ pub struct Entity {
pub data: AtomicI32, pub data: AtomicI32,
/// Stores entity boolean flags (on fire, sneaking, invisible, glowing, etc.) /// Stores entity boolean flags (on fire, sneaking, invisible, glowing, etc.)
pub flags: std::sync::atomic::AtomicI8, pub flags: std::sync::atomic::AtomicI8,
/// Stores Bedrock-specific entity boolean flags (bit 0-63)
pub bedrock_flags: std::sync::atomic::AtomicI64,
/// Stores more Bedrock-specific entity boolean flags (bit 0-63)
pub bedrock_flags_two: std::sync::atomic::AtomicI64,
/// If true, the entity cannot collide with anything (e.g. spectator) /// If true, the entity cannot collide with anything (e.g. spectator)
pub no_clip: AtomicBool, pub no_clip: AtomicBool,
/// Multiplies movement for one tick before being reset /// Multiplies movement for one tick before being reset
@@ -615,6 +624,8 @@ impl Entity {
damage_immunities: Mutex::new(Vec::new()), damage_immunities: Mutex::new(Vec::new()),
data: AtomicI32::new(0), data: AtomicI32::new(0),
flags: std::sync::atomic::AtomicI8::new(0), flags: std::sync::atomic::AtomicI8::new(0),
bedrock_flags: std::sync::atomic::AtomicI64::new(0),
bedrock_flags_two: std::sync::atomic::AtomicI64::new(0),
fire_immune: AtomicBool::new(false), fire_immune: AtomicBool::new(false),
fire_ticks: AtomicI32::new(-1), fire_ticks: AtomicI32::new(-1),
has_visual_fire: AtomicBool::new(false), has_visual_fire: AtomicBool::new(false),
@@ -2202,19 +2213,75 @@ impl Entity {
async fn set_flag(&self, flag: Flag, value: bool) { async fn set_flag(&self, flag: Flag, value: bool) {
let index = flag as u8; let index = flag as u8;
let mask = (1i8).wrapping_shl(index as u32); let mask = (1i8).wrapping_shl(index as u32);
let mut b = self.flags.load(Ordering::Relaxed); let new_je_flags = if value {
if value { self.flags.fetch_or(mask, Ordering::Relaxed) | mask
b |= mask;
} else { } else {
b &= !mask; self.flags.fetch_and(!mask, Ordering::Relaxed) & !mask
} };
self.flags.store(b, Ordering::Relaxed);
self.send_meta_data(&[Metadata::new( self.send_meta_data(&[Metadata::new(
TrackedData::SHARED_FLAGS_ID, TrackedData::SHARED_FLAGS_ID,
MetaDataType::BYTE, MetaDataType::BYTE,
b, new_je_flags,
)]) )])
.await; .await;
if let Some(bedrock_flag) = flag.to_bedrock() {
let (key, index) = if bedrock_flag >= 64 {
(entity_data_key::FLAGS_TWO, (bedrock_flag - 64) as u8)
} else {
(entity_data_key::FLAGS, bedrock_flag as u8)
};
if value {
let mask = 1i64 << index;
if key == entity_data_key::FLAGS {
self.bedrock_flags.fetch_or(mask, Ordering::Relaxed);
} else {
self.bedrock_flags_two.fetch_or(mask, Ordering::Relaxed);
}
} else {
let mask = !(1i64 << index);
if key == entity_data_key::FLAGS {
self.bedrock_flags.fetch_and(mask, Ordering::Relaxed);
} else {
self.bedrock_flags_two.fetch_and(mask, Ordering::Relaxed);
}
};
let world = self.world.load();
let chunk_pos = self.chunk_pos.load();
for player in world.players.load().iter() {
if let ClientPlatform::Bedrock(client) = &player.client {
let center = player.living_entity.entity.chunk_pos.load();
let view_distance =
crate::world::chunker::get_view_distance(player).get() as i32;
if is_within_view_distance(chunk_pos, center, view_distance) {
let mut metadata = EntityMetadata(std::collections::HashMap::new());
metadata.set(
entity_data_key::FLAGS,
MetadataValue::Long(self.bedrock_flags.load(Ordering::Relaxed)),
);
metadata.set(
entity_data_key::FLAGS_TWO,
MetadataValue::Long(self.bedrock_flags_two.load(Ordering::Relaxed)),
);
client
.send_game_packet(&CSetActorData {
actor_runtime_id: VarULong(self.entity_id as u64),
metadata,
synced_properties: PropertySyncData {
int_properties: std::collections::HashMap::new(),
float_properties: std::collections::HashMap::new(),
},
tick: VarULong(0),
})
.await;
}
}
}
}
} }
/// Plays sound at this entity's position with the entity's sound category /// Plays sound at this entity's position with the entity's sound category
@@ -2915,6 +2982,20 @@ pub enum Flag {
FallFlying = 7, FallFlying = 7,
} }
impl Flag {
pub const fn to_bedrock(&self) -> Option<u32> {
match self {
Self::OnFire => Some(entity_data_flag::ON_FIRE),
Self::Sneaking => Some(entity_data_flag::SNEAKING),
Self::Sprinting => Some(entity_data_flag::SPRINTING),
Self::Swimming => Some(entity_data_flag::SWIMMING),
Self::Invisible => Some(entity_data_flag::INVISIBLE),
Self::FallFlying => Some(entity_data_flag::GLIDING),
Self::Glowing => None,
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -3,6 +3,7 @@ use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
use std::f64::consts::TAU; use std::f64::consts::TAU;
use std::mem; use std::mem;
use std::num::NonZeroU8; use std::num::NonZeroU8;
use std::str::FromStr;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU8, AtomicU32, Ordering}; use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicU8, AtomicU32, Ordering};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -23,6 +24,7 @@ use pumpkin_protocol::bedrock::client::update_abilities::{
use pumpkin_protocol::bedrock::frame_set::FrameSet; use pumpkin_protocol::bedrock::frame_set::FrameSet;
use pumpkin_protocol::bedrock::server::text::SText; use pumpkin_protocol::bedrock::server::text::SText;
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_util::translation::Locale;
use pumpkin_world::chunk::{ChunkData, ChunkEntityData}; use pumpkin_world::chunk::{ChunkData, ChunkEntityData};
use pumpkin_world::inventory::Inventory; use pumpkin_world::inventory::Inventory;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -2704,7 +2706,9 @@ impl Player {
} }
ClientPlatform::Bedrock(client) => { ClientPlatform::Bedrock(client) => {
client client
.send_game_packet(&SText::system_message(text.clone().get_text())) .send_game_packet(&SText::system_message(text.clone().0.to_bedrock_legacy(
Locale::from_str(&self.config.load().locale).unwrap_or(Locale::EnUs),
)))
.await; .await;
} }
} }

View File

@@ -1,9 +1,11 @@
use crate::{ use crate::{
net::{ClientPlatform, DisconnectReason, GameProfile, bedrock::BedrockClient}, net::{ClientPlatform, DisconnectReason, GameProfile, PlayerConfig, bedrock::BedrockClient},
server::Server, server::Server,
}; };
use pumpkin_config::networking::compression::CompressionInfo; use pumpkin_config::networking::compression::CompressionInfo;
use pumpkin_protocol::bedrock::server::resource_pack_response::SResourcePackResponse; use pumpkin_protocol::bedrock::server::{
login::ClientData, resource_pack_response::SResourcePackResponse,
};
use pumpkin_protocol::{ use pumpkin_protocol::{
bedrock::{ bedrock::{
client::{ client::{
@@ -18,7 +20,7 @@ use pumpkin_protocol::{
}; };
use pumpkin_util::jwt::AuthError; use pumpkin_util::jwt::AuthError;
use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION}; use pumpkin_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
use serde::Deserialize; use serde::{Deserialize, de::Error};
use serde_repr::Deserialize_repr; use serde_repr::Deserialize_repr;
use std::sync::Arc; use std::sync::Arc;
use thiserror::Error; use thiserror::Error;
@@ -39,6 +41,8 @@ pub enum LoginError {
SelfSignedNotAllowed, SelfSignedNotAllowed,
#[error("Got a guest/splitscreen login request. Currently unimplemented.")] #[error("Got a guest/splitscreen login request. Currently unimplemented.")]
GuestUnimplemented, GuestUnimplemented,
#[error("Failed to decode extra using decode_b64_url_nopad.")]
DecodeExtraError,
} }
#[derive(Deserialize_repr)] #[derive(Deserialize_repr)]
@@ -135,9 +139,27 @@ impl BedrockClient {
pumpkin_util::jwt::extract_oidc_token_player_claims(&auth_payload.token)? pumpkin_util::jwt::extract_oidc_token_player_claims(&auth_payload.token)?
}; };
let raw_token_str = std::str::from_utf8(&packet.raw_token).map_err(|_| {
LoginError::InvalidTokenFormat(serde_json::Error::custom(
"raw_token is not valid UTF-8",
))
})?; // You'll need to add a string conversion error to LoginError, or handle it cleanly.
let mut parts = raw_token_str.split('.');
let _header = parts.next().ok_or(AuthError::InvalidTokenFormat)?;
let payload_b64 = parts.next().ok_or(AuthError::InvalidTokenFormat)?;
let payload_bytes = pumpkin_util::jwt::decode_b64_url_nopad(payload_b64)
.map_err(|_| LoginError::DecodeExtraError)?;
let client_data: ClientData = serde_json::from_slice(&payload_bytes)?;
let real_name = player_data.display_name;
// IMPORTANT: Bedrock allows spaces in names. While we could support this, it would significantly complicate parsing player arguments in commands, so we don't
let under_score_name = real_name.replace(" ", "_");
let profile = GameProfile { let profile = GameProfile {
id: Uuid::parse_str(&player_data.uuid).map_err(|_| LoginError::InvalidUuid)?, id: Uuid::parse_str(&player_data.uuid).map_err(|_| LoginError::InvalidUuid)?,
name: player_data.display_name, name: under_score_name,
properties: Vec::new(), properties: Vec::new(),
profile_actions: None, profile_actions: None,
}; };
@@ -158,6 +180,14 @@ impl BedrockClient {
.add_player(ClientPlatform::Bedrock(self.clone()), profile, None) .add_player(ClientPlatform::Bedrock(self.clone()), profile, None)
.await .await
{ {
// TODO: kinda sad we don't use more of client_data, we should store it somewhere, at least for plugin devs
let new_config = PlayerConfig {
locale: client_data.language_code,
..Default::default()
};
player.config.store(std::sync::Arc::new(new_config));
// player spawn happens after resource packs are resolved // player spawn happens after resource packs are resolved
*self.player.lock().await = Some(player); *self.player.lock().await = Some(player);
} }

View File

@@ -22,10 +22,12 @@ use pumpkin_protocol::{
packet_decoder::UDPNetworkDecoder, packet_decoder::UDPNetworkDecoder,
packet_encoder::UDPNetworkEncoder, packet_encoder::UDPNetworkEncoder,
server::{ server::{
animate::SAnimate,
client_cache_status::SClientCacheStatus, client_cache_status::SClientCacheStatus,
command_request::SCommandRequest, command_request::SCommandRequest,
container_close::SContainerClose, container_close::SContainerClose,
interaction::SInteraction, interaction::SInteraction,
loading_screen::SLoadingScreen,
login::SLogin, login::SLogin,
player_action::SPlayerAction, player_action::SPlayerAction,
player_auth_input::SPlayerAuthInput, player_auth_input::SPlayerAuthInput,
@@ -685,7 +687,7 @@ impl BedrockClient {
let reader = &mut &packet.payload[..]; let reader = &mut &packet.payload[..];
match packet.id { match packet.id {
SPlayerAuthInput::PACKET_ID => { SPlayerAuthInput::PACKET_ID => {
self.player_pos_update(player, SPlayerAuthInput::read(reader)?, server) self.handle_player_auth_input(player, SPlayerAuthInput::read(reader)?, server)
.await; .await;
} }
SRequestChunkRadius::PACKET_ID => { SRequestChunkRadius::PACKET_ID => {
@@ -718,6 +720,13 @@ impl BedrockClient {
self.handle_player_action(player, server, SPlayerAction::read(reader)?) self.handle_player_action(player, server, SPlayerAction::read(reader)?)
.await; .await;
} }
SAnimate::PACKET_ID => {
self.handle_animate(player, server, SAnimate::read(reader)?)
.await;
}
SLoadingScreen::PACKET_ID => {
// Ignore for now
}
_ => { _ => {
warn!("Bedrock: Received Unknown Game packet: {}", packet.id); warn!("Bedrock: Received Unknown Game packet: {}", packet.id);
} }

View File

@@ -1,13 +1,18 @@
use std::{ use std::{
num::{NonZero, NonZeroI32}, num::{NonZero, NonZeroI32},
sync::Arc, sync::{Arc, atomic::Ordering},
}; };
use pumpkin_macros::send_cancellable; use pumpkin_macros::send_cancellable;
use pumpkin_protocol::{ use pumpkin_protocol::{
bedrock::{ bedrock::{
client::{chunk_radius_update::CChunkRadiusUpdate, container_open::CContainerOpen}, client::{
chunk_radius_update::CChunkRadiusUpdate,
container_open::CContainerOpen,
level_event::{CLevelEvent, LevelEvent},
},
server::{ server::{
animate::{AnimateAction, SAnimate},
command_request::SCommandRequest, command_request::SCommandRequest,
container_close::SContainerClose, container_close::SContainerClose,
interaction::{Action, SInteraction}, interaction::{Action, SInteraction},
@@ -18,10 +23,14 @@ use pumpkin_protocol::{
text::SText, text::SText,
}, },
}, },
codec::{var_int::VarInt, var_long::VarLong}, codec::{var_int::VarInt, var_long::VarLong, var_ulong::VarULong},
java::client::play::CSystemChatMessage, java::client::play::{Animation, CEntityAnimation, CSetBlockDestroyStage, CSystemChatMessage},
};
use pumpkin_util::{
GameMode,
math::{position::BlockPos, vector3::Vector3},
text::TextComponent,
}; };
use pumpkin_util::{GameMode, math::position::BlockPos, text::TextComponent};
use pumpkin_world::world::BlockFlags; use pumpkin_world::world::BlockFlags;
@@ -94,7 +103,7 @@ impl BedrockClient {
player.set_client_loaded(true); player.set_client_loaded(true);
} }
pub async fn player_pos_update( pub async fn handle_player_auth_input(
&self, &self,
player: &Arc<Player>, player: &Arc<Player>,
packet: SPlayerAuthInput, packet: SPlayerAuthInput,
@@ -114,13 +123,19 @@ impl BedrockClient {
let input_data = packet.input_data; let input_data = packet.input_data;
let entity = player.get_entity(); let entity = player.get_entity();
if input_data.get(InputData::StartSprinting) { if input_data.get(InputData::StartSprinting as usize) {
entity.set_sprinting(true).await; entity.set_sprinting(true).await;
} else if input_data.get(InputData::StopSprinting) { } else if input_data.get(InputData::StopSprinting as usize) {
entity.set_sprinting(false).await; entity.set_sprinting(false).await;
} }
if input_data.get(InputData::StartFlying) { if input_data.get(InputData::StartSneaking as usize) {
entity.set_sneaking(true).await;
} else if input_data.get(InputData::StopSneaking as usize) {
entity.set_sneaking(false).await;
}
if input_data.get(InputData::StartFlying as usize) {
let mut abilities = player.abilities.lock().await; let mut abilities = player.abilities.lock().await;
if !abilities.flying { if !abilities.flying {
send_cancellable! {{ send_cancellable! {{
@@ -135,7 +150,7 @@ impl BedrockClient {
} }
}} }}
} }
} else if input_data.get(InputData::StopFlying) { } else if input_data.get(InputData::StopFlying as usize) {
let mut abilities = player.abilities.lock().await; let mut abilities = player.abilities.lock().await;
if abilities.flying { if abilities.flying {
send_cancellable! {{ send_cancellable! {{
@@ -152,13 +167,64 @@ impl BedrockClient {
} }
} }
if input_data.get(InputData::StartSneaking) { if let Some(block_actions) = packet.block_actions {
entity.set_sneaking(true).await; for action in block_actions {
} else if input_data.get(InputData::StopSneaking) { self.handle_player_block_action(player, server, action)
entity.set_sneaking(false).await; .await;
}
} }
} }
pub async fn handle_player_block_action(
&self,
player: &Arc<Player>,
server: &Server,
packet: pumpkin_protocol::bedrock::server::player_auth_input::PlayerBlockAction,
) {
use pumpkin_protocol::bedrock::server::player_action::Action as PlayerAction;
let action = PlayerAction::try_from(packet.action.0).unwrap();
self.handle_player_action(
player,
server,
SPlayerAction {
runtime_id: VarInt(0), // Unused
action,
block_pos: packet.block_pos,
result_pos: BlockPos::ZERO,
face: packet.face,
},
)
.await;
}
pub async fn handle_animate(&self, player: &Arc<Player>, _server: &Server, packet: SAnimate) {
if !player.has_client_loaded() {
return;
}
let entity = &player.living_entity.entity;
let world = entity.world.load();
// Broadcast the animation to other players
let java_animation = match packet.action {
AnimateAction::SwingArm => Some(Animation::SwingMainArm),
AnimateAction::WakeUp => Some(Animation::LeaveBed),
AnimateAction::CriticalHit => Some(Animation::CriticalEffect),
AnimateAction::MagicCriticalHit => Some(Animation::MagicCriticaleffect),
_ => None,
};
// if let Some(animation) = java_animation {
// let je_packet = CEntityAnimation::new(VarInt(entity.entity_id), animation);
// let be_packet = SAnimate {
// action: packet.action,
// runtime_entity_id: VarULong(entity.entity_id as u64),
// boat_rowing_time: packet.boat_rowing_time,
// };
// world.broadcast_editioned(&je_packet, &be_packet).await;
// }
}
pub async fn handle_interaction(&self, _player: &Arc<Player>, packet: SInteraction) { pub async fn handle_interaction(&self, _player: &Arc<Player>, packet: SInteraction) {
if matches!(packet.action, Action::OpenInventory) { if matches!(packet.action, Action::OpenInventory) {
self.send_game_packet(&CContainerOpen { self.send_game_packet(&CContainerOpen {
@@ -234,6 +300,7 @@ impl BedrockClient {
if !player.has_client_loaded() { if !player.has_client_loaded() {
return; return;
} }
player.update_last_action_time();
match packet.action { match packet.action {
PlayerAction::StartBreak | PlayerAction::CreativePlayerDestroyBlock => { PlayerAction::StartBreak | PlayerAction::CreativePlayerDestroyBlock => {
@@ -261,6 +328,9 @@ impl BedrockClient {
.await; .await;
} }
} else if !state.is_air() { } else if !state.is_air() {
// Broadcast that breaking started
world.set_block_breaking(entity, location, 0).await;
let speed = crate::block::calc_block_breaking(player, state, block).await; let speed = crate::block::calc_block_breaking(player, state, block).await;
if speed >= 1.0 { if speed >= 1.0 {
let broken_state = world.get_block_state(&location).await; let broken_state = world.get_block_state(&location).await;
@@ -279,10 +349,28 @@ impl BedrockClient {
player.apply_tool_damage_for_block_break(broken_state).await; player.apply_tool_damage_for_block_break(broken_state).await;
} }
} else { } else {
// TODO: Survival progressive breaking player.mining.store(true, Ordering::Relaxed);
*player.mining_pos.lock().await = location;
let progress = (speed * 10.0) as i32;
world.set_block_breaking(entity, location, progress).await;
player
.current_block_destroy_stage
.store(progress, Ordering::Relaxed);
} }
} }
} }
PlayerAction::CrackBreak => {
// Don't do anything for this action. It is no longer used. Block
// cracking is done fully server-side.
}
PlayerAction::AbortBreak | PlayerAction::StopBreak => {
let location = packet.block_pos;
let entity = &player.living_entity.entity;
let world = entity.world.load();
player.mining.store(false, Ordering::Relaxed);
world.set_block_breaking(entity, location, -1).await;
}
// TODO // TODO
_ => {} _ => {}
} }
@@ -292,16 +380,17 @@ impl BedrockClient {
&self, &self,
player: &Arc<Player>, player: &Arc<Player>,
server: &Arc<Server>, server: &Arc<Server>,
command: SCommandRequest, packet: SCommandRequest,
) { ) {
let player_clone = player.clone(); let player_clone = player.clone();
let server_clone = server.clone(); let server_clone = server.clone();
let command = packet.command.strip_prefix("/").unwrap_or(&packet.command);
send_cancellable! {{ send_cancellable! {{
server; server;
PlayerCommandSendEvent { PlayerCommandSendEvent {
player: player.clone(), player: player.clone(),
command: command.command.clone(), command: command.to_string(),
cancelled: false cancelled: false
}; };

View File

@@ -1,3 +1,4 @@
use pumpkin_protocol::bedrock::client::level_event::{CLevelEvent, LevelEvent};
use pumpkin_protocol::codec::data_component::data_to_proto_sound; use pumpkin_protocol::codec::data_component::data_to_proto_sound;
use std::pin::Pin; use std::pin::Pin;
use std::sync::atomic::Ordering::Relaxed; use std::sync::atomic::Ordering::Relaxed;
@@ -1708,16 +1709,24 @@ impl World {
metadata.set(entity_data_key::HEIGHT, MetadataValue::Float(1.8)); metadata.set(entity_data_key::HEIGHT, MetadataValue::Float(1.8));
// This is super important, otherwise the client will float by default // This is super important, otherwise the client will float by default
metadata.set_flag(entity_data_key::FLAGS, entity_data_flag::HAS_GRAVITY as u8); let entity = &player.living_entity.entity;
metadata.set_flag(entity_data_key::FLAGS, entity_data_flag::CLIMB as u8); entity.bedrock_flags.fetch_or(
// Player-specific: survival has collision (1i64 << entity_data_flag::HAS_GRAVITY)
metadata.set_flag( | (1i64 << entity_data_flag::CLIMB)
| (1i64 << entity_data_flag::HAS_COLLISION)
| (1i64 << entity_data_flag::BREATHING),
Ordering::Relaxed,
);
metadata.set(
entity_data_key::FLAGS, entity_data_key::FLAGS,
entity_data_flag::HAS_COLLISION as u8, MetadataValue::Long(entity.bedrock_flags.load(Ordering::Relaxed)),
);
metadata.set(
entity_data_key::FLAGS_TWO,
MetadataValue::Long(entity.bedrock_flags_two.load(Ordering::Relaxed)),
); );
// Prevents the client from showing air buddles on hud even when not in water
metadata.set_flag(entity_data_key::FLAGS, entity_data_flag::BREATHING as u8);
let actor_data = CSetActorData { let actor_data = CSetActorData {
actor_runtime_id: VarULong(runtime_id), actor_runtime_id: VarULong(runtime_id),
metadata, metadata,
@@ -3215,10 +3224,29 @@ impl World {
pub async fn set_block_breaking(&self, from: &Entity, location: BlockPos, progress: i32) { pub async fn set_block_breaking(&self, from: &Entity, location: BlockPos, progress: i32) {
let chunk_pos = location.chunk_position(); // pumpkin's BlockPos already has this method let chunk_pos = location.chunk_position(); // pumpkin's BlockPos already has this method
self.broadcast_to_chunk_except( let je_packet = CSetBlockDestroyStage::new(from.entity_id.into(), location, progress as i8);
let (event_id, data) = match progress {
-1 => (LevelEvent::BlockStopBreak, 0),
0 => (LevelEvent::BlockStartBreak, 0),
_ => (LevelEvent::BlockUpdateBreak, progress),
};
let be_packet = CLevelEvent {
event_id: VarInt(event_id as i32),
position: Vector3::new(
location.0.x as f32,
location.0.y as f32,
location.0.z as f32,
),
data: VarInt(data),
};
self.broadcast_to_chunk_except_editioned(
chunk_pos, chunk_pos,
&[from.entity_uuid], &[from.entity_uuid],
&CSetBlockDestroyStage::new(from.entity_id.into(), location, progress as i8), &je_packet,
&be_packet,
) )
.await; .await;
} }
@@ -4162,6 +4190,43 @@ impl World {
let recipients_by_version = Self::collect_java_recipients_by_version(recipients); let recipients_by_version = Self::collect_java_recipients_by_version(recipients);
Self::broadcast_java_grouped(packet, recipients_by_version).await; Self::broadcast_java_grouped(packet, recipients_by_version).await;
} }
pub async fn broadcast_to_chunk_except_editioned<J: ClientPacket, B: BClientPacket>(
&self,
chunk_pos: Vector2<i32>,
except: &[uuid::Uuid],
je_packet: &J,
be_packet: &B,
) {
let players = self.players.load();
let recipients = players.iter().filter(|p| {
if except.contains(&p.living_entity.entity.entity_uuid) {
return false;
}
let center = p.living_entity.entity.chunk_pos.load();
let view_distance = get_view_distance(p).get() as i32;
is_within_view_distance(chunk_pos, center, view_distance)
});
let mut java_recipients = Vec::new();
let mut bedrock_recipients = Vec::new();
for p in recipients {
match &p.client {
ClientPlatform::Java(_) => java_recipients.push(p),
ClientPlatform::Bedrock(be_client) => bedrock_recipients.push(be_client.clone()),
}
}
let je_recipients_by_version =
Self::collect_java_recipients_by_version(java_recipients.into_iter());
Self::broadcast_java_grouped(je_packet, je_recipients_by_version).await;
for recipient in bedrock_recipients {
recipient.send_game_packet(be_packet).await;
}
}
} }
impl pumpkin_world::world::SimpleWorld for World { impl pumpkin_world::world::SimpleWorld for World {