fix: bedrock edition and port it to 1.21.30

This commit is contained in:
Alexander Medvedev 2026-01-23 15:17:56 +01:00
parent 9c70f09360
commit d81acb0ab1
12 changed files with 235 additions and 193 deletions

View File

@ -29,7 +29,7 @@ impl PacketWrite for CreativeContent<'_> {
}
#[repr(i32)]
#[expect(unused)]
#[allow(unused)]
enum CreativeCategory {
Construction = 1,
Nature = 2,

View File

@ -4,14 +4,14 @@ use pumpkin_macros::packet;
use pumpkin_util::math::{position::BlockPos, vector2::Vector2};
use crate::{
codec::{bedrock_block_pos::NetworkPos, var_int::VarInt, var_uint::VarUInt},
codec::{var_int::VarInt, var_uint::VarUInt},
serial::PacketWrite,
};
#[packet(121)]
pub struct CNetworkChunkPublisherUpdate {
// https://mojang.github.io/bedrock-protocol-docs/html/NetworkChunkPublisherUpdatePacket.html
pub pos_for_view: NetworkPos,
pub pos_for_view: BlockPos,
// Is in blocks, not chunks!
pub new_radius: VarUInt,
// TODO
@ -21,8 +21,8 @@ pub struct CNetworkChunkPublisherUpdate {
impl CNetworkChunkPublisherUpdate {
pub fn new(pos_for_view: BlockPos, new_radius: u32) -> Self {
Self {
pos_for_view: NetworkPos(pos_for_view),
new_radius: VarUInt(new_radius),
pos_for_view,
new_radius: VarUInt(new_radius << 4),
server_build_chunk_list: Vec::new(),
}
}
@ -30,7 +30,7 @@ impl CNetworkChunkPublisherUpdate {
impl PacketWrite for CNetworkChunkPublisherUpdate {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.pos_for_view.write_signed(writer)?;
self.pos_for_view.write(writer)?;
self.new_radius.write(writer)?;
(self.server_build_chunk_list.len() as u32).write(writer)?;

View File

@ -6,10 +6,10 @@ use crate::{
#[derive(PacketWrite)]
#[packet(7)]
/// https://mojang.github.io/bedrock-protocol-docs/html/ResourcePackStackPacket.html
pub struct CResourcePackStackPacket {
// https://mojang.github.io/bedrock-protocol-docs/html/ResourcePackStackPacket.html
resource_pack_required: bool,
addons_list_size: VarUInt,
texture_pack_list_size: VarUInt,
game_version: String,
experiments: Experiments,
/// When connecting to an Editor world, include the vanilla editor packs in the stack
@ -19,14 +19,14 @@ pub struct CResourcePackStackPacket {
impl CResourcePackStackPacket {
pub fn new(
resource_pack_required: bool,
addons_list_size: VarUInt,
texture_pack_list_size: VarUInt,
game_version: String,
experiments: Experiments,
include_editor_packs: bool,
) -> Self {
Self {
resource_pack_required,
addons_list_size,
texture_pack_list_size,
game_version,
experiments,
include_editor_packs,

View File

@ -1,10 +1,9 @@
use std::io::{Error, Write};
use crate::serial::PacketWrite;
use pumpkin_macros::packet;
use crate::serial::PacketWrite;
#[derive(PacketWrite)]
#[packet(6)]
/// https://mojang.github.io/bedrock-protocol-docs/html/ResourcePacksInfoPacket.html
pub struct CResourcePacksInfo {
resource_pack_required: bool,
has_addon_packs: bool,
@ -12,36 +11,7 @@ pub struct CResourcePacksInfo {
is_vibrant_visuals_force_disabled: bool,
world_template_id: uuid::Uuid,
world_template_version: String,
resource_packs_size: u16,
resource_packs: Vec<ResourcePack>,
}
impl PacketWrite for CResourcePacksInfo {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.resource_pack_required.write(writer)?;
self.has_addon_packs.write(writer)?;
self.has_scripts.write(writer)?;
self.is_vibrant_visuals_force_disabled.write(writer)?;
self.world_template_id.write(writer)?;
self.world_template_version.write(writer)?;
self.resource_packs_size.write(writer)?;
self.resource_packs.write(writer)?;
Ok(())
}
}
#[derive(PacketWrite)]
pub struct ResourcePack {
pack_id: uuid::Uuid,
version: String,
size: u64,
content_key: String,
subpack_name: String,
content_identity: String,
has_scripts: bool,
is_addon_pack: bool,
is_raytracing_capable: bool,
cdn_url: String,
resource_packs_size: u16, // TODO: Add more
}
impl CResourcePacksInfo {
@ -52,7 +22,6 @@ impl CResourcePacksInfo {
is_vibrant_visuals_force_disabled: bool,
world_template_id: uuid::Uuid,
world_template_version: String,
resource_packs: Vec<ResourcePack>,
) -> Self {
Self {
resource_pack_required,
@ -61,8 +30,8 @@ impl CResourcePacksInfo {
is_vibrant_visuals_force_disabled,
world_template_id,
world_template_version,
resource_packs_size: resource_packs.len() as u16,
resource_packs,
// TODO
resource_packs_size: 0,
}
}
}

View File

@ -34,7 +34,6 @@ pub struct CStartGame {
pub current_level_time: u64,
pub enchantment_seed: VarInt,
pub block_properties_size: VarUInt,
//pub block_properties: [GG; 2],
pub multiplayer_correlation_id: String,
@ -51,8 +50,7 @@ pub struct CStartGame {
pub enable_clientside_generation: bool,
pub blocknetwork_ids_are_hashed: bool,
// NetworkPermissions
pub disable_client_sounds: bool,
pub server_auth_sounds: bool,
}
#[derive(PacketWrite)]
@ -115,7 +113,6 @@ pub struct LevelSettings {
pub new_nether: bool,
pub edu_shared_uri_button_name: String,
pub edu_shared_uri_link_uri: String,
pub override_force_experimental_gameplay_has_value: bool,
pub chat_restriction_level: i8,
pub disable_player_interactions: bool,

View File

@ -134,7 +134,7 @@ impl UDPNetworkDecoder {
err => PacketDecodeError::MalformedLength(err.to_string()),
})?;
let packet_len = packet_len.0 as usize;
let packet_len = packet_len.0 as u64;
let var_header = VarUInt::decode(&mut reader)?;
@ -157,7 +157,7 @@ impl UDPNetworkDecoder {
let gamepacket_id = (header & 0x3FF) as u16; // 0x3FF is 10 bits set to 1
let payload = reader
.read_boxed_slice(packet_len - var_header.written_size())
.read_boxed_slice(packet_len as usize - var_header.written_size())
.map_err(|err| PacketDecodeError::FailedDecompression(err.to_string()))?;
Ok(RawPacket {

View File

@ -1,97 +1,157 @@
use std::io::{Error, Read, Write};
use pumpkin_macros::packet;
use crate::{
codec::var_uint::VarUInt,
serial::{PacketRead, PacketWrite},
};
use pumpkin_macros::packet;
use std::io::{Error, ErrorKind, Read, Write};
#[derive(Debug)]
#[packet(9)]
pub struct SText {
// https://mojang.github.io/bedrock-protocol-docs/html/TextPacket.html
pub needs_translation: bool,
pub r#type: TextPacketType,
pub localize: bool,
pub player_name: String,
pub source_name: String,
pub message: String,
pub parameters: Vec<String>,
pub sender_xuid: String,
pub platform_id: String,
pub filtered_message: String,
pub xuid: String,
pub platform_chat_id: String,
pub filtered_message: Option<String>,
}
impl SText {
pub fn new(message: String, player_name: String) -> Self {
pub fn new(message: String, source_name: String) -> Self {
Self {
needs_translation: false,
r#type: TextPacketType::Chat,
localize: false,
player_name,
message,
source_name,
message: if message.is_empty() {
" ".to_string()
} else {
message
},
parameters: Vec::new(),
sender_xuid: String::new(),
platform_id: String::new(),
filtered_message: String::new(),
xuid: String::new(),
platform_chat_id: String::new(),
filtered_message: None,
}
}
pub fn system_message(message: String) -> Self {
Self {
r#type: TextPacketType::SystemMessage,
localize: false,
player_name: String::new(),
message,
needs_translation: false,
r#type: TextPacketType::System,
source_name: String::new(),
message: if message.is_empty() {
" ".to_string()
} else {
message
},
parameters: Vec::new(),
sender_xuid: String::new(),
platform_id: String::new(),
filtered_message: String::new(),
xuid: String::new(),
platform_chat_id: String::new(),
filtered_message: None,
}
}
fn get_dummy_strings(category: u8) -> &'static [&'static str] {
match category {
0 => &[
"raw",
"tip",
"systemMessage",
"textObjectWhisper",
"textObjectAnnouncement",
"textObject",
],
1 => &["chat", "whisper", "announcement"],
2 => &["translate", "popup", "jukeboxPopup"],
_ => &[],
}
}
fn get_category(&self) -> u8 {
match self.r#type {
TextPacketType::Raw
| TextPacketType::Tip
| TextPacketType::System
| TextPacketType::JsonWhisper
| TextPacketType::JsonAnnouncement
| TextPacketType::Json => 0, // CATEGORY_MESSAGE_ONLY
TextPacketType::Chat | TextPacketType::Whisper | TextPacketType::Announcement => 1, // CATEGORY_AUTHORED_MESSAGE
TextPacketType::Translation | TextPacketType::Popup | TextPacketType::JukeboxPopup => 2, // CATEGORY_MESSAGE_WITH_PARAMETERS
}
}
}
impl PacketRead for SText {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let r#type = TextPacketType::read(reader)?;
let localize = bool::read(reader)?;
let needs_translation = bool::read(reader)?;
let mut player_name = String::new();
let message;
let category = u8::read(reader)?;
let expected_dummies = Self::get_dummy_strings(category);
for &expected in expected_dummies {
let actual = String::read(reader)?;
if actual != expected {
return Err(Error::new(
ErrorKind::InvalidData,
format!(
"Dummy string mismatch: expected {}, got {}",
expected, actual
),
));
}
}
let r#type = TextPacketType::read(reader)?;
let mut source_name = String::new();
#[expect(unused)]
let mut message = String::new();
let mut parameters = Vec::new();
match r#type {
TextPacketType::Chat | TextPacketType::Whisper | TextPacketType::Announcement => {
source_name = String::read(reader)?;
message = String::read(reader)?;
}
TextPacketType::Raw
| TextPacketType::Tip
| TextPacketType::SystemMessage
| TextPacketType::TextObjectWhisper
| TextPacketType::TextObject
| TextPacketType::TextObjectAnnouncement => {
| TextPacketType::System
| TextPacketType::JsonWhisper
| TextPacketType::Json
| TextPacketType::JsonAnnouncement => {
message = String::read(reader)?;
}
TextPacketType::Chat | TextPacketType::Whisper | TextPacketType::Announcement => {
player_name = String::read(reader)?;
message = String::read(reader)?;
}
TextPacketType::Translate | TextPacketType::Popup | TextPacketType::JukeboxPopup => {
TextPacketType::Translation | TextPacketType::Popup | TextPacketType::JukeboxPopup => {
message = String::read(reader)?;
let count = VarUInt::read(reader)?.0 as usize;
parameters = Vec::with_capacity(count);
for _ in 0..count {
parameters.push(String::read(reader)?);
}
}
}
let sender_xuid = String::read(reader)?;
let platform_id = String::read(reader)?;
let filtered_message = String::read(reader)?;
let xuid = String::read(reader)?;
let platform_chat_id = String::read(reader)?;
let has_filtered = bool::read(reader)?;
let filtered_message = if has_filtered {
Some(String::read(reader)?)
} else {
None
};
Ok(Self {
needs_translation,
r#type,
localize,
player_name,
source_name,
message,
parameters,
sender_xuid,
platform_id,
xuid,
platform_chat_id,
filtered_message,
})
}
@ -99,22 +159,31 @@ impl PacketRead for SText {
impl PacketWrite for SText {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.needs_translation.write(writer)?;
let category = self.get_category();
category.write(writer)?;
for &dummy in Self::get_dummy_strings(category) {
dummy.to_string().write(writer)?;
}
self.r#type.write(writer)?;
self.localize.write(writer)?;
match self.r#type {
TextPacketType::Chat | TextPacketType::Whisper | TextPacketType::Announcement => {
self.source_name.write(writer)?;
self.message.write(writer)?;
}
TextPacketType::Raw
| TextPacketType::Tip
| TextPacketType::SystemMessage
| TextPacketType::TextObjectWhisper
| TextPacketType::TextObject
| TextPacketType::TextObjectAnnouncement => {
| TextPacketType::System
| TextPacketType::JsonWhisper
| TextPacketType::Json
| TextPacketType::JsonAnnouncement => {
self.message.write(writer)?;
}
TextPacketType::Chat | TextPacketType::Whisper | TextPacketType::Announcement => {
self.player_name.write(writer)?;
self.message.write(writer)?;
}
TextPacketType::Translate | TextPacketType::Popup | TextPacketType::JukeboxPopup => {
TextPacketType::Translation | TextPacketType::Popup | TextPacketType::JukeboxPopup => {
self.message.write(writer)?;
VarUInt(self.parameters.len() as u32).write(writer)?;
for param in &self.parameters {
@ -122,47 +191,36 @@ impl PacketWrite for SText {
}
}
}
self.sender_xuid.write(writer)?;
self.platform_id.write(writer)?;
self.filtered_message.write(writer)
self.xuid.write(writer)?;
self.platform_chat_id.write(writer)?;
if let Some(msg) = &self.filtered_message {
true.write(writer)?;
msg.write(writer)?;
} else {
false.write(writer)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum TextPacketType {
Raw = 0,
Chat = 1,
Translate = 2,
Translation = 2,
Popup = 3,
JukeboxPopup = 4,
Tip = 5,
SystemMessage = 6,
System = 6,
Whisper = 7,
Announcement = 8,
TextObjectWhisper = 9,
TextObject = 10,
TextObjectAnnouncement = 11,
}
impl PacketRead for TextPacketType {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
Ok(match u8::read(reader)? {
0 => Self::Raw,
1 => Self::Chat,
2 => Self::Translate,
3 => Self::Popup,
4 => Self::JukeboxPopup,
5 => Self::Tip,
6 => Self::SystemMessage,
7 => Self::Whisper,
8 => Self::Announcement,
9 => Self::TextObjectWhisper,
10 => Self::TextObject,
11 => Self::TextObjectAnnouncement,
_ => return Err(Error::new(std::io::ErrorKind::InvalidData, "")),
})
}
JsonWhisper = 9,
Json = 10,
JsonAnnouncement = 11,
}
impl PacketWrite for TextPacketType {
@ -170,3 +228,23 @@ impl PacketWrite for TextPacketType {
(*self as u8).write(writer)
}
}
impl PacketRead for TextPacketType {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
match u8::read(reader)? {
0 => Ok(Self::Raw),
1 => Ok(Self::Chat),
2 => Ok(Self::Translation),
3 => Ok(Self::Popup),
4 => Ok(Self::JukeboxPopup),
5 => Ok(Self::Tip),
6 => Ok(Self::System),
7 => Ok(Self::Whisper),
8 => Ok(Self::Announcement),
9 => Ok(Self::JsonWhisper),
10 => Ok(Self::Json),
11 => Ok(Self::JsonAnnouncement),
_ => Err(Error::new(ErrorKind::InvalidData, "Unknown Text Type")),
}
}
}

View File

@ -24,7 +24,7 @@ pub type BlockStateId = u16;
pub const CURRENT_MC_VERSION: &str = "1.21.11";
pub const LOWEST_SUPPRORTED_PROTOCOL_VERSION: u32 = 773;
pub const CURRENT_BEDROCK_MC_VERSION: &str = "1.21.32";
pub const CURRENT_BEDROCK_MC_VERSION: &str = "1.21.130";
pub const CURRENT_BEDROCK_MC_PROTOCOL: u32 = 898;
#[macro_export]

View File

@ -1785,18 +1785,7 @@ impl Player {
}
pub async fn send_system_message(&self, text: &TextComponent) {
match &self.client {
ClientPlatform::Java(client) => {
client
.enqueue_packet(&CSystemChatMessage::new(text, false))
.await;
}
ClientPlatform::Bedrock(client) => {
client
.send_game_packet(&SText::system_message(text.clone().get_text()))
.await;
}
}
self.send_system_message_raw(text, false).await;
}
pub async fn send_system_message_raw(&self, text: &TextComponent, overlay: bool) {
@ -2508,13 +2497,6 @@ impl EntityBase for Player {
if self.abilities.lock().await.invulnerable && damage_type != DamageType::GENERIC_KILL {
return false;
}
let dyn_self = self
.living_entity
.entity
.world
.get_entity_by_id(self.living_entity.entity.entity_id)
.await
.expect("Entity not found in world");
let result = self
.living_entity
.damage_with_context(caller, amount, damage_type, position, source, cause)
@ -2523,8 +2505,7 @@ impl EntityBase for Player {
let health = self.living_entity.health.load();
if health <= 0.0 {
let death_message =
LivingEntity::get_death_message(&*dyn_self, damage_type, source, cause)
.await;
LivingEntity::get_death_message(caller, damage_type, source, cause).await;
self.handle_killed(death_message).await;
}
}

View File

@ -49,14 +49,12 @@ struct CertificateChainPayload {
impl BedrockClient {
pub async fn handle_request_network_settings(&self, _packet: SRequestNetworkSettings) {
log::debug!("Bedrock: Sending Network settings");
self.send_game_packet(&CNetworkSettings::new(0, 0, false, 0, 0.0))
.await;
self.set_compression(CompressionInfo::default()).await;
}
pub async fn handle_login(self: &Arc<Self>, packet: SLogin, server: &Server) -> Option<()> {
log::debug!("Bedrock: handle login");
match self.try_handle_login(packet, server).await {
Ok(()) => Some(()),
Err(error) => {
@ -103,15 +101,7 @@ impl BedrockClient {
self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set)
.await;
self.write_game_packet_to_set(
&CResourcePacksInfo::new(
false,
false,
false,
true,
uuid::Uuid::default(),
String::new(),
Vec::new(),
),
&CResourcePacksInfo::new(false, false, false, false, Uuid::default(), String::new()),
&mut frame_set,
)
.await;

View File

@ -484,7 +484,6 @@ impl BedrockClient {
packet: RawPacket,
) -> Result<(), Error> {
let payload = &mut Cursor::new(&packet.payload);
log::error!("packet id {}", packet.id);
match packet.id {
SRequestNetworkSettings::PACKET_ID => {
self.handle_request_network_settings(SRequestNetworkSettings::read(payload)?)

View File

@ -1,3 +1,4 @@
use std::num::NonZeroU32;
use std::pin::Pin;
use std::sync::Weak;
use std::sync::atomic::Ordering::Relaxed;
@ -56,7 +57,10 @@ use pumpkin_data::{
use pumpkin_data::{BlockDirection, BlockState};
use pumpkin_inventory::screen_handler::InventoryPlayer;
use pumpkin_nbt::{compound::NbtCompound, to_bytes_unnamed};
use pumpkin_protocol::bedrock::client::chunk_radius_update::CChunkRadiusUpdate;
use pumpkin_protocol::bedrock::client::network_chunk_publisher_update::CNetworkChunkPublisherUpdate;
use pumpkin_protocol::bedrock::client::start_game::CStartGame;
use pumpkin_protocol::bedrock::frame_set::FrameSet;
use pumpkin_protocol::java::client::play::CPlayerSpawnPosition;
use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata};
use pumpkin_protocol::ser::serializer::Serializer;
@ -1161,7 +1165,7 @@ impl World {
self.dimension.min_y
}
#[expect(clippy::too_many_lines)]
#[allow(clippy::too_many_lines)]
pub async fn spawn_bedrock_player(
&self,
base_config: &BasicConfiguration,
@ -1249,15 +1253,15 @@ impl World {
entity_id: VarLong(runtime_id as _),
runtime_entity_id: VarULong(runtime_id),
player_gamemode: player.gamemode.load(),
position: Vector3::new(0.0, 100.0, 0.0),
position: Vector3::new(0.0, 200.0, 0.0),
pitch: 0.0,
yaw: 0.0,
level_settings,
level_id: String::new(),
level_name: "World".to_string(),
level_name: "Pumpkin world".to_string(),
premium_world_template_id: String::new(),
is_trial: false,
rewind_history_size: VarInt(0),
rewind_history_size: VarInt(40),
server_authoritative_block_breaking: false,
current_level_time: self.level_time.lock().await.world_age as _,
enchantment_seed: VarInt(0),
@ -1278,10 +1282,15 @@ impl World {
// TODO The client needs extra biome data for this
enable_clientside_generation: false,
blocknetwork_ids_are_hashed: false,
disable_client_sounds: false,
server_auth_sounds: false,
})
.await;
// chunker::update_position(&player).await;
client
.send_game_packet(&CChunkRadiusUpdate {
chunk_radius: VarInt(player.config.read().await.view_distance.get().into()),
})
.await;
chunker::update_position(&player).await;
client
.send_game_packet(&CreativeContent {
groups: &[Group {
@ -1293,8 +1302,20 @@ impl World {
})
.await;
let mut frame_set = FrameSet::default();
client
.send_game_packet(&CUpdateAttributes {
.write_game_packet_to_set(
&CNetworkChunkPublisherUpdate::new(
BlockPos::new(0, 200, 0),
NonZeroU32::from(player.config.read().await.view_distance).into(),
),
&mut frame_set,
)
.await;
client
.write_game_packet_to_set(
&CUpdateAttributes {
runtime_id: VarULong(runtime_id),
attributes: vec![Attribute {
min_value: 0.0,
@ -1307,15 +1328,22 @@ impl World {
modifiers_list_size: VarUInt(0),
}],
player_tick: VarULong(0),
})
},
&mut frame_set,
)
.await;
player.send_abilities_update().await;
client
.write_game_packet_to_set(&CPlayStatus::PlayerSpawn, &mut frame_set)
.await;
client.send_frame_set(frame_set, 0x84).await;
{
let mut abilities = player.abilities.lock().await;
abilities.set_for_gamemode(player.gamemode.load());
};
client.send_game_packet(&CPlayStatus::PlayerSpawn).await;
player.send_abilities_update().await;
}
#[expect(clippy::too_many_lines)]