feat(bedrock): add resource pack support

This commit is contained in:
Alexander Medvedev 2026-05-06 11:49:20 +02:00
parent f1263fff21
commit 3b2eaf9467
11 changed files with 177 additions and 95 deletions

View File

@ -319,7 +319,7 @@ impl LoadConfiguration for AdvancedConfiguration {
} }
fn validate(&self) { fn validate(&self) {
self.resource_pack.validate(); //self.resource_pack.validate();
} }
} }

View File

@ -1,12 +1,17 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Configuration for server resource pack distribution.
///
/// Controls whether a resource pack is offered or enforced,
/// along with its metadata and client prompt behaviour.
#[derive(Deserialize, Serialize, Default)] #[derive(Deserialize, Serialize, Default)]
#[serde(default)] #[serde(default)]
pub struct ResourcePackConfig { pub struct ResourcePackConfig {
pub java: JavaResourcePackConfig,
pub bedrock: BedrockResourcePackConfig,
}
/// Java-specific resource pack configuration (Single URL/Hash)
#[derive(Deserialize, Serialize, Default)]
#[serde(default)]
pub struct JavaResourcePackConfig {
/// Whether the resource pack system is enabled. /// Whether the resource pack system is enabled.
pub enabled: bool, pub enabled: bool,
/// The URL to the resource pack. /// The URL to the resource pack.
@ -19,23 +24,33 @@ pub struct ResourcePackConfig {
pub force: bool, pub force: bool,
} }
impl ResourcePackConfig { /// Bedrock-specific configuration (Supports multiple local/remote packs)
pub fn validate(&self) { #[derive(Deserialize, Serialize, Default)]
if !self.enabled { #[serde(default)]
return; pub struct BedrockResourcePackConfig {
pub enabled: bool,
/// If true, players cannot join without accepting packs.
pub force: bool,
/// List of packs to be sent to the client.
pub packs: Vec<BedrockPack>,
} }
assert_eq!( #[derive(Deserialize, Serialize)]
!self.url.is_empty(), pub struct BedrockPack {
!self.sha1.is_empty(), pub uuid: Uuid,
"Resource pack path or SHA1 hash is missing" pub version: String,
); pub size: u64,
pub download_url: String,
let hash_len = self.sha1.len(); #[serde(default)]
pub content_key: String,
assert_eq!( #[serde(default)]
hash_len, 40, pub sub_pack_name: String,
"Resource pack SHA1 hash is the wrong length (should be 40, is {hash_len})" #[serde(default)]
); pub content_id: String,
} #[serde(default)]
pub has_scripts: bool,
#[serde(default)]
pub addon_pack: bool,
#[serde(default)]
pub rtx_enabled: bool,
} }

View File

@ -25,4 +25,4 @@ pub mod set_time;
pub mod set_title; pub mod set_title;
pub mod start_game; pub mod start_game;
pub mod update_abilities; pub mod update_abilities;
pub mod update_artributes; pub mod update_attributes;

View File

@ -1,33 +1,35 @@
use crate::{bedrock::client::start_game::Experiments, serial::PacketWrite};
use pumpkin_macros::packet; use pumpkin_macros::packet;
use crate::{ #[derive(PacketWrite)]
bedrock::client::start_game::Experiments, codec::var_uint::VarUInt, serial::PacketWrite, pub struct ResourcePackStackEntry {
}; pub uuid: String,
pub version: String,
pub sub_pack_name: String,
}
#[derive(PacketWrite)] #[derive(PacketWrite)]
#[packet(7)] #[packet(7)]
pub struct CResourcePackStackPacket { pub struct CResourcePackStackPacket {
// https://mojang.github.io/bedrock-protocol-docs/html/ResourcePackStackPacket.html pub resource_pack_required: bool,
resource_pack_required: bool, pub resource_packs: Vec<ResourcePackStackEntry>,
texture_pack_list_size: VarUInt, pub game_version: String,
game_version: String, pub experiments: Experiments,
experiments: Experiments, pub include_editor_packs: bool,
/// When connecting to an Editor world, include the vanilla editor packs in the stack
include_editor_packs: bool,
} }
impl CResourcePackStackPacket { impl CResourcePackStackPacket {
#[must_use] #[must_use]
pub const fn new( pub const fn new(
resource_pack_required: bool, resource_pack_required: bool,
texture_pack_list_size: VarUInt, resource_packs: Vec<ResourcePackStackEntry>,
game_version: String, game_version: String,
experiments: Experiments, experiments: Experiments,
include_editor_packs: bool, include_editor_packs: bool,
) -> Self { ) -> Self {
Self { Self {
resource_pack_required, resource_pack_required,
texture_pack_list_size, resource_packs,
game_version, game_version,
experiments, experiments,
include_editor_packs, include_editor_packs,

View File

@ -1,39 +1,62 @@
use pumpkin_macros::packet;
use crate::serial::PacketWrite; use crate::serial::PacketWrite;
use pumpkin_macros::packet;
use std::io::{Error, Write};
pub struct ResourcePackEntry {
pub uuid: String,
pub version: String,
pub size: u64,
pub content_key: String,
pub sub_pack_name: String,
pub content_id: String,
pub has_scripts: bool,
pub addon_pack: bool,
pub rtx_enabled: bool,
pub download_url: String,
}
impl PacketWrite for ResourcePackEntry {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
self.uuid.write(writer)?;
self.version.write(writer)?;
// Bedrock uses Little Endian u64 for size
writer.write_all(&self.size.to_le_bytes())?;
self.content_key.write(writer)?;
self.sub_pack_name.write(writer)?;
self.content_id.write(writer)?;
self.has_scripts.write(writer)?;
Ok(())
}
}
#[derive(PacketWrite)]
#[packet(6)] #[packet(6)]
pub struct CResourcePacksInfo { pub struct CResourcePacksInfo {
resource_pack_required: bool, pub resource_pack_required: bool,
has_addon_packs: bool, pub has_addon_packs: bool,
has_scripts: bool, pub has_scripts: bool,
is_vibrant_visuals_force_disabled: bool, pub is_vibrant_visuals_force_disabled: bool,
world_template_id: uuid::Uuid, pub world_template_id: uuid::Uuid,
world_template_version: String, pub world_template_version: String,
resource_packs_size: u16, // TODO: Add more pub resource_packs: Vec<ResourcePackEntry>,
} }
impl CResourcePacksInfo { impl PacketWrite for CResourcePacksInfo {
#[must_use] fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
#[expect(clippy::fn_params_excessive_bools)] self.resource_pack_required.write(writer)?;
pub const fn new( self.has_addon_packs.write(writer)?;
resource_pack_required: bool, self.has_scripts.write(writer)?;
has_addon_packs: bool, self.is_vibrant_visuals_force_disabled.write(writer)?;
has_scripts: bool,
is_vibrant_visuals_force_disabled: bool, self.world_template_id.write(writer)?;
world_template_id: uuid::Uuid,
world_template_version: String, self.world_template_version.write(writer)?;
) -> Self {
Self { let len = self.resource_packs.len() as u16;
resource_pack_required, writer.write_all(&len.to_le_bytes())?;
has_addon_packs,
has_scripts, for entry in &self.resource_packs {
is_vibrant_visuals_force_disabled, entry.write(writer)?;
world_template_id, }
world_template_version,
// TODO Ok(())
resource_packs_size: 0,
}
} }
} }

View File

@ -94,7 +94,7 @@ pub async fn send_attribute_updates_for_living(
living: &crate::entity::living::LivingEntity, living: &crate::entity::living::LivingEntity,
attributes: Vec<Attributes>, attributes: Vec<Attributes>,
) { ) {
use pumpkin_protocol::bedrock::client::update_artributes::{ use pumpkin_protocol::bedrock::client::update_attributes::{
Attribute as BeAttribute, CUpdateAttributes as BePacket, Attribute as BeAttribute, CUpdateAttributes as BePacket,
}; };
use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::codec::var_int::VarInt;

View File

@ -2,11 +2,7 @@ use crate::{
net::{ClientPlatform, DisconnectReason, GameProfile, PlayerConfig, bedrock::BedrockClient}, net::{ClientPlatform, DisconnectReason, GameProfile, PlayerConfig, bedrock::BedrockClient},
server::Server, server::Server,
}; };
use pumpkin_protocol::bedrock::server::{ use pumpkin_protocol::bedrock::{
login::ClientData, resource_pack_response::SResourcePackResponse,
};
use pumpkin_protocol::{
bedrock::{
client::{ client::{
network_settings::CNetworkSettings, play_status::CPlayStatus, network_settings::CNetworkSettings, play_status::CPlayStatus,
resource_pack_stack::CResourcePackStackPacket, resource_packs_info::CResourcePacksInfo, resource_pack_stack::CResourcePackStackPacket, resource_packs_info::CResourcePacksInfo,
@ -14,8 +10,10 @@ use pumpkin_protocol::{
}, },
frame_set::FrameSet, frame_set::FrameSet,
server::{login::SLogin, request_network_settings::SRequestNetworkSettings}, server::{login::SLogin, request_network_settings::SRequestNetworkSettings},
}, };
codec::var_uint::VarUInt, use pumpkin_protocol::bedrock::{
client::{resource_pack_stack::ResourcePackStackEntry, resource_packs_info::ResourcePackEntry},
server::{login::ClientData, resource_pack_response::SResourcePackResponse},
}; };
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};
@ -183,10 +181,36 @@ impl BedrockClient {
self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set) self.write_game_packet_to_set(&CPlayStatus::LoginSuccess, &mut frame_set)
.await; .await;
self.write_game_packet_to_set( let br_config = &server.advanced_config.resource_pack.bedrock;
&CResourcePacksInfo::new(false, false, false, false, Uuid::default(), String::new()),
&mut frame_set, let mut entries = Vec::new();
) if br_config.enabled {
for pack in &br_config.packs {
entries.push(ResourcePackEntry {
uuid: pack.uuid.to_string(),
version: pack.version.clone(),
size: pack.size,
download_url: pack.download_url.clone(),
content_key: pack.content_key.clone(),
sub_pack_name: pack.sub_pack_name.clone(),
content_id: pack.content_id.clone(),
has_scripts: pack.has_scripts,
addon_pack: pack.addon_pack,
rtx_enabled: pack.rtx_enabled,
});
}
}
let packs_info = CResourcePacksInfo {
resource_pack_required: br_config.force,
has_addon_packs: false,
has_scripts: false,
is_vibrant_visuals_force_disabled: false,
world_template_id: uuid::Uuid::nil(),
world_template_version: String::new(),
resource_packs: entries,
};
self.write_game_packet_to_set(&packs_info, &mut frame_set)
.await; .await;
self.send_frame_set(frame_set, 0x84).await; self.send_frame_set(frame_set, 0x84).await;
@ -234,10 +258,27 @@ impl BedrockClient {
debug!("Bedrock: SResourcePackResponse::STATUS_HAVE_ALL_PACKS"); debug!("Bedrock: SResourcePackResponse::STATUS_HAVE_ALL_PACKS");
let mut frame_set = FrameSet::default(); let mut frame_set = FrameSet::default();
let br_config = &server.advanced_config.resource_pack.bedrock;
// Convert your config packs into protocol stack entries
let resource_packs = if br_config.enabled {
br_config
.packs
.iter()
.map(|pack| ResourcePackStackEntry {
uuid: pack.uuid.to_string(),
version: pack.version.clone(),
sub_pack_name: String::new(),
})
.collect()
} else {
Vec::new()
};
self.write_game_packet_to_set( self.write_game_packet_to_set(
&CResourcePackStackPacket::new( &CResourcePackStackPacket::new(
false, br_config.force,
VarUInt(0), resource_packs,
CURRENT_BEDROCK_MC_VERSION.to_string(), CURRENT_BEDROCK_MC_VERSION.to_string(),
Experiments { Experiments {
names_size: 0, names_size: 0,
@ -248,6 +289,7 @@ impl BedrockClient {
&mut frame_set, &mut frame_set,
) )
.await; .await;
self.send_frame_set(frame_set, 0x84).await; self.send_frame_set(frame_set, 0x84).await;
} }
SResourcePackResponse::STATUS_COMPLETED => { SResourcePackResponse::STATUS_COMPLETED => {

View File

@ -76,7 +76,7 @@ impl JavaClient {
server: &Server, server: &Server,
packet: SConfigResourcePack, packet: SConfigResourcePack,
) { ) {
let resource_config = &server.advanced_config.resource_pack; let resource_config = &server.advanced_config.resource_pack.java;
if resource_config.enabled { if resource_config.enabled {
let expected_uuid = let expected_uuid =
uuid::Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes()); uuid::Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes());

View File

@ -368,7 +368,7 @@ impl JavaClient {
self.send_packet_now(&CConfigServerLinks::new(&links)).await; self.send_packet_now(&CConfigServerLinks::new(&links)).await;
} }
let resource_config = &server.advanced_config.resource_pack; let resource_config = &server.advanced_config.resource_pack.java;
if resource_config.enabled { if resource_config.enabled {
let uuid = Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes()); let uuid = Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes());
let resource_pack = CConfigAddResourcePack::new( let resource_pack = CConfigAddResourcePack::new(

View File

@ -80,7 +80,7 @@ use pumpkin_protocol::{
creative_content::{CreativeContent, Group}, creative_content::{CreativeContent, Group},
gamerules_changed::GameRules, gamerules_changed::GameRules,
start_game::{Experiments, GamePublishSetting, LevelSettings}, start_game::{Experiments, GamePublishSetting, LevelSettings},
update_artributes::{Attribute, CUpdateAttributes}, update_attributes::{Attribute, CUpdateAttributes},
}, },
network_item::NetworkItemDescriptor, network_item::NetworkItemDescriptor,
server::text::SText, server::text::SText,