feat(bedrock): add support for vibrant visuals (#2888)

* feat(bedrock): support vibrant visuals biome data

* Use NBT biome definitions for Bedrock

* Pre-generate Bedrock biome definitions

---------

Co-authored-by: Zoltán Virágh <zoltan.viragh@cloudtalk.io>
This commit is contained in:
ZlordHUN
2026-08-12 12:06:13 +02:00
committed by GitHub
parent 1529e811e5
commit 7f6e5d21eb
11 changed files with 614 additions and 3 deletions

View File

@@ -19,6 +19,10 @@ 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.
- `biome_definitions.nbt`
- downloaded from [Kaooot/bedrock-network-data](https://github.com/Kaooot/bedrock-network-data/blob/master/release/1.26.40/biome_definitions.nbt)
- Contains the gzip-compressed vanilla biome registry extracted from Bedrock Dedicated Server 1.26.40.
- Validated and converted by `pumpkin-codegen` into the static `BiomeDefinitionList` wire payload used during Bedrock world initialization.
- `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.

Binary file not shown.

View File

@@ -48,11 +48,13 @@ default = [
"placed_feature",
"configured_feature",
"slot_ranges",
"bedrock_creative"
"bedrock_creative",
"bedrock_biome"
]
advancement = []
bedrock_creative = []
bedrock_biome = []
item = []
packet = []
jukebox_song = []

File diff suppressed because one or more lines are too long

View File

@@ -274,6 +274,11 @@ pub mod particle_id_remap;
#[path = "generated/bedrock_creative.rs"]
pub mod bedrock_creative;
#[cfg(feature = "bedrock_biome")]
#[rustfmt::skip]
#[path = "generated/bedrock_biome.rs"]
pub mod bedrock_biome;
#[cfg(feature = "tag")]
#[rustfmt::skip]
#[path = "generated/tag.rs"]

View File

@@ -13,7 +13,7 @@ query = []
[dependencies]
pumpkin-nbt.workspace = true
pumpkin-data = { workspace = true, features = ["packet", "item_id_remap", "entity_id_remap", "sound_id_remap", "particle_id_remap", "recipes", "bedrock_creative"] }
pumpkin-data = { workspace = true, features = ["packet", "item_id_remap", "entity_id_remap", "sound_id_remap", "particle_id_remap", "recipes", "bedrock_creative", "bedrock_biome"] }
pumpkin-macros.workspace = true
pumpkin-world.workspace = true
pumpkin-util.workspace = true

View File

@@ -0,0 +1,34 @@
use std::io::{Error, Write};
use pumpkin_data::bedrock_biome::BIOME_DEFINITIONS;
use pumpkin_macros::packet;
use crate::serial::PacketWrite;
#[packet(122)]
pub struct CBiomeDefinitionList;
impl PacketWrite for CBiomeDefinitionList {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
writer.write_all(BIOME_DEFINITIONS)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{codec::var_uint::VarUInt, serial::PacketRead};
#[test]
fn generated_registry_contains_all_biomes() {
let biome_count = VarUInt::read(&mut BIOME_DEFINITIONS.as_slice())
.expect("generated biome count must decode");
assert_eq!(
biome_count.0 as usize,
pumpkin_data::bedrock_biome::BIOME_COUNT
);
assert_eq!(biome_count.0, 88);
assert!(BIOME_DEFINITIONS.len() > 100_000);
}
}

View File

@@ -2,6 +2,7 @@ pub mod add_actor;
pub mod add_item_actor;
pub mod add_player;
pub mod available_commands;
pub mod biome_definition_list;
pub mod boss_event;
pub mod change_dimension;
pub mod chunk_radius_update;
@@ -56,6 +57,7 @@ pub use add_actor::*;
pub use add_item_actor::*;
pub use add_player::*;
pub use available_commands::*;
pub use biome_definition_list::*;
pub use boss_event::*;
pub use change_dimension::*;
pub use chunk_radius_update::*;

View File

@@ -3,9 +3,9 @@ use dashmap::DashMap;
use pumpkin_data::attributes::Attributes;
use pumpkin_data::chunk::Biome;
use pumpkin_data::item::{BedrockItem, BedrockItemVersion};
use pumpkin_protocol::bedrock::client::EntityProperties;
use pumpkin_protocol::bedrock::client::item_registry::{CItemRegistry, ItemDefinition};
use pumpkin_protocol::bedrock::client::level_event::{CLevelEvent, LevelEvent};
use pumpkin_protocol::bedrock::client::{CBiomeDefinitionList, EntityProperties};
use pumpkin_protocol::bedrock::network_item::{NetworkItemDescriptor, NetworkItemStackDescriptor};
use pumpkin_protocol::codec::data_component::data_to_proto_sound;
use pumpkin_world::generation::proto_chunk::GenerationCache;
@@ -2075,6 +2075,8 @@ impl World {
})
.await;
client.send_game_packet(&CBiomeDefinitionList).await;
client
.send_game_packet(&CItemRegistry {
items: BedrockItem::ALL_BEDROCK_ITEMS

View File

@@ -0,0 +1,557 @@
use std::{
fs,
io::{Cursor, Error, ErrorKind, Write},
};
use proc_macro2::{Literal, TokenStream};
use pumpkin_nbt::{NbtCompound, nbt_compress::read_gzip_compound_tag, tag::NbtTag};
use quote::quote;
trait PacketWrite {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error>;
}
macro_rules! packet_write_le {
($($ty:ty),+ $(,)?) => {$ (
impl PacketWrite for $ty {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
writer.write_all(&self.to_le_bytes())
}
}
)+ };
}
packet_write_le!(i16, i32, u8, u16, u32, f32);
impl PacketWrite for bool {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
writer.write_all(&[u8::from(*self)])
}
}
impl PacketWrite for str {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
VarUInt(
u32::try_from(self.len())
.map_err(|_| Error::new(ErrorKind::InvalidData, "biome string is too long"))?,
)
.write(writer)?;
writer.write_all(self.as_bytes())
}
}
struct VarUInt(u32);
impl PacketWrite for VarUInt {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
let mut value = self.0;
loop {
let mut byte = (value & 0x7f) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
writer.write_all(&[byte])?;
if value == 0 {
return Ok(());
}
}
}
}
struct VarInt(i32);
impl PacketWrite for VarInt {
fn write<W: Write>(&self, writer: &mut W) -> Result<(), Error> {
VarUInt(self.0 as u32).write(writer)
}
}
fn number(compound: &NbtCompound, key: &str) -> i64 {
match compound.get(key) {
Some(NbtTag::Byte(value)) => i64::from(*value),
Some(NbtTag::Short(value)) => i64::from(*value),
Some(NbtTag::Int(value)) => i64::from(*value),
Some(NbtTag::Long(value)) => *value,
Some(NbtTag::Float(value)) => *value as i64,
Some(NbtTag::Double(value)) => *value as i64,
_ => 0,
}
}
fn decimal(compound: &NbtCompound, key: &str) -> f32 {
match compound.get(key) {
Some(NbtTag::Byte(value)) => f32::from(*value),
Some(NbtTag::Short(value)) => f32::from(*value),
Some(NbtTag::Int(value)) => *value as f32,
Some(NbtTag::Long(value)) => *value as f32,
Some(NbtTag::Float(value)) => *value,
Some(NbtTag::Double(value)) => *value as f32,
_ => 0.0,
}
}
fn text<'a>(compound: &'a NbtCompound, key: &str) -> &'a str {
compound.get_string(key).unwrap_or_default()
}
fn compounds<'a>(compound: &'a NbtCompound, key: &str) -> Vec<&'a NbtCompound> {
compound
.get_list(key)
.unwrap_or_default()
.iter()
.filter_map(NbtTag::extract_compound)
.collect()
}
fn numbers(compound: &NbtCompound, key: &str) -> Vec<i64> {
match compound.get(key) {
Some(NbtTag::List(values)) => values
.iter()
.map(|value| match value {
NbtTag::Byte(value) => i64::from(*value),
NbtTag::Short(value) => i64::from(*value),
NbtTag::Int(value) => i64::from(*value),
NbtTag::Long(value) => *value,
_ => 0,
})
.collect(),
Some(NbtTag::IntArray(values)) => values.iter().copied().map(i64::from).collect(),
Some(NbtTag::LongArray(values)) => values.clone(),
_ => Vec::new(),
}
}
fn decimals(compound: &NbtCompound, key: &str) -> Vec<f32> {
compound
.get_list(key)
.unwrap_or_default()
.iter()
.map(|value| match value {
NbtTag::Float(value) => *value,
NbtTag::Double(value) => *value as f32,
_ => 0.0,
})
.collect()
}
fn as_i16(value: i64) -> i16 {
i16::try_from(value).unwrap_or_default()
}
fn as_i32(value: i64) -> i32 {
i32::try_from(value).unwrap_or_default()
}
fn as_u8(value: i64) -> u8 {
u8::try_from(value).unwrap_or_default()
}
fn as_u16(value: i64) -> u16 {
u16::try_from(value).unwrap_or(u16::MAX)
}
fn as_u32(value: i64) -> u32 {
u32::try_from(value).unwrap_or_default()
}
fn write_len<W: Write>(writer: &mut W, length: usize) -> Result<(), Error> {
VarUInt(
u32::try_from(length)
.map_err(|_| Error::new(ErrorKind::InvalidData, "biome list is too long"))?,
)
.write(writer)
}
fn write_expression_op<W: Write>(
writer: &mut W,
compound: &NbtCompound,
key: &str,
) -> Result<(), Error> {
VarInt(if compound.has(key) {
as_i32(number(compound, key))
} else {
-1
})
.write(writer)
}
fn write_compound_list<W, F>(
writer: &mut W,
entries: Vec<&NbtCompound>,
mut encode: F,
) -> Result<(), Error>
where
W: Write,
F: FnMut(&mut W, &NbtCompound) -> Result<(), Error>,
{
write_len(writer, entries.len())?;
for entry in entries {
encode(writer, entry)?;
}
Ok(())
}
fn write_optional_compound<W, F>(
writer: &mut W,
compound: &NbtCompound,
key: &str,
encode: F,
) -> Result<(), Error>
where
W: Write,
F: FnOnce(&mut W, &NbtCompound) -> Result<(), Error>,
{
if let Some(value) = compound.get_compound(key) {
true.write(writer)?;
encode(writer, value)
} else {
false.write(writer)
}
}
fn write_coordinate<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
write_expression_op(writer, data, "minValueType")?;
as_u16(number(data, "minValue")).write(writer)?;
write_expression_op(writer, data, "maxValueType")?;
as_u16(number(data, "maxValue")).write(writer)?;
as_u32(number(data, "gridOffset")).write(writer)?;
as_u32(number(data, "gridStepSize")).write(writer)?;
VarInt(as_i32(number(data, "distribution"))).write(writer)
}
fn write_scatter<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
write_compound_list(writer, compounds(data, "coordinates"), write_coordinate)?;
VarInt(as_i32(number(data, "evalOrder"))).write(writer)?;
write_expression_op(writer, data, "chancePercentType")?;
as_u16(number(data, "chancePercent")).write(writer)?;
as_i32(number(data, "chanceNumerator")).write(writer)?;
as_i32(number(data, "chanceDenominator")).write(writer)?;
write_expression_op(writer, data, "iterationsType")?;
as_u16(number(data, "iterations")).write(writer)
}
fn write_feature<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
if let Some(scatter) = data.get_compound("scatter") {
write_scatter(writer, scatter)?;
} else {
write_scatter(writer, &NbtCompound::new())?;
}
as_u16(number(data, "feature")).write(writer)?;
as_u16(number(data, "identifier")).write(writer)?;
as_u16(number(data, "pass")).write(writer)?;
(number(data, "canUseInternalFeature") != 0).write(writer)
}
fn write_climate<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
decimal(data, "temperature").write(writer)?;
decimal(data, "downfall").write(writer)?;
decimal(data, "snowAccumulationMin").write(writer)?;
decimal(data, "snowAccumulationMax").write(writer)
}
fn write_mountain_params<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
as_i32(number(data, "steepBlock")).write(writer)?;
for key in [
"northSlopes",
"southSlopes",
"westSlopes",
"eastSlopes",
"topSlideEnabled",
] {
(number(data, key) != 0).write(writer)?;
}
Ok(())
}
fn write_surface_material<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
for key in [
"topBlock",
"midBlock",
"seaFloorBlock",
"foundationBlock",
"seaBlock",
"seaFloorDepth",
] {
as_i32(number(data, key)).write(writer)?;
}
Ok(())
}
fn write_element<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
decimal(data, "noiseFrequencyScale").write(writer)?;
decimal(data, "noiseLowerBound").write(writer)?;
decimal(data, "noiseUpperBound").write(writer)?;
write_expression_op(writer, data, "heightMinType")?;
as_u16(number(data, "heightMin")).write(writer)?;
write_expression_op(writer, data, "heightMaxType")?;
as_u16(number(data, "heightMax")).write(writer)?;
if let Some(materials) = data.get_compound("adjustedMaterials") {
write_surface_material(writer, materials)
} else {
write_surface_material(writer, &NbtCompound::new())
}
}
fn write_weighted<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
as_u16(number(data, "biomeIdentifier")).write(writer)?;
as_i32(number(data, "weight")).write(writer)
}
fn write_conditional<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
write_compound_list(writer, compounds(data, "transformsInto"), write_weighted)?;
as_u16(number(data, "conditionJson")).write(writer)?;
as_u32(number(data, "minPassingNeighbors")).write(writer)
}
fn write_weighted_temperature<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
VarInt(as_i32(number(data, "temperature"))).write(writer)?;
as_i32(number(data, "weight")).write(writer)
}
fn write_overworld_gen_rules<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
for key in [
"hillsTransformations",
"mutateTransformations",
"riverTransformations",
"shoreTransformations",
] {
write_compound_list(writer, compounds(data, key), write_weighted)?;
}
for key in ["preHillsEdge", "postShoreEdge"] {
write_compound_list(writer, compounds(data, key), write_conditional)?;
}
write_compound_list(
writer,
compounds(data, "climate"),
write_weighted_temperature,
)
}
fn write_multinoise_gen_rules<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
for key in ["temperature", "humidity", "altitude", "weirdness", "weight"] {
decimal(data, key).write(writer)?;
}
Ok(())
}
fn write_replacement<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
as_i16(number(data, "biome")).write(writer)?;
as_i16(number(data, "dimension")).write(writer)?;
let targets = numbers(data, "targetBiomes");
write_len(writer, targets.len())?;
for target in targets {
as_i16(target).write(writer)?;
}
decimal(data, "amount").write(writer)?;
decimal(data, "noiseFrequencyScale").write(writer)?;
as_i32(number(data, "replacementIndex")).write(writer)
}
fn write_mesa_surface<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
as_i32(number(data, "clayMaterial")).write(writer)?;
as_i32(number(data, "hardClayMaterial")).write(writer)?;
(number(data, "brycePillars") != 0).write(writer)?;
(number(data, "hasForest") != 0).write(writer)
}
fn write_optional_block<W: Write>(
writer: &mut W,
data: &NbtCompound,
key: &str,
) -> Result<(), Error> {
if data.has(key) {
true.write(writer)?;
as_i32(number(data, key)).write(writer)
} else {
false.write(writer)
}
}
fn write_int_list<W: Write>(writer: &mut W, values: &[i64]) -> Result<(), Error> {
write_len(writer, values.len())?;
for value in values {
as_i32(*value).write(writer)?;
}
Ok(())
}
fn write_capped_surface<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
write_int_list(writer, &numbers(data, "floorBlocks"))?;
write_int_list(writer, &numbers(data, "ceilingBlocks"))?;
for key in ["seaBlock", "foundationBlock", "beachBlock"] {
write_optional_block(writer, data, key)?;
}
Ok(())
}
fn write_noise_block<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
text(data, "noise").write(writer)?;
decimal(data, "threshold").write(writer)?;
let range = data.get_compound("range");
range
.map_or(0.0, |range| decimal(range, "min"))
.write(writer)?;
range
.map_or(0.0, |range| decimal(range, "max"))
.write(writer)?;
as_i32(number(data, "block")).write(writer)
}
fn write_noise_gradient_surface<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
write_int_list(writer, &numbers(data, "nonReplaceableBlocks"))?;
write_compound_list(writer, compounds(data, "gradientBlocks"), write_noise_block)?;
let empty = NbtCompound::new();
let noise = data.get_compound("noise").unwrap_or(&empty);
text(noise, "name").write(writer)?;
as_i32(number(noise, "firstOctave")).write(writer)?;
let amplitudes = decimals(noise, "amplitudes");
write_len(writer, amplitudes.len())?;
for amplitude in amplitudes {
amplitude.write(writer)?;
}
Ok(())
}
fn write_surface_builder<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
write_optional_compound(writer, data, "surfaceMaterials", write_surface_material)?;
for key in [
"hasDefaultOverworldSurface",
"hasSwampSurface",
"hasFrozenOceanSurface",
"hasTheEndSurface",
] {
(number(data, key) != 0).write(writer)?;
}
write_optional_compound(writer, data, "mesaSurface", write_mesa_surface)?;
write_optional_compound(writer, data, "cappedSurface", write_capped_surface)?;
write_optional_compound(
writer,
data,
"noiseGradientSurface",
write_noise_gradient_surface,
)
}
fn write_chunk_gen_data<W: Write>(writer: &mut W, data: &NbtCompound) -> Result<(), Error> {
write_optional_compound(writer, data, "climate", write_climate)?;
write_optional_compound(writer, data, "consolidatedFeatures", |writer, features| {
write_compound_list(writer, compounds(features, "features"), write_feature)
})?;
write_optional_compound(writer, data, "mountainParams", write_mountain_params)?;
write_optional_compound(
writer,
data,
"surfaceMaterialAdjustment",
|writer, adjustment| {
write_compound_list(
writer,
compounds(adjustment, "biomeElements"),
write_element,
)
},
)?;
write_optional_compound(writer, data, "overworldGenRules", write_overworld_gen_rules)?;
write_optional_compound(
writer,
data,
"multinoiseGenRules",
write_multinoise_gen_rules,
)?;
write_optional_compound(writer, data, "legacyWorldGenRules", |writer, legacy| {
write_compound_list(
writer,
compounds(legacy, "legacyPreHills"),
write_conditional,
)
})?;
write_optional_compound(writer, data, "replacementBiomes", |writer, replacements| {
write_compound_list(
writer,
compounds(replacements, "replacements"),
write_replacement,
)
})?;
if data.has("villageType") {
true.write(writer)?;
as_u8(number(data, "villageType")).write(writer)?;
} else {
false.write(writer)?;
}
write_optional_compound(writer, data, "surfaceBuilderData", write_surface_builder)?;
write_optional_compound(writer, data, "subSurfaceBuilderData", write_surface_builder)
}
fn write_biome<W: Write>(writer: &mut W, entry: &NbtCompound) -> Result<(), Error> {
as_u16(number(entry, "index")).write(writer)?;
let data = entry.get_compound("data").ok_or_else(|| {
Error::new(
ErrorKind::InvalidData,
"biome definition has no data compound",
)
})?;
as_u16(number(data, "id")).write(writer)?;
for key in ["temperature", "downfall", "foliageSnow", "depth", "scale"] {
decimal(data, key).write(writer)?;
}
as_i32(number(data, "mapWaterColorARGB")).write(writer)?;
(number(data, "rain") != 0).write(writer)?;
write_optional_compound(writer, data, "tags", |writer, tags| {
let values = numbers(tags, "tags");
write_len(writer, values.len())?;
for value in values {
as_u16(value).write(writer)?;
}
Ok(())
})?;
write_optional_compound(writer, data, "chunkGenData", write_chunk_gen_data)
}
fn encode_biome_definitions(document: &[u8]) -> Result<(Vec<u8>, usize), Error> {
let root = read_gzip_compound_tag(Cursor::new(document))
.map_err(|error| Error::new(ErrorKind::InvalidData, error))?;
let biomes = compounds(&root, "biomeData");
if biomes.is_empty() {
return Err(Error::new(
ErrorKind::InvalidData,
"biome definitions contain no biomeData",
));
}
let mut payload = Vec::new();
write_len(&mut payload, biomes.len())?;
let biome_count = biomes.len();
for biome in biomes {
write_biome(&mut payload, biome)?;
}
let strings = root
.get_list("biomeStringList")
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "missing biomeStringList"))?;
write_len(&mut payload, strings.len())?;
for string in strings {
string
.extract_string()
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "invalid biome string"))?
.write(&mut payload)?;
}
Ok((payload, biome_count))
}
pub fn build() -> TokenStream {
let document = fs::read("../../assets/bedrock/biome_definitions.nbt")
.expect("Failed to read bedrock/biome_definitions.nbt");
let (payload, biome_count) = encode_biome_definitions(&document)
.expect("Failed to encode bedrock/biome_definitions.nbt");
let payload_len = payload.len();
let payload = Literal::byte_string(&payload);
quote! {
pub const BIOME_DEFINITIONS: &[u8; #payload_len] = #payload;
pub const BIOME_COUNT: usize = #biome_count;
}
}

View File

@@ -27,6 +27,7 @@ use std::{
mod advancement;
mod attributes;
mod bedrock_biome;
mod bedrock_creative;
mod biome;
mod bitsets;
@@ -97,6 +98,7 @@ pub fn main() {
let mut build_functions: Vec<(BuilderFn, &str)> = vec![
(advancement::build, "advancement.rs"),
(bedrock_biome::build, "bedrock_biome.rs"),
(bedrock_creative::build, "bedrock_creative.rs"),
(packet::build, "packet.rs"),
(screen::build, "screen.rs"),