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 175 additions and 93 deletions

View File

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

View File

@@ -1,12 +1,17 @@
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)]
#[serde(default)]
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.
pub enabled: bool,
/// The URL to the resource pack.
@@ -19,23 +24,33 @@ pub struct ResourcePackConfig {
pub force: bool,
}
impl ResourcePackConfig {
pub fn validate(&self) {
if !self.enabled {
return;
}
/// Bedrock-specific configuration (Supports multiple local/remote packs)
#[derive(Deserialize, Serialize, Default)]
#[serde(default)]
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!(
!self.url.is_empty(),
!self.sha1.is_empty(),
"Resource pack path or SHA1 hash is missing"
);
let hash_len = self.sha1.len();
assert_eq!(
hash_len, 40,
"Resource pack SHA1 hash is the wrong length (should be 40, is {hash_len})"
);
}
#[derive(Deserialize, Serialize)]
pub struct BedrockPack {
pub uuid: Uuid,
pub version: String,
pub size: u64,
pub download_url: String,
#[serde(default)]
pub content_key: String,
#[serde(default)]
pub sub_pack_name: String,
#[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 start_game;
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 crate::{
bedrock::client::start_game::Experiments, codec::var_uint::VarUInt, serial::PacketWrite,
};
#[derive(PacketWrite)]
pub struct ResourcePackStackEntry {
pub uuid: String,
pub version: String,
pub sub_pack_name: String,
}
#[derive(PacketWrite)]
#[packet(7)]
pub struct CResourcePackStackPacket {
// https://mojang.github.io/bedrock-protocol-docs/html/ResourcePackStackPacket.html
resource_pack_required: bool,
texture_pack_list_size: VarUInt,
game_version: String,
experiments: Experiments,
/// When connecting to an Editor world, include the vanilla editor packs in the stack
include_editor_packs: bool,
pub resource_pack_required: bool,
pub resource_packs: Vec<ResourcePackStackEntry>,
pub game_version: String,
pub experiments: Experiments,
pub include_editor_packs: bool,
}
impl CResourcePackStackPacket {
#[must_use]
pub const fn new(
resource_pack_required: bool,
texture_pack_list_size: VarUInt,
resource_packs: Vec<ResourcePackStackEntry>,
game_version: String,
experiments: Experiments,
include_editor_packs: bool,
) -> Self {
Self {
resource_pack_required,
texture_pack_list_size,
resource_packs,
game_version,
experiments,
include_editor_packs,

View File

@@ -1,39 +1,62 @@
use pumpkin_macros::packet;
use crate::serial::PacketWrite;
#[derive(PacketWrite)]
#[packet(6)]
pub struct CResourcePacksInfo {
resource_pack_required: bool,
has_addon_packs: bool,
has_scripts: bool,
is_vibrant_visuals_force_disabled: bool,
world_template_id: uuid::Uuid,
world_template_version: String,
resource_packs_size: u16, // TODO: Add more
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 CResourcePacksInfo {
#[must_use]
#[expect(clippy::fn_params_excessive_bools)]
pub const fn new(
resource_pack_required: bool,
has_addon_packs: bool,
has_scripts: bool,
is_vibrant_visuals_force_disabled: bool,
world_template_id: uuid::Uuid,
world_template_version: String,
) -> Self {
Self {
resource_pack_required,
has_addon_packs,
has_scripts,
is_vibrant_visuals_force_disabled,
world_template_id,
world_template_version,
// TODO
resource_packs_size: 0,
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(())
}
}
#[packet(6)]
pub struct CResourcePacksInfo {
pub resource_pack_required: bool,
pub has_addon_packs: bool,
pub has_scripts: bool,
pub is_vibrant_visuals_force_disabled: bool,
pub world_template_id: uuid::Uuid,
pub world_template_version: String,
pub resource_packs: Vec<ResourcePackEntry>,
}
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)?;
let len = self.resource_packs.len() as u16;
writer.write_all(&len.to_le_bytes())?;
for entry in &self.resource_packs {
entry.write(writer)?;
}
Ok(())
}
}

View File

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

View File

@@ -2,20 +2,18 @@ use crate::{
net::{ClientPlatform, DisconnectReason, GameProfile, PlayerConfig, bedrock::BedrockClient},
server::Server,
};
use pumpkin_protocol::bedrock::server::{
login::ClientData, resource_pack_response::SResourcePackResponse,
};
use pumpkin_protocol::{
bedrock::{
client::{
network_settings::CNetworkSettings, play_status::CPlayStatus,
resource_pack_stack::CResourcePackStackPacket, resource_packs_info::CResourcePacksInfo,
start_game::Experiments,
},
frame_set::FrameSet,
server::{login::SLogin, request_network_settings::SRequestNetworkSettings},
use pumpkin_protocol::bedrock::{
client::{
network_settings::CNetworkSettings, play_status::CPlayStatus,
resource_pack_stack::CResourcePackStackPacket, resource_packs_info::CResourcePacksInfo,
start_game::Experiments,
},
codec::var_uint::VarUInt,
frame_set::FrameSet,
server::{login::SLogin, request_network_settings::SRequestNetworkSettings},
};
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_world::{CURRENT_BEDROCK_MC_PROTOCOL, CURRENT_BEDROCK_MC_VERSION};
@@ -183,11 +181,37 @@ 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, false, Uuid::default(), String::new()),
&mut frame_set,
)
.await;
let br_config = &server.advanced_config.resource_pack.bedrock;
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;
self.send_frame_set(frame_set, 0x84).await;
@@ -234,10 +258,27 @@ impl BedrockClient {
debug!("Bedrock: SResourcePackResponse::STATUS_HAVE_ALL_PACKS");
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(
&CResourcePackStackPacket::new(
false,
VarUInt(0),
br_config.force,
resource_packs,
CURRENT_BEDROCK_MC_VERSION.to_string(),
Experiments {
names_size: 0,
@@ -248,6 +289,7 @@ impl BedrockClient {
&mut frame_set,
)
.await;
self.send_frame_set(frame_set, 0x84).await;
}
SResourcePackResponse::STATUS_COMPLETED => {

View File

@@ -76,7 +76,7 @@ impl JavaClient {
server: &Server,
packet: SConfigResourcePack,
) {
let resource_config = &server.advanced_config.resource_pack;
let resource_config = &server.advanced_config.resource_pack.java;
if resource_config.enabled {
let expected_uuid =
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;
}
let resource_config = &server.advanced_config.resource_pack;
let resource_config = &server.advanced_config.resource_pack.java;
if resource_config.enabled {
let uuid = Uuid::new_v3(&uuid::Uuid::NAMESPACE_DNS, resource_config.url.as_bytes());
let resource_pack = CConfigAddResourcePack::new(

View File

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