fix: vanilla level.dat loading

This commit is contained in:
Alexander Medvedev 2026-07-13 12:53:33 +02:00
parent e480f32265
commit b1b2a31c49
82 changed files with 2083 additions and 599 deletions

View File

@ -12,7 +12,9 @@
and customizable experience. It prioritizes performance and player enjoyment while adhering to the core mechanics of the game.
<div align="center">
![chunk loading](/assets/pumpkin_chunk_loading.webp)
<video src="/assets/pumpkin_chunk_loading.webm" autoplay loop muted playsinline>
Your browser does not support the video tag.
</video>
</div>

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 MiB

View File

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
/// Configuration for chunk storage format.
///
/// Supports multiple chunk formats, currently `Anvil` and `Linear`.
#[derive(Deserialize, Default, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone)]
#[serde(tag = "type")]
pub enum ChunkConfig {
/// Standard Anvil chunk storage.
@ -16,10 +16,15 @@ pub enum ChunkConfig {
Linear,
/// Pumpkin's own optimized world format.
#[serde(rename = "pump")]
#[default]
Pump,
}
impl Default for ChunkConfig {
fn default() -> Self {
Self::Anvil(AnvilChunkConfig::default())
}
}
/// Configuration for Anvil chunk storage.
#[derive(Deserialize, Serialize, Default, Clone)]
#[serde(default)]

View File

@ -5,7 +5,6 @@ use pumpkin_util::{Difficulty, GameMode, PermissionLvl, random};
use recipe::RecipeConfig;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::{fs, num::NonZeroU8, path::Path};
use tracing::{debug, warn};
@ -20,7 +19,9 @@ pub mod resource_pack;
pub use chat::ChatConfig;
pub use commands::CommandsConfig;
pub use networking::auth::AuthenticationConfig;
pub use networking::bedrock::BedrockConfig;
pub use networking::compression::CompressionConfig;
pub use networking::java::JavaConfig;
pub use networking::lan_broadcast::LANBroadcastConfig;
pub use networking::rcon::RCONConfig;
pub use plugins::PluginsConfig;
@ -64,6 +65,48 @@ impl LoadConfiguration for PumpkinConfig {
fn validate(&self) {
self.basic.validate();
self.advanced.validate();
let min_vd = NonZeroU8::new(2).unwrap();
let max_vd = NonZeroU8::new(64).unwrap();
// Validate Java
assert!(
self.advanced.networking.java.view_distance >= min_vd,
"Java View distance must be at least 2"
);
assert!(
self.advanced.networking.java.view_distance <= max_vd,
"Java View distance must be less than 64"
);
if self.advanced.networking.java.online_mode {
assert!(
self.advanced.networking.java.encryption,
"When online mode is enabled, encryption must be enabled"
);
}
// Validate Bedrock
assert!(
self.advanced.networking.bedrock.view_distance >= min_vd,
"Bedrock View distance must be at least 2"
);
assert!(
self.advanced.networking.bedrock.view_distance <= max_vd,
"Bedrock View distance must be less than 64"
);
if self.advanced.networking.bedrock.online_mode {
assert!(
self.advanced.networking.bedrock.encryption,
"When online mode is enabled, bedrock_encryption must be enabled"
);
}
if self.basic.allow_chat_reports {
assert!(
self.advanced.networking.java.online_mode,
"When allow_chat_reports is enabled, java.online_mode must be enabled"
);
}
}
}
@ -110,24 +153,8 @@ pub struct AdvancedConfiguration {
#[derive(Serialize, Deserialize)]
#[serde(default)]
pub struct BasicConfiguration {
/// Whether Java Edition Clients are Accepted.
pub java_edition: bool,
/// The address and port to which the Java Edition server will bind.
pub java_edition_address: SocketAddr,
/// Whether Bedrock Edition Clients are Accepted.
pub bedrock_edition: bool,
/// Whether Bedrock Edition Clients are Accepted.
pub bedrock_edition_address: SocketAddr,
/// Whether packet encryption is enabled for Bedrock Edition.
pub bedrock_encryption: bool,
/// The seed for the world generation.
pub seed: Seed,
/// The maximum number of players allowed on the server. Specifying `0` disables the limit.
pub max_players: u32,
/// The maximum view distance for players.
pub view_distance: NonZeroU8,
/// The maximum simulated view distance.
pub simulation_distance: NonZeroU8,
/// The default game difficulty.
pub default_difficulty: Difficulty,
/// The op level assigned by the /op command.
@ -138,12 +165,6 @@ pub struct BasicConfiguration {
pub allow_end: bool,
/// Whether the server is in hardcore mode.
pub hardcore: bool,
/// Whether online mode is enabled. Requires valid Minecraft accounts.
pub online_mode: bool,
/// Whether packet encryption is enabled. Required when online mode is enabled.
pub encryption: bool,
/// Message of the Day; the server's description displayed on the status screen.
pub motd: String,
/// The server's ticks per second.
pub tps: f32,
/// The default gamemode for players.
@ -169,23 +190,12 @@ pub struct BasicConfiguration {
impl Default for BasicConfiguration {
fn default() -> Self {
Self {
java_edition: true,
java_edition_address: "0.0.0.0:25565".parse().unwrap(),
bedrock_edition: true,
bedrock_edition_address: "0.0.0.0:19132".parse().unwrap(),
bedrock_encryption: true,
seed: Seed(random::get_seed()),
max_players: 1000,
view_distance: NonZeroU8::new(16).unwrap(),
simulation_distance: NonZeroU8::new(10).unwrap(),
default_difficulty: Difficulty::Normal,
op_permission_level: PermissionLvl::Four,
allow_nether: true,
allow_end: true,
hardcore: false,
online_mode: true,
encryption: true,
motd: "A blazingly fast Pumpkin server!".to_string(),
tps: 20.0,
default_gamemode: GameMode::Survival,
force_gamemode: false,
@ -207,35 +217,7 @@ impl BasicConfiguration {
PathBuf::from(&self.default_level_name)
}
pub fn validate(&self) {
let min = NonZeroU8::new(2).unwrap();
let max = NonZeroU8::new(64).unwrap();
assert!(
self.view_distance.ge(&min),
"View distance must be at least 2"
);
assert!(
self.view_distance.le(&max),
"View distance must be less than 64"
);
if self.online_mode {
assert!(
self.encryption,
"When online mode is enabled, encryption must be enabled"
);
assert!(
self.bedrock_encryption,
"When online mode is enabled, bedrock_encryption must be enabled"
);
}
if self.allow_chat_reports {
assert!(
self.online_mode,
"When allow_chat_reports is enabled, online_mode must be enabled"
);
}
}
pub const fn validate(&self) {}
}
impl AdvancedConfiguration {

View File

@ -0,0 +1,72 @@
use crate::CompressionConfig;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::num::NonZeroU8;
/// Configuration for Bedrock authentication.
#[derive(Deserialize, Serialize, Clone)]
#[serde(default)]
pub struct BedrockAuthenticationConfig {
/// Whether Xbox Live authentication is enabled/enforced.
pub enabled: bool,
/// Optional custom authentication/discovery URL.
pub url: Option<String>,
/// Connection timeout in milliseconds.
pub connect_timeout: u32,
/// Read timeout in milliseconds.
pub read_timeout: u32,
}
impl Default for BedrockAuthenticationConfig {
fn default() -> Self {
Self {
enabled: true,
url: None,
connect_timeout: 5000,
read_timeout: 5000,
}
}
}
/// Configuration for Bedrock Edition client connections.
#[derive(Deserialize, Serialize, Clone)]
#[serde(default)]
pub struct BedrockConfig {
/// Whether Bedrock Edition Clients are Accepted.
pub enabled: bool,
/// Whether Bedrock Edition Clients are Accepted.
pub address: SocketAddr,
/// Whether packet encryption is enabled for Bedrock Edition.
pub encryption: bool,
/// Whether online mode is enabled.
pub online_mode: bool,
/// The maximum number of players allowed on the server. Specifying `0` disables the limit.
pub max_players: u32,
/// The maximum view distance for players.
pub view_distance: NonZeroU8,
/// The maximum simulated view distance.
pub simulation_distance: NonZeroU8,
/// Bedrock Edition packet compression settings.
pub compression: CompressionConfig,
/// Message of the Day; the server's description displayed on the status screen.
pub motd: String,
/// Bedrock Edition authentication settings.
pub authentication: BedrockAuthenticationConfig,
}
impl Default for BedrockConfig {
fn default() -> Self {
Self {
enabled: true,
address: "0.0.0.0:19132".parse().unwrap(),
encryption: true,
online_mode: true,
max_players: 1000,
view_distance: NonZeroU8::new(16).unwrap(),
simulation_distance: NonZeroU8::new(10).unwrap(),
compression: CompressionConfig::default(),
motd: "A blazingly fast Pumpkin server!".to_string(),
authentication: BedrockAuthenticationConfig::default(),
}
}
}

View File

@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
/// Configuration for packet compression.
///
/// Controls whether network packet compression is enabled and the compression parameters.
#[derive(Deserialize, Serialize)]
#[derive(Deserialize, Serialize, Clone)]
#[serde(default)]
pub struct CompressionConfig {
/// Whether compression is enabled.

View File

@ -0,0 +1,47 @@
use crate::{AuthenticationConfig, CompressionConfig};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::num::NonZeroU8;
/// Configuration for Java Edition client connections.
#[derive(Deserialize, Serialize, Clone)]
#[serde(default)]
pub struct JavaConfig {
/// Whether Java Edition Clients are Accepted.
pub enabled: bool,
/// The address and port to which the Java Edition server will bind.
pub address: SocketAddr,
/// Whether packet encryption is enabled. Required when online mode is enabled.
pub encryption: bool,
/// Whether online mode is enabled. Requires valid Minecraft accounts.
pub online_mode: bool,
/// The maximum number of players allowed on the server. Specifying `0` disables the limit.
pub max_players: u32,
/// The maximum view distance for players.
pub view_distance: NonZeroU8,
/// The maximum simulated view distance.
pub simulation_distance: NonZeroU8,
/// Java Edition packet compression settings.
pub compression: CompressionConfig,
/// Message of the Day; the server's description displayed on the status screen.
pub motd: String,
/// Authentication settings for client connections.
pub authentication: AuthenticationConfig,
}
impl Default for JavaConfig {
fn default() -> Self {
Self {
enabled: true,
address: "0.0.0.0:25565".parse().unwrap(),
encryption: true,
online_mode: true,
max_players: 1000,
view_distance: NonZeroU8::new(16).unwrap(),
simulation_distance: NonZeroU8::new(10).unwrap(),
compression: CompressionConfig::default(),
motd: "A blazingly fast Pumpkin server!".to_string(),
authentication: AuthenticationConfig::default(),
}
}
}

View File

@ -1,13 +1,16 @@
use auth::AuthenticationConfig;
use proxy::ProxyConfig;
use query::QueryConfig;
use rcon::RCONConfig;
use serde::{Deserialize, Serialize};
use crate::{CompressionConfig, LANBroadcastConfig};
use crate::LANBroadcastConfig;
use bedrock::BedrockConfig;
use java::JavaConfig;
pub mod auth;
pub mod bedrock;
pub mod compression;
pub mod java;
pub mod lan_broadcast;
pub mod proxy;
pub mod query;
@ -18,19 +21,18 @@ pub mod rcon;
/// Covers authentication, query, RCON, proxying, packet compression,
/// and LAN broadcast behaviour.
#[derive(Deserialize, Serialize, Default)]
#[serde(default)]
pub struct NetworkingConfig {
/// Authentication settings for client connections.
pub authentication: AuthenticationConfig,
/// Query protocol settings for server status requests.
pub query: QueryConfig,
/// RCON (remote console) configuration.
pub rcon: RCONConfig,
/// Proxy-related networking settings.
pub proxy: ProxyConfig,
/// Java Edition packet compression settings.
pub java_compression: CompressionConfig,
/// Bedrock Edition packet compression settings.
pub bedrock_compression: CompressionConfig,
/// LAN broadcast settings.
pub lan_broadcast: LANBroadcastConfig,
/// Java Edition configuration settings.
pub java: JavaConfig,
/// Bedrock Edition configuration settings.
pub bedrock: BedrockConfig,
}

View File

@ -26,7 +26,7 @@ impl PacketWrite for CUnconnectedPong {
pub struct ServerInfo {
/// (BE or MCEE for Education Edition)
pub edition: &'static str,
pub motd_line_1: &'static str,
pub motd_line_1: String,
pub protocol_version: u32,
pub version_name: &'static str,
pub player_count: i32,

View File

@ -85,6 +85,7 @@ impl PacketWrite for EntityMetadata {
}
}
#[derive(Clone)]
pub enum MetadataValue {
Byte(i8),
Short(i16),

View File

@ -3,7 +3,7 @@ use std::num::NonZeroI32;
use pumpkin_data::item::{BedrockItem, JavaToBedrockItemMapping};
use pumpkin_data::item_stack::ItemStack;
use pumpkin_nbt::Nbt;
use pumpkin_nbt::{Nbt, deserializer::NbtReadHelperBedrock};
use crate::{
codec::{var_int::VarInt, var_uint::VarUInt},
@ -54,12 +54,45 @@ impl PacketRead for NetworkItemDescriptor {
let mut user_data = vec![0u8; user_data_len as usize];
buf.read_exact(&mut user_data)?;
let mut nbt_data = Nbt::default();
let mut place_on_blocks = Vec::new();
let mut destroy_blocks = Vec::new();
let mut shield_blocking_tick = 0;
if !user_data.is_empty() {
let mut cursor = std::io::Cursor::new(user_data);
let nbt_version = i16::read(&mut cursor)?;
if nbt_version == -1 {
let _version = i8::read(&mut cursor)?;
let mut nbt_reader = NbtReadHelperBedrock::new(&mut cursor);
nbt_data = Nbt::read(&mut nbt_reader)
.map_err(|e| Error::new(std::io::ErrorKind::InvalidData, e))?;
}
let place_on_len = VarUInt::read(&mut cursor)?.0;
for _ in 0..place_on_len {
place_on_blocks.push(String::read(&mut cursor)?);
}
let destroy_len = VarUInt::read(&mut cursor)?.0;
for _ in 0..destroy_len {
destroy_blocks.push(String::read(&mut cursor)?);
}
if id.0 == (BedrockItem::SHIELD.id as i32) {
shield_blocking_tick = i64::read(&mut cursor)?;
}
}
Ok(Self {
id,
stack_size,
aux_value,
block_runtime_id,
..Default::default()
nbt_data,
place_on_blocks,
destroy_blocks,
shield_blocking_tick,
})
}
}
@ -217,13 +250,46 @@ impl PacketRead for ItemStackWrapper {
let mut user_data = vec![0u8; user_data_len as usize];
buf.read_exact(&mut user_data)?;
let mut nbt_data = Nbt::default();
let mut place_on_blocks = Vec::new();
let mut destroy_blocks = Vec::new();
let mut shield_blocking_tick = 0;
if !user_data.is_empty() {
let mut cursor = std::io::Cursor::new(user_data);
let nbt_version = i16::read(&mut cursor)?;
if nbt_version == -1 {
let _version = i8::read(&mut cursor)?;
let mut nbt_reader = NbtReadHelperBedrock::new(&mut cursor);
nbt_data = Nbt::read(&mut nbt_reader)
.map_err(|e| Error::new(std::io::ErrorKind::InvalidData, e))?;
}
let place_on_len = VarUInt::read(&mut cursor)?.0;
for _ in 0..place_on_len {
place_on_blocks.push(String::read(&mut cursor)?);
}
let destroy_len = VarUInt::read(&mut cursor)?.0;
for _ in 0..destroy_len {
destroy_blocks.push(String::read(&mut cursor)?);
}
if id == (BedrockItem::SHIELD.id) {
shield_blocking_tick = i64::read(&mut cursor)?;
}
}
Ok(Self {
id,
stack_size,
aux_value,
block_runtime_id,
nbt_data,
place_on_blocks,
destroy_blocks,
shield_blocking_tick,
net_id,
..Default::default()
})
}
}

View File

@ -58,7 +58,7 @@ impl ClientPacket for CSetEntityMetadata {
pub struct Metadata<T> {
pub index: TrackedId,
pub r#type: MetaDataType,
value: T,
pub value: T,
}
impl<T> Metadata<T> {

View File

@ -227,8 +227,24 @@ pub const OIDC_AUDIENCE: &str = "api://auth-minecraft-services/multiplayer";
pub const OIDC_DISCOVERY_URL: &str =
"https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/1.0.0.0";
pub fn fetch_oidc_jwks() -> Result<(String, Jwks), AuthError> {
let discovery: Value = ureq::get(OIDC_DISCOVERY_URL)
pub fn fetch_oidc_jwks(
discovery_url: Option<&str>,
connect_timeout_ms: u32,
read_timeout_ms: u32,
) -> Result<(String, Jwks), AuthError> {
let url = discovery_url.unwrap_or(OIDC_DISCOVERY_URL);
let config = ureq::Agent::config_builder()
.timeout_connect(Some(std::time::Duration::from_millis(
connect_timeout_ms as u64,
)))
.timeout_recv_response(Some(std::time::Duration::from_millis(
read_timeout_ms as u64,
)))
.build();
let agent: ureq::Agent = config.into();
let discovery: Value = agent
.get(url)
.call()
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
.body_mut()
@ -248,7 +264,8 @@ pub fn fetch_oidc_jwks() -> Result<(String, Jwks), AuthError> {
.ok_or_else(|| AuthError::PublicKeyBuild("Discovery missing serviceUri".into()))?;
let openid_config_url = format!("{service_uri}/.well-known/openid-configuration");
let openid_config: Value = ureq::get(&openid_config_url)
let openid_config: Value = agent
.get(&openid_config_url)
.call()
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
.body_mut()
@ -269,7 +286,8 @@ pub fn fetch_oidc_jwks() -> Result<(String, Jwks), AuthError> {
.ok_or_else(|| AuthError::PublicKeyBuild("OpenID config missing issuer".into()))?
.to_string();
let jwks: Jwks = ureq::get(jwks_uri)
let jwks: Jwks = agent
.get(jwks_uri)
.call()
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
.body_mut()

View File

@ -5,7 +5,7 @@ use pumpkin_data::dimension::Dimension;
use pumpkin_util::world_seed::Seed;
use pumpkin_world::ProtoChunk;
use pumpkin_world::chunk_system::{Cache, Chunk, StagedChunkEnum, generate_single_chunk};
use pumpkin_world::generation::generator::VanillaGenerator;
use pumpkin_world::generation::generator::WorldGenerator;
use pumpkin_world::generation::get_world_gen;
use pumpkin_world::world::WorldPortalExt;
use std::hint::black_box;
@ -13,7 +13,6 @@ use std::sync::Arc;
const SEED: Seed = Seed(42);
// Stub portal — allows all block placements, skips mob spawning.
struct BlockRegistry;
impl WorldPortalExt for BlockRegistry {
fn can_place_at(
@ -54,11 +53,13 @@ impl WorldPortalExt for BlockRegistry {
}
}
/// Build a cache containing chunk 0, 0 at the center so that features
/// and lighting stages get a 3×3 cache.
fn make_world_gen() -> Box<WorldGenerator> {
get_world_gen(SEED, Dimension::OVERWORLD, false, Vec::new(), String::new())
}
fn setup_cache(
target_stage: StagedChunkEnum,
world_gen: &VanillaGenerator,
world_gen: &WorldGenerator,
block_registry: &dyn WorldPortalExt,
) -> Cache {
let radius = target_stage.get_direct_radius();
@ -97,10 +98,9 @@ fn setup_cache(
cache
}
/// Full pipeline
fn bench_full_chunk_generation(c: &mut Criterion) {
let dimension = Dimension::OVERWORLD;
let world_gen = get_world_gen(SEED, dimension.clone());
let world_gen = make_world_gen();
let block_registry = Arc::new(BlockRegistry);
c.bench_function("full_chunk_generation", |b| {
@ -118,9 +118,8 @@ fn bench_full_chunk_generation(c: &mut Criterion) {
});
}
/// Per-phase benchmarks
fn bench_biomes_generation(c: &mut Criterion) {
let world_gen = get_world_gen(SEED, Dimension::OVERWORLD);
let world_gen = make_world_gen();
let block_registry = Arc::new(BlockRegistry);
c.bench_function("biomes_generation", |b| {
@ -141,7 +140,7 @@ fn bench_biomes_generation(c: &mut Criterion) {
}
fn bench_structure_starts_generation(c: &mut Criterion) {
let world_gen = get_world_gen(SEED, Dimension::OVERWORLD);
let world_gen = make_world_gen();
let block_registry = Arc::new(BlockRegistry);
c.bench_function("structure_starts_generation", |b| {
@ -168,7 +167,7 @@ fn bench_structure_starts_generation(c: &mut Criterion) {
}
fn bench_structure_references_generation(c: &mut Criterion) {
let world_gen = get_world_gen(SEED, Dimension::OVERWORLD);
let world_gen = make_world_gen();
let block_registry = Arc::new(BlockRegistry);
c.bench_function("structure_references_generation", |b| {
@ -195,7 +194,7 @@ fn bench_structure_references_generation(c: &mut Criterion) {
}
fn bench_noise_generation(c: &mut Criterion) {
let world_gen = get_world_gen(SEED, Dimension::OVERWORLD);
let world_gen = make_world_gen();
let block_registry = Arc::new(BlockRegistry);
c.bench_function("noise_generation", |b| {
@ -216,7 +215,7 @@ fn bench_noise_generation(c: &mut Criterion) {
}
fn bench_surface_generation(c: &mut Criterion) {
let world_gen = get_world_gen(SEED, Dimension::OVERWORLD);
let world_gen = make_world_gen();
let block_registry = Arc::new(BlockRegistry);
c.bench_function("surface_generation", |b| {
@ -243,7 +242,7 @@ fn bench_surface_generation(c: &mut Criterion) {
}
fn bench_carvers_generation(c: &mut Criterion) {
let world_gen = get_world_gen(SEED, Dimension::OVERWORLD);
let world_gen = make_world_gen();
let block_registry = Arc::new(BlockRegistry);
c.bench_function("carvers_generation", |b| {
@ -270,7 +269,7 @@ fn bench_carvers_generation(c: &mut Criterion) {
}
fn bench_features_generation(c: &mut Criterion) {
let world_gen = get_world_gen(SEED, Dimension::OVERWORLD);
let world_gen = make_world_gen();
let block_registry = Arc::new(BlockRegistry);
c.bench_function("features_generation", |b| {
@ -297,7 +296,7 @@ fn bench_features_generation(c: &mut Criterion) {
}
fn bench_lighting_generation(c: &mut Criterion) {
let world_gen = get_world_gen(SEED, Dimension::OVERWORLD);
let world_gen = make_world_gen();
let block_registry = Arc::new(BlockRegistry);
c.bench_function("lighting_generation", |b| {

View File

@ -63,7 +63,13 @@ fn bench_concurrent_chunk_generation(c: &mut Criterion) {
.build()
.expect("Failed to build rayon thread pool");
let world_gen = Arc::new(get_world_gen(SEED, Dimension::OVERWORLD));
let world_gen = Arc::new(get_world_gen(
SEED,
Dimension::OVERWORLD,
false,
Vec::new(),
String::new(),
));
let block_registry = Arc::new(BlockRegistry);
let bench_name = format!(

View File

@ -79,7 +79,7 @@ mod test {
#[test]
fn wide_area_surface() {
use crate::generation::generator::{GeneratorInit, VanillaGenerator};
use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator};
use crate::generation::noise::router::multi_noise_sampler::{
MultiNoiseSampler, MultiNoiseSamplerBuilderOptions,
};
@ -96,13 +96,19 @@ mod test {
read_data_from_file!("../../assets/biome_no_blend_no_beard_0.json");
let seed = 0;
let generator = VanillaGenerator::new(Seed(seed as u64), Dimension::OVERWORLD);
let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(
Seed(seed as u64),
Dimension::OVERWORLD,
)));
let WorldGenerator::Noise(generator) = &world_gen else {
unreachable!()
};
for data in expected_data {
let chunk_x = data.x;
let chunk_z = data.z;
let mut chunk = ProtoChunk::new(chunk_x, chunk_z, &generator);
let mut chunk = ProtoChunk::new(chunk_x, chunk_z, &world_gen);
// Create MultiNoiseSampler for populate_biomes

View File

@ -45,7 +45,7 @@ mod test {
#[test]
fn sample_value() {
use crate::generation::generator::{GeneratorInit, VanillaGenerator};
use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator};
use crate::generation::noise::router::multi_noise_sampler::{
MultiNoiseSampler, MultiNoiseSamplerBuilderOptions,
};
@ -59,9 +59,16 @@ mod test {
let chunk_x = 0;
let chunk_z = 0;
let generator = VanillaGenerator::new(Seed(seed as u64), Dimension::OVERWORLD);
let generator = Box::new(VanillaGenerator::new(
Seed(seed as u64),
Dimension::OVERWORLD,
));
let world_gen = WorldGenerator::Noise(generator);
let WorldGenerator::Noise(generator) = &world_gen else {
unreachable!()
};
let _chunk = ProtoChunk::new(chunk_x, chunk_z, &generator);
let _chunk = ProtoChunk::new(chunk_x, chunk_z, &world_gen);
let start_x = chunk_pos::start_block_x(chunk_x);
let start_z = chunk_pos::start_block_z(chunk_z);

View File

@ -922,7 +922,7 @@ mod tests {
let _ = env_logger::try_init();
let generator = get_world_gen(Seed(0), Dimension::Overworld);
let generator = get_world_gen(Seed(0), Dimension::Overworld, false, Vec::new(), String::new());
let temp_dir = TempDir::new().unwrap();
let level_folder = LevelFolder {
@ -1198,7 +1198,7 @@ mod tests {
let _ = env_logger::try_init();
let generator = get_world_gen(Seed(0), Dimension::Overworld);
let generator = get_world_gen(Seed(0), Dimension::Overworld, false, Vec::new(), String::new());
let temp_dir = TempDir::new().unwrap();
let level_folder = LevelFolder {

View File

@ -78,8 +78,15 @@ impl ChunkData {
chunk_data: &[u8],
position: Vector2<i32>,
) -> Result<Self, ChunkParsingError> {
let chunk_data =
let is_named = chunk_data.len() >= 3
&& chunk_data[0] == 0x0a
&& chunk_data[1] == 0x00
&& chunk_data[2] == 0x00;
let chunk_data = if is_named {
pumpkin_nbt::from_bytes::<ChunkNbt>(std::io::Cursor::new(chunk_data))
} else {
pumpkin_nbt::from_bytes_unnamed::<ChunkNbt>(std::io::Cursor::new(chunk_data))
}
.map_err(|e| ChunkParsingError::ErrorDeserializingChunk(e.to_string()))?;
if chunk_data.x_pos != position.x || chunk_data.z_pos != position.y {
@ -222,7 +229,7 @@ impl ChunkData {
};
let mut result = Vec::new();
pumpkin_nbt::to_bytes_unnamed(&nbt_ref, &mut result)
pumpkin_nbt::to_bytes(&nbt_ref, &mut result)
.map_err(ChunkSerializingError::ErrorSerializingChunk)?;
Ok(result.into())
@ -272,8 +279,15 @@ impl ChunkEntityData {
chunk_data: &[u8],
position: Vector2<i32>,
) -> Result<Self, ChunkParsingError> {
let chunk_entity_data =
let is_named = chunk_data.len() >= 3
&& chunk_data[0] == 0x0a
&& chunk_data[1] == 0x00
&& chunk_data[2] == 0x00;
let chunk_entity_data = if is_named {
pumpkin_nbt::from_bytes::<EntityNbt>(std::io::Cursor::new(chunk_data))
} else {
pumpkin_nbt::from_bytes_unnamed::<EntityNbt>(std::io::Cursor::new(chunk_data))
}
.map_err(|e| ChunkParsingError::ErrorDeserializingChunk(e.to_string()))?;
if chunk_entity_data.position[0] != position.x
@ -304,7 +318,7 @@ impl ChunkEntityData {
};
let mut result = Vec::new();
pumpkin_nbt::to_bytes_unnamed(&nbt, &mut result)
pumpkin_nbt::to_bytes(&nbt, &mut result)
.map_err(ChunkSerializingError::ErrorSerializingChunk)?;
Ok(result.into())
}
@ -476,12 +490,13 @@ struct ChunkNbt {
status: ChunkStatus,
#[serde(rename = "sections")]
sections: Vec<ChunkSectionNBT>,
#[serde(default)]
heightmaps: ChunkHeightmaps,
#[serde(rename = "block_ticks")]
#[serde(rename = "block_ticks", default)]
block_ticks: Vec<ScheduledTick<&'static Block>>,
#[serde(rename = "fluid_ticks")]
#[serde(rename = "fluid_ticks", default)]
fluid_ticks: Vec<ScheduledTick<&'static Fluid>>,
#[serde(rename = "block_entities")]
#[serde(rename = "block_entities", default)]
block_entities: Vec<NbtCompound>,
#[serde(rename = "isLightOn", default)]
light_correct: bool,

View File

@ -1,7 +1,7 @@
use pumpkin_data::dimension::Dimension;
use crate::ProtoChunk;
use crate::generation::generator::VanillaGenerator;
use crate::generation::generator::WorldGenerator;
use crate::world::WorldPortalExt;
use pumpkin_config::lighting::LightingEngineConfig;
@ -10,7 +10,7 @@ use super::{Cache, Chunk, StagedChunkEnum};
pub fn generate_single_chunk(
_dimension: &Dimension,
_biome_mixer_seed: i64,
generator: &VanillaGenerator,
generator: &WorldGenerator,
block_registry: &dyn WorldPortalExt,
chunk_x: i32,
chunk_z: i32,
@ -117,13 +117,13 @@ mod tests {
let dimension = Dimension::OVERWORLD;
let seed = Seed(42);
let block_registry = Arc::new(BlockRegistry);
let world_gen = get_world_gen(seed, dimension.clone());
let biome_mixer_seed = hash_seed(world_gen.random_config.seed);
let world_gen = get_world_gen(seed, dimension.clone(), false, Vec::new(), String::new());
let biome_mixer_seed = hash_seed(world_gen.seed());
let _ = generate_single_chunk(
&dimension,
biome_mixer_seed,
&world_gen,
&*world_gen,
block_registry.as_ref(),
0,
0,
@ -136,13 +136,13 @@ mod tests {
let dimension = Dimension::OVERWORLD;
let seed = Seed(1_782_124_772_053_846_960);
let block_registry = Arc::new(BlockRegistry);
let world_gen = get_world_gen(seed, dimension.clone());
let biome_mixer_seed = hash_seed(world_gen.random_config.seed);
let world_gen = get_world_gen(seed, dimension.clone(), false, Vec::new(), String::new());
let biome_mixer_seed = hash_seed(world_gen.seed());
let chunk = generate_single_chunk(
&dimension,
biome_mixer_seed,
&world_gen,
&*world_gen,
block_registry.as_ref(),
31,
-12,

View File

@ -350,7 +350,7 @@ impl Cache {
pub fn advance(
&mut self,
stage: StagedChunkEnum,
generator: &generator::VanillaGenerator,
generator: &generator::WorldGenerator,
block_registry: &dyn WorldPortalExt,
lighting_config: &LightingEngineConfig,
) {
@ -362,35 +362,74 @@ impl Cache {
}
match stage {
StagedChunkEnum::Empty => panic!("empty stage"),
StagedChunkEnum::StructureStart => {
StagedChunkEnum::StructureStart => match generator {
generator::WorldGenerator::Noise(noise_gen) => {
self.chunks[mid]
.get_proto_chunk_mut()
.set_structure_starts(generator);
.set_structure_starts(noise_gen);
}
StagedChunkEnum::StructureReferences => {
generator::WorldGenerator::Flat(_) => {}
},
StagedChunkEnum::StructureReferences => match generator {
generator::WorldGenerator::Noise(noise_gen) => {
self.chunks[mid]
.get_proto_chunk_mut()
.set_structure_references(generator);
.set_structure_references(noise_gen);
}
StagedChunkEnum::Biomes => self.chunks[mid]
generator::WorldGenerator::Flat(_) => {}
},
StagedChunkEnum::Biomes => match generator {
generator::WorldGenerator::Noise(noise_gen) => {
self.chunks[mid]
.get_proto_chunk_mut()
.step_to_biomes(generator),
StagedChunkEnum::Noise => self.chunks[mid]
.step_to_biomes(noise_gen);
}
generator::WorldGenerator::Flat(flat_gen) => {
flat_gen.step_to_biomes(self.chunks[mid].get_proto_chunk_mut());
}
},
StagedChunkEnum::Noise => match generator {
generator::WorldGenerator::Noise(noise_gen) => {
self.chunks[mid]
.get_proto_chunk_mut()
.step_to_noise(generator),
StagedChunkEnum::Surface => self.chunks[mid]
.step_to_noise(noise_gen);
}
generator::WorldGenerator::Flat(flat_gen) => {
flat_gen.step_to_noise(self.chunks[mid].get_proto_chunk_mut());
}
},
StagedChunkEnum::Surface => match generator {
generator::WorldGenerator::Noise(noise_gen) => {
self.chunks[mid]
.get_proto_chunk_mut()
.step_to_surface(generator),
StagedChunkEnum::Carvers => self.chunks[mid]
.step_to_surface(noise_gen);
}
generator::WorldGenerator::Flat(flat_gen) => {
flat_gen.step_to_surface(self.chunks[mid].get_proto_chunk_mut());
}
},
StagedChunkEnum::Carvers => match generator {
generator::WorldGenerator::Noise(noise_gen) => {
self.chunks[mid]
.get_proto_chunk_mut()
.step_to_carvers(generator),
StagedChunkEnum::Features => {
.step_to_carvers(noise_gen);
}
generator::WorldGenerator::Flat(flat_gen) => {
flat_gen.step_to_carvers(self.chunks[mid].get_proto_chunk_mut());
}
},
StagedChunkEnum::Features => match generator {
generator::WorldGenerator::Noise(noise_gen) => {
ProtoChunk::generate_features_and_structure(
self,
block_registry,
&generator.random_config,
&noise_gen.random_config,
);
}
generator::WorldGenerator::Flat(_) => {
self.chunks[mid].get_proto_chunk_mut().stage = StagedChunkEnum::Features;
}
},
StagedChunkEnum::Lighting => {
let mut engine = crate::lighting::LightEngine::new();
engine.initialize_light(self, lighting_config);
@ -410,7 +449,7 @@ impl Cache {
let chunk = self.chunks[mid].get_proto_chunk_mut();
debug_assert_eq!(chunk.stage, StagedChunkEnum::Spawn);
chunk.stage = StagedChunkEnum::Full;
self.chunks[mid].upgrade_to_level_chunk(&generator.dimension, lighting_config);
self.chunks[mid].upgrade_to_level_chunk(generator.dimension(), lighting_config);
}
StagedChunkEnum::None => {}
}

View File

@ -1272,7 +1272,7 @@ impl GenerationSchedule {
let send_chunk = self.send_chunk.clone();
let level = level.clone();
let settings =
GenerationSettings::from_dimension(&level.world_gen.dimension);
GenerationSettings::from_dimension(level.world_gen.dimension());
pool.spawn(move || {
let result = crate::chunk_system::worker_logic::run_generation(

View File

@ -179,7 +179,10 @@ pub async fn io_write_work(recv: AsyncRx<Vec<(ChunkPos, Chunk)>>, level: Arc<Lev
Chunk::Level(chunk) => vec.push((pos, chunk)),
Chunk::Proto(chunk) => {
let mut temp = Chunk::Proto(chunk);
temp.upgrade_to_level_chunk(&level.world_gen.dimension, &level.lighting_config);
temp.upgrade_to_level_chunk(
level.world_gen.dimension(),
&level.lighting_config,
);
let Chunk::Level(chunk) = temp else { panic!() };
vec.push((pos, chunk));
}
@ -262,7 +265,7 @@ pub fn generation_work(
send: &crossfire::compat::MTx<(ChunkPos, RecvChunk)>,
level: &Arc<Level>,
) {
let settings = GenerationSettings::from_dimension(&level.world_gen.dimension);
let settings = GenerationSettings::from_dimension(level.world_gen.dimension());
loop {
let Ok((pos, cache, stage)) = recv.recv() else {

View File

@ -365,11 +365,14 @@ fn with_carve_run_options<F>(
) where
F: FnOnce(&mut CarveRun<'_, '_>),
{
use crate::generation::generator::{GeneratorInit, VanillaGenerator};
use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator};
use pumpkin_util::world_seed::Seed;
let generator = VanillaGenerator::new(Seed(42), dimension);
let mut chunk = ProtoChunk::new(0, 0, &generator);
let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(Seed(42), dimension)));
let WorldGenerator::Noise(generator) = &world_gen else {
unreachable!()
};
let mut chunk = ProtoChunk::new(0, 0, &world_gen);
let start_x = crate::generation::positions::chunk_pos::start_block_x(chunk.x);
let start_z = crate::generation::positions::chunk_pos::start_block_z(chunk.z);

View File

@ -0,0 +1,70 @@
use super::FlatLayer;
use crate::chunk_system::StagedChunkEnum;
use crate::generation::Seed;
use crate::generation::positions::chunk_pos::{start_block_x, start_block_z};
use crate::generation::proto_chunk::ProtoChunk;
use pumpkin_data::Block;
use pumpkin_data::dimension::Dimension;
pub struct FlatGenerator {
pub seed: u64,
pub dimension: Dimension,
pub layers: Vec<FlatLayer>,
pub biome: String,
}
impl FlatGenerator {
#[must_use]
pub const fn new(
seed: Seed,
dimension: Dimension,
layers: Vec<FlatLayer>,
biome: String,
) -> Self {
Self {
seed: seed.0,
dimension,
layers,
biome,
}
}
pub fn step_to_biomes(&self, chunk: &mut ProtoChunk) {
let clean_biome = self.biome.strip_prefix("minecraft:").unwrap_or(&self.biome);
let biome_id = pumpkin_data::chunk::Biome::from_name(clean_biome)
.map_or(pumpkin_data::chunk::Biome::PLAINS.id, |b| b.id);
chunk.flat_biome_map.fill(biome_id);
chunk.stage = StagedChunkEnum::Biomes;
}
pub fn step_to_noise(&self, chunk: &mut ProtoChunk) {
let start_x = start_block_x(chunk.x);
let start_z = start_block_z(chunk.z);
for x in 0..16 {
for z in 0..16 {
let mut current_y = chunk.bottom_y() as i32;
for layer in &self.layers {
let block = Block::from_name(&layer.block);
let state = block.map_or(Block::AIR.default_state, |b| b.default_state);
for _ in 0..layer.height {
if current_y < chunk.bottom_y() as i32 + chunk.height() as i32 {
chunk.set_block_state(start_x + x, current_y, start_z + z, state);
current_y += 1;
} else {
break;
}
}
}
}
}
chunk.stage = StagedChunkEnum::Noise;
}
pub const fn step_to_surface(&self, chunk: &mut ProtoChunk) {
chunk.stage = StagedChunkEnum::Surface;
}
pub const fn step_to_carvers(&self, chunk: &mut ProtoChunk) {
chunk.stage = StagedChunkEnum::Carvers;
}
}

View File

@ -18,6 +18,47 @@ pub trait GeneratorInit {
use pumpkin_data::structures::{StructurePlacementCalculator, StructureSet};
use rustc_hash::FxHashMap;
pub mod flat;
#[derive(Clone, Debug)]
pub struct FlatLayer {
pub block: String,
pub height: i32,
}
pub enum WorldGenerator {
Noise(Box<VanillaGenerator>),
Flat(flat::FlatGenerator),
}
impl WorldGenerator {
#[must_use]
pub const fn dimension(&self) -> &Dimension {
match self {
Self::Noise(noise_gen) => &noise_gen.dimension,
Self::Flat(flat_gen) => &flat_gen.dimension,
}
}
#[must_use]
pub const fn seed(&self) -> u64 {
match self {
Self::Noise(noise_gen) => noise_gen.random_config.seed,
Self::Flat(flat_gen) => flat_gen.seed,
}
}
#[must_use]
pub const fn global_structure_cache(
&self,
) -> Option<&crate::generation::structure::placement::GlobalStructureCache> {
match self {
Self::Noise(noise_gen) => Some(&noise_gen.global_structure_cache),
Self::Flat(_) => None,
}
}
}
pub struct VanillaGenerator {
pub random_config: GlobalRandomConfig,
pub base_router: ProtoNoiseRouters,

View File

@ -25,9 +25,22 @@ use pumpkin_util::{
};
#[must_use]
pub fn get_world_gen(seed: Seed, dimension: Dimension) -> Box<VanillaGenerator> {
// TODO decide which WorldGenerator to pick based on config.
Box::new(VanillaGenerator::new(seed, dimension))
pub fn get_world_gen(
seed: Seed,
dimension: Dimension,
is_flat: bool,
flat_layers: Vec<generator::FlatLayer>,
flat_biome: String,
) -> Box<generator::WorldGenerator> {
if is_flat {
Box::new(generator::WorldGenerator::Flat(
generator::flat::FlatGenerator::new(seed, dimension, flat_layers, flat_biome),
))
} else {
Box::new(generator::WorldGenerator::Noise(Box::new(
VanillaGenerator::new(seed, dimension),
)))
}
}
pub struct GlobalRandomConfig {

View File

@ -176,17 +176,28 @@ impl TerrainCache {
impl ProtoChunk {
#[must_use]
pub fn new(x: i32, z: i32, generator: &super::generator::VanillaGenerator) -> Self {
let dimension = &generator.dimension;
pub fn new(x: i32, z: i32, generator: &super::generator::WorldGenerator) -> Self {
let dimension = generator.dimension();
let height = dimension.logical_height as u16;
let section_count = (height as usize) / 16;
let default_block = match generator {
super::generator::WorldGenerator::Noise(noise_gen) => noise_gen.default_block,
super::generator::WorldGenerator::Flat(_) => Block::AIR.default_state,
};
let biome_mixer_seed = match generator {
super::generator::WorldGenerator::Noise(noise_gen) => noise_gen.biome_mixer_seed,
super::generator::WorldGenerator::Flat(flat_gen) => {
crate::biome::hash_seed(flat_gen.seed)
}
};
let default_heightmap = [i16::MIN; CHUNK_AREA];
Self {
x,
z,
default_block: generator.default_block,
biome_mixer_seed: generator.biome_mixer_seed,
default_block,
biome_mixer_seed,
flat_block_map: vec![BlockStateId::AIR; CHUNK_AREA * height as usize]
.into_boxed_slice(),
flat_biome_map: vec![
@ -225,7 +236,7 @@ impl ProtoChunk {
#[must_use]
pub fn from_chunk_data(
chunk_data: &ChunkData,
generator: &super::generator::VanillaGenerator,
generator: &super::generator::WorldGenerator,
) -> Self {
let mut proto_chunk = Self::new(chunk_data.x, chunk_data.z, generator);
@ -518,7 +529,6 @@ impl ProtoChunk {
#[expect(clippy::too_many_lines)]
pub fn step_to_noise(&mut self, generator: &super::generator::VanillaGenerator) {
debug_assert_eq!(self.stage, StagedChunkEnum::StructureReferences);
let settings = generator.settings;
let generation_shape = &settings.shape;
let horizontal_cell_count = CHUNK_DIM / generation_shape.horizontal_cell_block_count();
@ -717,7 +727,6 @@ impl ProtoChunk {
pub fn step_to_carvers(&mut self, generator: &super::generator::VanillaGenerator) {
debug_assert_eq!(self.stage, StagedChunkEnum::Surface);
super::carver::carve(self, generator);
self.stage = StagedChunkEnum::Carvers;

View File

@ -1,7 +1,7 @@
use crate::chunk::format::linear::LinearV2File;
use crate::chunk::format::pump::PumpFile;
use crate::chunk_system::{ChunkListener, ChunkLoading, GenerationSchedule, LevelChannel};
use crate::generation::generator::VanillaGenerator;
use crate::generation::generator::WorldGenerator;
use crate::lighting::DynamicLightEngine;
use crate::{
chunk::{
@ -77,7 +77,7 @@ pub struct Level {
pub chunk_saver: Arc<dyn FileIO<Data = SyncChunk>>,
entity_saver: Arc<dyn FileIO<Data = SyncEntityChunk>>,
pub world_gen: Arc<VanillaGenerator>,
pub world_gen: Arc<WorldGenerator>,
/// Handles runtime lighting updates
pub light_engine: DynamicLightEngine,
@ -125,6 +125,7 @@ pub struct LevelFolder {
impl Level {
#[must_use]
#[expect(clippy::too_many_lines)]
pub fn from_root_folder(
level_config: &LevelConfig,
root_folder: PathBuf,
@ -132,11 +133,18 @@ impl Level {
dimension: Dimension,
gen_pool: Option<Arc<rayon::ThreadPool>>,
) -> Arc<Self> {
let dim_folder = if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name
|| dimension.minecraft_name == Dimension::THE_NETHER.minecraft_name
|| dimension.minecraft_name == Dimension::THE_END.minecraft_name
{
root_folder.clone()
} else {
let (namespace, name) = match dimension.minecraft_name.split_once(':') {
Some((ns, n)) => (ns, n),
None => ("minecraft", dimension.minecraft_name),
};
let dim_folder = root_folder.join("dimensions").join(namespace).join(name);
root_folder.join("dimensions").join(namespace).join(name)
};
let region_folder = dim_folder.join("region");
let entities_folder = dim_folder.join("entities");
@ -154,8 +162,62 @@ impl Level {
poi_folder,
});
let main_folder = if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name {
level_folder.root_folder.clone()
} else {
level_folder
.root_folder
.parent()
.unwrap_or(&level_folder.root_folder)
.to_path_buf()
};
let mut is_flat = false;
let mut flat_layers = Vec::new();
let mut flat_biome = "minecraft:plains".to_string();
if let Some(wgs) = crate::world_info::data_files::read_world_gen_settings(&main_folder)
&& let Some(dim_settings) = wgs.dimensions.get(dimension.minecraft_name)
&& dim_settings.generator.generator_type == "minecraft:flat"
{
is_flat = true;
if let Some(crate::world_info::GeneratorSettings::Compound(nbt)) =
&dim_settings.generator.settings
{
if let Some(pumpkin_nbt::tag::NbtTag::String(b)) = nbt.child_tags.get("biome") {
flat_biome = b.to_string();
}
if let Some(pumpkin_nbt::tag::NbtTag::List(list)) = nbt.child_tags.get("layers") {
for tag in list {
if let pumpkin_nbt::tag::NbtTag::Compound(layer_compound) = tag {
let mut block = "minecraft:air".to_string();
let mut height = 1;
if let Some(pumpkin_nbt::tag::NbtTag::String(bl)) =
layer_compound.child_tags.get("block")
{
block = bl.to_string();
}
if let Some(pumpkin_nbt::tag::NbtTag::Int(h)) =
layer_compound.child_tags.get("height")
{
height = *h;
}
flat_layers
.push(crate::generation::generator::FlatLayer { block, height });
}
}
}
}
}
let seed = Seed(seed as u64);
let world_gen = get_world_gen(seed, dimension).into();
let world_gen: Arc<WorldGenerator> = Arc::from(get_world_gen(
seed,
dimension,
is_flat,
flat_layers,
flat_biome,
));
let chunk_saver: Arc<dyn FileIO<Data = SyncChunk>> = match &level_config.chunk {
ChunkConfig::Linear => Arc::new(ChunkFileManager::<LinearV2File<ChunkData>>::new(())),

View File

@ -1,3 +1,5 @@
#![allow(clippy::unreachable)]
use pumpkin_data::{
Block, BlockState, chunk_gen_settings::GenerationSettings, dimension::Dimension,
};
@ -57,15 +59,21 @@ pub fn bench_create_and_populate_noise(
_terrain_cache: &TerrainCache,
_default_block: &'static BlockState,
) {
use crate::generation::generator::{GeneratorInit, VanillaGenerator};
use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator};
use crate::generation::noise::router::surface_height_sampler::{
SurfaceHeightEstimateSampler, SurfaceHeightSamplerBuilderOptions,
};
use crate::generation::proto_chunk::StandardChunkFluidLevelSampler;
use pumpkin_util::world_seed::Seed;
let generator = VanillaGenerator::new(Seed(random_config.seed), Dimension::OVERWORLD);
let mut chunk = ProtoChunk::new(0, 0, &generator);
let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(
Seed(random_config.seed),
Dimension::OVERWORLD,
)));
let WorldGenerator::Noise(generator) = &world_gen else {
unreachable!()
};
let mut chunk = ProtoChunk::new(0, 0, &world_gen);
// Create noise sampler and other required components
let settings = generator.settings;
@ -119,7 +127,7 @@ pub fn bench_create_and_populate_noise(
);
chunk.populate_noise(
&generator,
generator,
&mut noise_sampler,
&generator.random_config.ore_random_deriver,
&mut surface_height_estimate_sampler,
@ -133,15 +141,21 @@ pub fn bench_create_and_populate_biome(
_terrain_cache: &TerrainCache,
_default_block: &'static BlockState,
) {
use crate::generation::generator::{GeneratorInit, VanillaGenerator};
use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator};
use crate::generation::noise::router::multi_noise_sampler::{
MultiNoiseSampler, MultiNoiseSamplerBuilderOptions,
};
use crate::generation::{biome_coords, positions::chunk_pos};
use pumpkin_util::world_seed::Seed;
let generator = VanillaGenerator::new(Seed(random_config.seed), Dimension::OVERWORLD);
let mut chunk = ProtoChunk::new(0, 0, &generator);
let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(
Seed(random_config.seed),
Dimension::OVERWORLD,
)));
let WorldGenerator::Noise(generator) = &world_gen else {
unreachable!()
};
let mut chunk = ProtoChunk::new(0, 0, &world_gen);
// Create multi-noise sampler
let start_x = chunk_pos::start_block_x(0);
@ -159,7 +173,7 @@ pub fn bench_create_and_populate_biome(
let mut multi_noise_sampler =
MultiNoiseSampler::generate(&generator.base_router.multi_noise, &multi_noise_config);
chunk.populate_biomes(&generator, &mut multi_noise_sampler);
chunk.populate_biomes(generator, &mut multi_noise_sampler);
}
pub fn bench_create_and_populate_noise_with_surface(
@ -169,7 +183,7 @@ pub fn bench_create_and_populate_noise_with_surface(
_terrain_cache: &TerrainCache,
_default_block: &'static BlockState,
) {
use crate::generation::generator::{GeneratorInit, VanillaGenerator};
use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator};
use crate::generation::noise::router::{
multi_noise_sampler::{MultiNoiseSampler, MultiNoiseSamplerBuilderOptions},
surface_height_sampler::{
@ -179,8 +193,14 @@ pub fn bench_create_and_populate_noise_with_surface(
use crate::generation::proto_chunk::StandardChunkFluidLevelSampler;
use pumpkin_util::world_seed::Seed;
let generator = VanillaGenerator::new(Seed(random_config.seed), Dimension::OVERWORLD);
let mut chunk = ProtoChunk::new(0, 0, &generator);
let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(
Seed(random_config.seed),
Dimension::OVERWORLD,
)));
let WorldGenerator::Noise(generator) = &world_gen else {
unreachable!()
};
let mut chunk = ProtoChunk::new(0, 0, &world_gen);
// Create all required components
let settings = generator.settings;
@ -241,12 +261,12 @@ pub fn bench_create_and_populate_noise_with_surface(
&surface_config,
);
chunk.populate_biomes(&generator, &mut multi_noise_sampler);
chunk.populate_biomes(generator, &mut multi_noise_sampler);
chunk.populate_noise(
&generator,
generator,
&mut noise_sampler,
&generator.random_config.ore_random_deriver,
&mut surface_height_estimate_sampler,
);
chunk.build_surface(&generator, &mut surface_height_estimate_sampler);
chunk.build_surface(generator, &mut surface_height_estimate_sampler);
}

View File

@ -177,7 +177,7 @@ impl PoiRegion {
/// Compress chunk data to bytes
fn compress_chunk_data(chunk_data: &PoiChunkData) -> std::io::Result<Vec<u8>> {
let mut uncompressed = Vec::new();
pumpkin_nbt::to_bytes_unnamed(chunk_data, &mut uncompressed)
pumpkin_nbt::to_bytes(chunk_data, &mut uncompressed)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
@ -191,7 +191,15 @@ impl PoiRegion {
let mut uncompressed = Vec::new();
decoder.read_to_end(&mut uncompressed)?;
let is_named = uncompressed.len() >= 3
&& uncompressed[0] == 0x0a
&& uncompressed[1] == 0x00
&& uncompressed[2] == 0x00;
if is_named {
pumpkin_nbt::from_bytes(Cursor::new(uncompressed))
} else {
pumpkin_nbt::from_bytes_unnamed(Cursor::new(uncompressed))
}
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))
}

View File

@ -12,6 +12,12 @@ use serde::{Deserialize, Serialize};
use crate::world_info::{
MAXIMUM_SUPPORTED_LEVEL_VERSION, MAXIMUM_SUPPORTED_WORLD_DATA_VERSION,
MINIMUM_SUPPORTED_LEVEL_VERSION, MINIMUM_SUPPORTED_WORLD_DATA_VERSION,
data_files::{
minecraft_data_dir, read_game_rules, read_wandering_trader, read_weather,
read_world_clocks, read_world_gen_settings, write_custom_boss_events_stub,
write_game_rules, write_scheduled_events_stub, write_wandering_trader, write_weather,
write_world_clocks, write_world_gen_settings,
},
};
use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter};
@ -89,10 +95,43 @@ impl WorldInfoReader for AnvilLevelInfo {
check_file_data_version(&buf)?;
check_file_level_version(&buf)?;
let info = pumpkin_nbt::from_bytes::<LevelDat>(Cursor::new(buf))
let mut info = pumpkin_nbt::from_bytes::<LevelDat>(Cursor::new(buf))
.map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?;
// TODO: check version
// game_rules.dat – prefer the new file; fall back to level.dat values
if minecraft_data_dir(level_folder)
.join("game_rules.dat")
.exists()
{
info.data.game_rules = read_game_rules(level_folder);
}
// world_gen_settings.dat
if let Some(wgs) = read_world_gen_settings(level_folder) {
info.data.world_gen_settings = wgs;
}
// world_clocks.dat – read the overworld day_time
if minecraft_data_dir(level_folder)
.join("world_clocks.dat")
.exists()
{
let clocks = read_world_clocks(level_folder);
if let Some(overworld) = clocks.clocks.get("minecraft:overworld") {
info.data.day_time = overworld.total_ticks;
}
}
// weather.dat
if minecraft_data_dir(level_folder)
.join("weather.dat")
.exists()
{
let weather = read_weather(level_folder);
info.data.clear_weather_time = weather.clear_weather_time;
}
// (wandering_trader.dat is not part of LevelData; stored separately when needed)
Ok(info.data)
}
@ -112,20 +151,76 @@ impl WorldInfoWriter for AnvilLevelInfo {
level_data.last_played = since_the_epoch.as_millis() as i64;
let level = LevelDat { data: level_data };
// open file
// ── Write level.dat ───────────────────────────────────────────────────
let path = level_folder.join(LEVEL_DAT_FILE_NAME);
let world_info_file = File::create(path)?;
// write compressed data into file
let compression_writer = GzEncoder::new(world_info_file, Compression::best());
// TODO: Proper error handling
pumpkin_nbt::to_bytes(&level, compression_writer)
.expect("Failed to write level.dat to disk");
let data_version = info.data_version;
// ── Write data/minecraft/*.dat files ─────────────────────────────────
// game_rules.dat
if let Err(e) = write_game_rules(level_folder, &info.game_rules, data_version) {
error!("Failed to write game_rules.dat: {e}");
}
// world_gen_settings.dat
if let Err(e) =
write_world_gen_settings(level_folder, &info.world_gen_settings, data_version)
{
error!("Failed to write world_gen_settings.dat: {e}");
}
// world_clocks.dat – persist the overworld day_time; preserve other
let mut clocks = read_world_clocks(level_folder);
clocks.data_version = data_version;
clocks
.clocks
.entry("minecraft:overworld".to_string())
.and_modify(|c| c.total_ticks = info.day_time)
.or_insert(crate::world_info::data_files::DimensionClock {
total_ticks: info.day_time,
});
if let Err(e) = write_world_clocks(level_folder, &clocks) {
error!("Failed to write world_clocks.dat: {e}");
}
// weather.dat
let mut weather = read_weather(level_folder);
weather.clear_weather_time = info.clear_weather_time;
weather.data_version = data_version;
if let Err(e) = write_weather(level_folder, &weather) {
error!("Failed to write weather.dat: {e}");
}
// wandering_trader.dat (stub / load-save)
let mut wandering_trader = read_wandering_trader(level_folder);
wandering_trader.data_version = data_version;
if let Err(e) = write_wandering_trader(level_folder, &wandering_trader) {
error!("Failed to write wandering_trader.dat: {e}");
}
// custom_boss_events.dat
if let Err(e) = write_custom_boss_events_stub(level_folder, data_version) {
error!("Failed to write custom_boss_events.dat: {e}");
}
// scheduled_events.dat
if let Err(e) = write_scheduled_events_stub(level_folder, data_version) {
error!("Failed to write scheduled_events.dat: {e}");
}
Ok(())
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct LevelDat {
// This tag contains all the level data.
#[serde(rename = "Data")]
@ -267,7 +362,13 @@ mod test {
let level_dat_again: LevelDat =
from_bytes(Cursor::new(serialized)).expect("Failed to decode from bytes");
assert_eq!(level_dat_again, *LEVEL_DAT);
let mut expected = (*LEVEL_DAT).clone();
expected.data.game_rules = GameRuleRegistry::default();
expected.data.world_gen_settings = WorldGenSettings::default();
expected.data.day_time = 0;
expected.data.clear_weather_time = 0;
assert_eq!(level_dat_again, expected);
}
#[test]

View File

@ -0,0 +1,417 @@
use std::{
fs::{self, File},
io::BufWriter,
path::{Path, PathBuf},
};
use pumpkin_data::game_rules::{GameRule, GameRuleRegistry, GameRuleValue};
use pumpkin_nbt::{
compound::NbtCompound,
nbt_compress::{from_gzip_bytes, read_gzip_compound_tag, to_gzip_bytes},
tag::NbtTag,
};
use serde::{Deserialize, Serialize};
use tracing::warn;
use crate::world_info::{WorldGenSettings, WorldInfoError};
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct DataFileRoot<T> {
#[serde(rename = "data")]
pub data: T,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct WeatherData {
#[serde(rename = "rain_time", default)]
pub rain_time: i32,
#[serde(rename = "raining", default)]
pub raining: bool,
#[serde(rename = "thundering", default)]
pub thundering: bool,
#[serde(rename = "thunder_time", default)]
pub thunder_time: i32,
#[serde(rename = "clear_weather_time", default)]
pub clear_weather_time: i32,
#[serde(rename = "DataVersion", default)]
pub data_version: i32,
}
impl Default for WeatherData {
fn default() -> Self {
Self {
rain_time: 0,
raining: false,
thundering: false,
thunder_time: 0,
clear_weather_time: -1,
data_version: 0,
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
pub struct WorldGenSettingsData {
#[serde(flatten)]
pub settings: WorldGenSettings,
#[serde(rename = "DataVersion", default)]
pub data_version: i32,
#[serde(rename = "bonus_chest", default)]
pub bonus_chest: bool,
#[serde(rename = "generate_structures", default = "default_true")]
pub generate_structures: bool,
}
const fn default_true() -> bool {
true
}
impl WorldGenSettingsData {
#[must_use]
pub const fn new(settings: WorldGenSettings, data_version: i32) -> Self {
Self {
settings,
data_version,
bonus_chest: false,
generate_structures: true,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct DimensionClock {
pub total_ticks: i64,
}
#[derive(Clone, PartialEq, Eq, Debug, Default)]
pub struct WorldClocksData {
pub clocks: std::collections::HashMap<String, DimensionClock>,
pub data_version: i32,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
pub struct WanderingTraderData {
#[serde(rename = "spawn_delay", default = "default_wandering_trader_delay")]
pub spawn_delay: i32,
#[serde(rename = "spawn_chance", default = "default_wandering_trader_chance")]
pub spawn_chance: i32,
#[serde(rename = "DataVersion", default)]
pub data_version: i32,
}
const fn default_wandering_trader_delay() -> i32 {
24_000
}
const fn default_wandering_trader_chance() -> i32 {
25
}
impl Default for WanderingTraderData {
fn default() -> Self {
Self {
spawn_delay: default_wandering_trader_delay(),
spawn_chance: default_wandering_trader_chance(),
data_version: 0,
}
}
}
#[must_use]
pub fn minecraft_data_dir(level_folder: &Path) -> PathBuf {
level_folder.join("data").join("minecraft")
}
/// Ensures the `<world>/data/minecraft/` directory exists.
pub fn ensure_minecraft_data_dir(level_folder: &Path) -> Result<PathBuf, WorldInfoError> {
let dir = minecraft_data_dir(level_folder);
fs::create_dir_all(&dir)?;
Ok(dir)
}
pub fn read_weather(level_folder: &Path) -> WeatherData {
let path = minecraft_data_dir(level_folder).join("weather.dat");
if !path.exists() {
return WeatherData::default();
}
match File::open(&path) {
Ok(f) => match from_gzip_bytes::<DataFileRoot<WeatherData>, _>(f) {
Ok(root) => root.data,
Err(e) => {
warn!("Failed to deserialize weather.dat, using defaults: {e}");
WeatherData::default()
}
},
Err(e) => {
warn!("Failed to open weather.dat, using defaults: {e}");
WeatherData::default()
}
}
}
pub fn write_weather(level_folder: &Path, data: &WeatherData) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("weather.dat");
let file = File::create(&path)?;
let root = DataFileRoot { data: data.clone() };
to_gzip_bytes(&root, BufWriter::new(file))
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
pub fn read_world_gen_settings(level_folder: &Path) -> Option<WorldGenSettings> {
let path = minecraft_data_dir(level_folder).join("world_gen_settings.dat");
if !path.exists() {
return None;
}
match File::open(&path) {
Ok(f) => match from_gzip_bytes::<DataFileRoot<WorldGenSettingsData>, _>(f) {
Ok(root) => Some(root.data.settings),
Err(e) => {
warn!("Failed to deserialize world_gen_settings.dat: {e}");
None
}
},
Err(e) => {
warn!("Failed to open world_gen_settings.dat: {e}");
None
}
}
}
pub fn write_world_gen_settings(
level_folder: &Path,
settings: &WorldGenSettings,
data_version: i32,
) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("world_gen_settings.dat");
let file = File::create(&path)?;
let data = WorldGenSettingsData::new(settings.clone(), data_version);
let root = DataFileRoot { data };
to_gzip_bytes(&root, BufWriter::new(file))
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
#[must_use]
pub fn game_rules_to_nbt(rules: &GameRuleRegistry, data_version: i32) -> NbtCompound {
let mut inner = NbtCompound::new();
for rule in GameRule::all() {
let key = format!("minecraft:{rule}");
match rules.get(rule) {
GameRuleValue::Bool(b) => inner.put(&key, NbtTag::Byte(i8::from(*b))),
GameRuleValue::Int(i) => inner.put(&key, NbtTag::Int(*i as i32)),
}
}
inner.put_int("DataVersion", data_version);
let mut root = NbtCompound::new();
root.put_compound("data", inner);
root
}
pub fn game_rules_from_nbt(root: &NbtCompound) -> GameRuleRegistry {
let mut registry = GameRuleRegistry::default();
let Some(inner) = root.get_compound("data") else {
warn!("game_rules.dat missing 'data' compound, using defaults");
return registry;
};
for rule in GameRule::all() {
let key = format!("minecraft:{rule}");
match registry.get_mut(rule) {
GameRuleValue::Bool(b) => {
if let Some(v) = inner.get_byte(&key) {
*b = v != 0;
}
}
GameRuleValue::Int(i) => {
if let Some(v) = inner.get_int(&key) {
*i = i64::from(v);
}
}
}
}
registry
}
pub fn read_game_rules(level_folder: &Path) -> GameRuleRegistry {
let path = minecraft_data_dir(level_folder).join("game_rules.dat");
if !path.exists() {
return GameRuleRegistry::default();
}
match File::open(&path) {
Ok(f) => match read_gzip_compound_tag(f) {
Ok(compound) => game_rules_from_nbt(&compound),
Err(e) => {
warn!("Failed to parse game_rules.dat: {e}");
GameRuleRegistry::default()
}
},
Err(e) => {
warn!("Failed to open game_rules.dat: {e}");
GameRuleRegistry::default()
}
}
}
pub fn write_game_rules(
level_folder: &Path,
rules: &GameRuleRegistry,
data_version: i32,
) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("game_rules.dat");
let compound = game_rules_to_nbt(rules, data_version);
let file = File::create(&path)?;
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(compound, file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
pub fn read_world_clocks(level_folder: &Path) -> WorldClocksData {
let path = minecraft_data_dir(level_folder).join("world_clocks.dat");
if !path.exists() {
return WorldClocksData::default();
}
match File::open(&path) {
Ok(f) => match read_gzip_compound_tag(f) {
Ok(compound) => world_clocks_from_nbt(&compound),
Err(e) => {
warn!("Failed to parse world_clocks.dat: {e}");
WorldClocksData::default()
}
},
Err(e) => {
warn!("Failed to open world_clocks.dat: {e}");
WorldClocksData::default()
}
}
}
fn world_clocks_from_nbt(root: &NbtCompound) -> WorldClocksData {
let mut result = WorldClocksData::default();
let Some(inner) = root.get_compound("data") else {
return result;
};
result.data_version = inner.get_int("DataVersion").unwrap_or(0);
for (key, tag) in &inner.child_tags {
if key.as_ref() == "DataVersion" {
continue;
}
if let NbtTag::Compound(dim_compound) = tag {
let total_ticks = dim_compound.get_long("total_ticks").unwrap_or(0);
result
.clocks
.insert(key.to_string(), DimensionClock { total_ticks });
}
}
result
}
pub fn write_world_clocks(
level_folder: &Path,
clocks: &WorldClocksData,
) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("world_clocks.dat");
let mut inner = NbtCompound::new();
for (dim_name, clock) in &clocks.clocks {
let mut dim_compound = NbtCompound::new();
dim_compound.put_long("total_ticks", clock.total_ticks);
inner.put_compound(dim_name, dim_compound);
}
inner.put_int("DataVersion", clocks.data_version);
let mut root = NbtCompound::new();
root.put_compound("data", inner);
let file = File::create(&path)?;
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
pub fn read_wandering_trader(level_folder: &Path) -> WanderingTraderData {
let path = minecraft_data_dir(level_folder).join("wandering_trader.dat");
if !path.exists() {
return WanderingTraderData::default();
}
match File::open(&path) {
Ok(f) => match from_gzip_bytes::<DataFileRoot<WanderingTraderData>, _>(f) {
Ok(root) => root.data,
Err(e) => {
warn!("Failed to deserialize wandering_trader.dat, using defaults: {e}");
WanderingTraderData::default()
}
},
Err(e) => {
warn!("Failed to open wandering_trader.dat: {e}");
WanderingTraderData::default()
}
}
}
pub fn write_wandering_trader(
level_folder: &Path,
data: &WanderingTraderData,
) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("wandering_trader.dat");
let file = File::create(&path)?;
let root = DataFileRoot { data: data.clone() };
to_gzip_bytes(&root, BufWriter::new(file))
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
pub fn write_custom_boss_events_stub(
level_folder: &Path,
data_version: i32,
) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("custom_boss_events.dat");
// Only create if absent; actual boss-bar persistence lives elsewhere.
if path.exists() {
return Ok(());
}
let mut inner = NbtCompound::new();
inner.put_int("DataVersion", data_version);
let mut root = NbtCompound::new();
root.put_compound("data", inner);
let file = File::create(&path)?;
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}
pub fn write_scheduled_events_stub(
level_folder: &Path,
data_version: i32,
) -> Result<(), WorldInfoError> {
let dir = ensure_minecraft_data_dir(level_folder)?;
let path = dir.join("scheduled_events.dat");
if path.exists() {
return Ok(());
}
let mut inner = NbtCompound::new();
inner.put("events", NbtTag::List(vec![]));
inner.put_int("DataVersion", data_version);
let mut root = NbtCompound::new();
root.put_compound("data", inner);
let file = File::create(&path)?;
pumpkin_nbt::nbt_compress::write_gzip_compound_tag(root, file)
.map_err(|e| WorldInfoError::SerializationError(e.to_string()))
}

View File

@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
pub mod anvil;
pub mod data_files;
// Constraint: disk biome palette serialization changed in 1.21.5
pub const MINIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4435; // 1.21.9
@ -48,21 +49,14 @@ pub struct LevelData {
pub border_warning_blocks: f64,
#[serde(default = "default_border_warning_time")]
pub border_warning_time: f64,
#[serde(rename = "clearWeatherTime", default)]
pub clear_weather_time: i32,
#[serde(default = "default_data_packs")]
pub data_packs: DataPacks,
pub data_version: i32,
#[serde(default)]
pub day_time: i64,
#[serde(with = "serde_enum_as_integer", default = "default_difficulty")]
pub difficulty: Difficulty,
#[serde(default)]
pub difficulty_locked: bool,
#[serde(default)]
pub game_rules: GameRuleRegistry,
pub world_gen_settings: WorldGenSettings,
#[serde(default)]
pub last_played: i64,
#[serde(default = "default_level_name")]
pub level_name: String,
@ -82,6 +76,26 @@ pub struct LevelData {
pub level_version: i32,
#[serde(rename = "map_id", default)]
pub map_id: i32,
// These are NOT serialized to level.dat, but are still deserialized from it if present.
// They are loaded and saved by AnvilLevelInfo via the data_files module.
/// Game rules – persisted to `data/minecraft/game_rules.dat`.
#[serde(skip_serializing, default)]
pub game_rules: GameRuleRegistry,
/// World generation settings – persisted to `data/minecraft/world_gen_settings.dat`.
#[serde(skip_serializing, default)]
pub world_gen_settings: WorldGenSettings,
/// In-game time of day (overworld dimension clock).
/// Persisted to `data/minecraft/world_clocks.dat`.
#[serde(skip_serializing, default)]
pub day_time: i64,
/// Remaining ticks of forced-clear weather.
/// Persisted to `data/minecraft/weather.dat`.
#[serde(rename = "clearWeatherTime", skip_serializing, default)]
pub clear_weather_time: i32,
}
const DEFAULT_BORDER_DAMAGE_PER_BLOCK: f64 = 0.2;
@ -134,6 +148,13 @@ pub struct WorldGenSettings {
pub dimensions: Dimensions,
}
impl Default for WorldGenSettings {
fn default() -> Self {
// Use seed 0 as placeholder; actual seed comes from config or world_gen_settings.dat
Self::new(pumpkin_util::world_seed::Seed(0))
}
}
pub type Dimensions = HashMap<String, Dimension>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Dimension {
@ -144,7 +165,8 @@ pub struct Dimension {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Generator {
pub settings: GeneratorSettings,
#[serde(default)]
pub settings: Option<GeneratorSettings>,
#[serde(default)]
pub biome_source: Option<BiomeSource>,
#[serde(rename = "type")]
@ -227,7 +249,9 @@ impl WorldGenSettings {
"minecraft:overworld".to_string(),
Dimension {
generator: Generator {
settings: GeneratorSettings::Reference("minecraft:overworld".to_string()),
settings: Some(GeneratorSettings::Reference(
"minecraft:overworld".to_string(),
)),
biome_source: Some(BiomeSource::WithPreset {
preset: "minecraft:overworld".to_string(),
biome_type: "minecraft:multi_noise".to_string(),
@ -241,7 +265,7 @@ impl WorldGenSettings {
"minecraft:the_nether".to_string(),
Dimension {
generator: Generator {
settings: GeneratorSettings::Reference("minecraft:nether".to_string()),
settings: Some(GeneratorSettings::Reference("minecraft:nether".to_string())),
biome_source: Some(BiomeSource::WithPreset {
preset: "minecraft:nether".to_string(),
biome_type: "minecraft:multi_noise".to_string(),
@ -255,7 +279,7 @@ impl WorldGenSettings {
"minecraft:the_end".to_string(),
Dimension {
generator: Generator {
settings: GeneratorSettings::Reference("minecraft:end".to_string()),
settings: Some(GeneratorSettings::Reference("minecraft:end".to_string())),
biome_source: Some(BiomeSource::Simple {
biome_type: "minecraft:the_end".to_string(),
}),
@ -310,14 +334,10 @@ impl LevelData {
border_size_lerp_time: 0,
border_warning_blocks: DEFAULT_BORDER_WARNING_BLOCKS,
border_warning_time: DEFAULT_BORDER_WARNING_TIME,
clear_weather_time: -1,
data_packs: default_data_packs(),
data_version: MAXIMUM_SUPPORTED_WORLD_DATA_VERSION,
day_time: 0,
difficulty: DEFAULT_DIFFICULTY,
difficulty_locked: false,
game_rules: GameRuleRegistry::default(),
world_gen_settings: WorldGenSettings::new(seed),
last_played: -1,
level_name: DEFAULT_LEVEL_NAME.to_string(),
spawn_x: 0,
@ -328,6 +348,11 @@ impl LevelData {
world_version: WorldVersion::default(),
level_version: MAXIMUM_SUPPORTED_LEVEL_VERSION,
map_id: 0,
// fields now in data/minecraft/*.dat
game_rules: GameRuleRegistry::default(),
world_gen_settings: WorldGenSettings::new(seed),
day_time: 0,
clear_weather_time: -1,
}
}
@ -345,6 +370,8 @@ pub enum WorldInfoError {
InfoNotFound,
#[error("Deserialization error: {0}")]
DeserializationError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Unsupported world data version: {0}")]
UnsupportedDataVersion(i32),
#[error("Unsupported world level version: {0}")]

View File

@ -153,7 +153,6 @@ impl BlockBehaviour for DropperBlock {
fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> {
Box::pin(async move {
if let Some(block_entity) = args.world.get_block_entity(args.position) {
// Safely extract the dropper instead of unwrap()
let Some(dropper) = block_entity.as_any().downcast_ref::<DropperBlockEntity>()
else {
return;

View File

@ -242,10 +242,10 @@ async fn resolve_profiles_from_token(
return Ok(vec![profile]);
}
if server.basic_config.online_mode {
if server.advanced_config.networking.java.online_mode {
match lookup_profile_by_name(
raw_arg.value,
&server.advanced_config.networking.authentication,
&server.advanced_config.networking.java.authentication,
) {
Ok(Some((uuid, resolved_name))) => {
server

View File

@ -70,10 +70,10 @@ impl GameProfileResult {
return Ok(vec![profile]);
}
if server.basic_config.online_mode {
if server.advanced_config.networking.java.online_mode {
return match lookup_profile_by_name(
name,
&server.advanced_config.networking.authentication,
&server.advanced_config.networking.java.authentication,
) {
Ok(Some((uuid, resolved_name))) => {
server

View File

@ -37,6 +37,23 @@ impl CommandExecutor for ListCommandExecutor {
ListMode::Uuids => get_player_names_and_ids(&players),
};
let max_players = context.source.output.as_player().map_or_else(
|| context.server().advanced_config.networking.java.max_players,
|player| match player.client.as_ref() {
crate::net::ClientPlatform::Java(_) => {
context.server().advanced_config.networking.java.max_players
}
crate::net::ClientPlatform::Bedrock(_) => {
context
.server()
.advanced_config
.networking
.bedrock
.max_players
}
},
);
context
.source
.send_feedback(
@ -45,9 +62,7 @@ impl CommandExecutor for ListCommandExecutor {
COMMANDS_LIST_PLAYERS,
[
TextComponent::text(players_len.to_string()),
TextComponent::text(
context.server().basic_config.max_players.to_string(),
),
TextComponent::text(max_players.to_string()),
list,
],
),

View File

@ -51,14 +51,14 @@ impl BegGoal {
}
fn set_begging(mob: &dyn Mob, begging: bool) {
mob.get_mob_entity()
.living_entity
.entity
.send_meta_data(&[Metadata::new(
mob.get_mob_entity().living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::INTERESTED_ID,
MetaDataType::BOOLEAN,
begging,
)]);
)],
None,
);
}
}

View File

@ -173,29 +173,35 @@ impl EntityBase for AreaEffectCloudEntity {
};
// Send initial particle and radius
self.entity
.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new(
self.entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
pumpkin_data::tracked_data::TrackedData::PARTICLE,
pumpkin_data::meta_data_type::MetaDataType::PARTICLE,
&meta,
)]);
)],
None,
);
self.entity
.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new(
self.entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
pumpkin_data::tracked_data::TrackedData::RADIUS,
pumpkin_data::meta_data_type::MetaDataType::FLOAT,
radius,
)]);
)],
None,
);
// Initial waiting flag
let wait_time = *self.wait_time.lock().await;
let is_waiting = 0 < wait_time;
self.entity
.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new(
self.entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
pumpkin_data::tracked_data::TrackedData::WAITING,
pumpkin_data::meta_data_type::MetaDataType::BOOLEAN,
is_waiting,
)]);
)],
None,
);
})
}
@ -225,12 +231,14 @@ impl EntityBase for AreaEffectCloudEntity {
// When the waiting period ends, notify clients so they render full particles
if age == wait_time && wait_time > 0 {
self.entity
.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new(
self.entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
pumpkin_data::tracked_data::TrackedData::WAITING,
pumpkin_data::meta_data_type::MetaDataType::BOOLEAN,
false,
)]);
)],
None,
);
}
if age < wait_time {
@ -251,12 +259,14 @@ impl EntityBase for AreaEffectCloudEntity {
// Send new radius
drop(radius);
self.entity
.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new(
self.entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
pumpkin_data::tracked_data::TrackedData::RADIUS,
pumpkin_data::meta_data_type::MetaDataType::FLOAT,
current_radius,
)]);
)],
None,
);
}
// Tick down reapplication map
@ -380,13 +390,14 @@ impl EntityBase for AreaEffectCloudEntity {
drop(radius_lock);
// Send updated radius to clients
self.entity.send_meta_data(&[
pumpkin_protocol::java::client::play::Metadata::new(
self.entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
pumpkin_data::tracked_data::TrackedData::RADIUS,
pumpkin_data::meta_data_type::MetaDataType::FLOAT,
current_radius,
),
]);
)],
None,
);
}
// Apply duration-on-use (shorten lifespan)

View File

@ -152,11 +152,21 @@ impl BreathManager {
pub fn send_air_supply(&self, player: &Player) {
let air = self.air_supply.load(Ordering::Relaxed).clamp(0, MAX_AIR);
player.get_entity().send_meta_data(&[Metadata::new(
let mut bedrock_meta =
pumpkin_protocol::bedrock::client::set_actor_data::EntityMetadata::new();
bedrock_meta.set(
pumpkin_protocol::bedrock::client::set_actor_data::entity_data_key::AIR_SUPPLY,
pumpkin_protocol::bedrock::client::set_actor_data::MetadataValue::Short(air as i16),
);
player.get_entity().send_meta_data(
&[Metadata::new(
TrackedData::AIR_SUPPLY_ID,
MetaDataType::INT,
VarInt(air),
)]);
)],
Some(&bedrock_meta),
);
}
pub fn reset(&self, player: &Player) {

View File

@ -22,11 +22,14 @@ impl EndCrystalEntity {
impl EndCrystalEntity {
pub fn set_show_bottom(&self, show_bottom: bool) {
self.entity.send_meta_data(&[Metadata::new(
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::SHOW_BOTTOM,
MetaDataType::BOOLEAN,
show_bottom,
)]);
)],
None,
);
}
}

View File

@ -93,11 +93,14 @@ impl EntityBase for FallingEntity {
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
self.entity.send_meta_data(&[Metadata::new(
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::START_POS,
MetaDataType::BLOCK_POS,
self.entity.block_pos.load(),
)]);
)],
None,
);
})
}

View File

@ -470,11 +470,14 @@ impl EntityBase for ItemEntity {
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async {
self.entity.send_meta_data(&[Metadata::new(
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::ITEM,
MetaDataType::ITEM_STACK,
&ItemStackSerializer::from(self.item_stack.lock().await.clone()),
)]);
)],
None,
);
})
}

View File

@ -237,11 +237,26 @@ impl LivingEntity {
b &= !index;
}
self.livings_flags.store(b, Ordering::Relaxed);
self.entity.send_meta_data(&[Metadata::new(
let bedrock_meta = (flag == Self::USING_ITEM_FLAG).then(|| {
let mut meta = pumpkin_protocol::bedrock::client::set_actor_data::EntityMetadata::new();
meta.set_flag(
pumpkin_protocol::bedrock::client::set_actor_data::entity_data_key::FLAGS,
pumpkin_protocol::bedrock::client::set_actor_data::entity_data_flag::USING_ITEM
as u8,
value,
);
meta
});
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::LIVING_ENTITY_FLAGS,
MetaDataType::BYTE,
b,
)]);
)],
bedrock_meta.as_ref(),
);
}
pub async fn clear_active_hand(&self) {
@ -274,11 +289,14 @@ impl LivingEntity {
let clamped = health.max(0.0).min(max_health);
self.health.store(clamped);
// tell everyone entities health changed
self.entity.send_meta_data(&[Metadata::new(
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::HEALTH_ID,
MetaDataType::FLOAT,
clamped,
)]);
)],
None,
);
}
/// Returns the current maximum health for this entity
@ -328,8 +346,10 @@ impl LivingEntity {
// Send absorption metadata for players (visual yellow hearts)
if let Some(tracked_id) = self.player_absorption_id() {
self.entity
.send_meta_data(&[Metadata::new(tracked_id, MetaDataType::FLOAT, new_abs)]);
self.entity.send_meta_data(
&[Metadata::new(tracked_id, MetaDataType::FLOAT, new_abs)],
None,
);
}
}
@ -1791,11 +1811,14 @@ impl LivingEntity {
// Clear any absorption
self.absorption.store(0.0);
// Send health metadata
self.entity.send_meta_data(&[Metadata::new(
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::HEALTH_ID,
MetaDataType::FLOAT,
max_health,
)]);
)],
None,
);
self.reset_effects_and_attributes().await;

View File

@ -86,14 +86,14 @@ impl CreeperEntity {
pub fn set_fuse_speed(&self, speed: i32) {
self.fuse_speed.store(speed, Ordering::Relaxed);
self.mob_entity
.living_entity
.entity
.send_meta_data(&[Metadata::new(
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::FUSE_ID,
MetaDataType::INTEGER,
VarInt(speed),
)]);
)],
None,
);
}
async fn explode(&self) {
@ -219,11 +219,14 @@ impl Mob for CreeperEntity {
);
self.ignited.store(true, Ordering::Relaxed);
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::IS_IGNITED,
MetaDataType::BOOLEAN,
true,
)]);
)],
None,
);
if player.gamemode.load() != pumpkin_util::GameMode::Creative {
// TODO: Handle DamageResult::Broken to broadcast item break and update player slot.

View File

@ -282,14 +282,14 @@ impl EndermanEntity {
pub fn set_angry(&self, angry: bool) {
self.angry.store(angry, Ordering::Relaxed);
self.mob_entity
.living_entity
.entity
.send_meta_data(&[Metadata::new(
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::CREEPY,
MetaDataType::BOOLEAN,
angry,
)]);
)],
None,
);
}
pub fn is_angry(&self) -> bool {
@ -298,27 +298,27 @@ impl EndermanEntity {
pub fn set_provoked(&self, provoked: bool) {
self.provoked.store(provoked, Ordering::Relaxed);
self.mob_entity
.living_entity
.entity
.send_meta_data(&[Metadata::new(
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::STARED_AT,
MetaDataType::BOOLEAN,
provoked,
)]);
)],
None,
);
}
pub fn set_carried_block(&self, block_state: Option<BlockStateId>) {
self.carried_block.store(block_state);
let value = block_state.map_or(VarInt(0), |id| VarInt(id.as_u16() as i32));
self.mob_entity
.living_entity
.entity
.send_meta_data(&[Metadata::new(
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::CARRY_STATE,
MetaDataType::OPTIONAL_BLOCK_STATE,
value,
)]);
)],
None,
);
}
pub fn get_carried_block(&self) -> Option<BlockStateId> {

View File

@ -165,11 +165,14 @@ impl MobEntity {
if new_b != old_b {
self.mob_flags.store(new_b, Ordering::Relaxed);
self.living_entity.entity.send_meta_data(&[Metadata::new(
self.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::MOB_FLAGS_ID,
MetaDataType::BYTE,
new_b,
)]);
)],
None,
);
}
}
@ -493,11 +496,14 @@ pub trait Mob: EntityBase + Send + Sync {
let entity = self.get_entity();
let is_baby = entity.age.load(std::sync::atomic::Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)]);
)],
None,
);
}
})
}

View File

@ -110,11 +110,14 @@ impl ShulkerEntity {
fn set_attach_face(&self, face: BlockDirection) {
self.attach_face.store(face as u8, Ordering::Relaxed);
let entity = &self.mob_entity.living_entity.entity;
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::ATTACH_FACE_ID,
MetaDataType::DIRECTION,
VarInt(face as i32),
)]);
)],
None,
);
}
pub fn get_raw_peek(&self) -> u8 {
@ -148,11 +151,14 @@ impl ShulkerEntity {
self.peek_amount.store(amount, Ordering::Relaxed);
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::PEEK_ID,
MetaDataType::BYTE,
amount,
)]);
)],
None,
);
}
pub fn is_closed(&self) -> bool {

View File

@ -179,11 +179,16 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any {
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(&[Metadata::new(
let mut bedrock_meta = EntityMetadata::new();
bedrock_meta.set_flag(entity_data_key::FLAGS, entity_data_flag::BABY as u8, true);
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)]);
)],
Some(&bedrock_meta),
);
}
})
}
@ -1015,20 +1020,59 @@ impl Entity {
/// Sets a custom name for the entity, typically used with nametags
pub fn set_custom_name(&self, name: TextComponent) {
self.custom_name.store(Arc::new(Some(name.clone())));
self.send_meta_data(&[Metadata::new(
let mut bedrock_meta = EntityMetadata::new();
bedrock_meta.set(
entity_data_key::NAME,
MetadataValue::String(name.clone().get_text()),
);
let visible = self.custom_name_visible.load(Ordering::Relaxed);
bedrock_meta.set_flag(
entity_data_key::FLAGS,
entity_data_flag::SHOW_NAME as u8,
visible,
);
bedrock_meta.set_flag(
entity_data_key::FLAGS,
entity_data_flag::ALWAYS_SHOW_NAME as u8,
visible,
);
self.send_meta_data(
&[Metadata::new(
TrackedData::CUSTOM_NAME,
MetaDataType::OPTIONAL_TEXT_COMPONENT,
Some(name),
)]);
)],
Some(&bedrock_meta),
);
}
pub fn set_custom_name_visible(&self, visible: bool) {
self.custom_name_visible.store(visible, Ordering::Relaxed);
self.send_meta_data(&[Metadata::new(
let mut bedrock_meta = EntityMetadata::new();
if let Some(name) = &**self.custom_name.load() {
bedrock_meta.set(
entity_data_key::NAME,
MetadataValue::String(name.clone().get_text()),
);
}
bedrock_meta.set_flag(
entity_data_key::FLAGS,
entity_data_flag::SHOW_NAME as u8,
visible,
);
bedrock_meta.set_flag(
entity_data_key::FLAGS,
entity_data_flag::ALWAYS_SHOW_NAME as u8,
visible,
);
self.send_meta_data(
&[Metadata::new(
TrackedData::CUSTOM_NAME_VISIBLE,
MetaDataType::BOOLEAN,
visible,
)]);
)],
Some(&bedrock_meta),
);
}
pub fn send_velocity(&self) {
@ -2383,11 +2427,19 @@ impl Entity {
// Only update and send metadata if the value changed
if new_frozen_ticks != old_frozen_ticks {
self.frozen_ticks.store(new_frozen_ticks, Ordering::Relaxed);
self.send_meta_data(&[Metadata::new(
let mut bedrock_meta = EntityMetadata::new();
bedrock_meta.set(
entity_data_key::FREEZING_EFFECT_STRENGTH,
MetadataValue::Float(new_frozen_ticks as f32),
);
self.send_meta_data(
&[Metadata::new(
TrackedData::TICKS_FROZEN,
MetaDataType::INTEGER,
VarInt(new_frozen_ticks),
)]);
)],
Some(&bedrock_meta),
);
}
// Vanilla parity: full-freeze damage is tick-phase based.
@ -2639,11 +2691,14 @@ impl Entity {
self.flags.fetch_and(!mask, Ordering::Relaxed) & !mask
};
self.send_meta_data(&[Metadata::new(
self.send_meta_data(
&[Metadata::new(
TrackedData::SHARED_FLAGS_ID,
MetaDataType::BYTE,
new_je_flags,
)]);
)],
None,
);
if let Some(bedrock_flag) = flag.to_bedrock() {
let (key, index) = if bedrock_flag >= 64 {
@ -2710,14 +2765,21 @@ impl Entity {
.play_sound(sound, SoundCategory::Neutral, &self.pos.load());
}
pub fn send_meta_data<T: Serialize>(&self, meta: &[Metadata<T>]) {
pub fn send_meta_data<T: Serialize>(
&self,
meta: &[Metadata<T>],
bedrock_meta: Option<&EntityMetadata>,
) {
let world = self.world.load();
let chunk_pos = self.chunk_pos.load();
for player in world.players.load().iter() {
if let ClientPlatform::Java(client) = player.client.as_ref() {
match player.client.as_ref() {
ClientPlatform::Java(client) => {
// Apply Chebyshev distance check
let center = player.get_entity().chunk_pos.load();
let view_distance = crate::world::chunker::get_view_distance(player).get() as i32;
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 buf = Vec::new();
@ -2731,6 +2793,26 @@ impl Entity {
));
}
}
ClientPlatform::Bedrock(client) => {
if let Some(bedrock_meta) = bedrock_meta {
let center = player.get_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) {
client.try_enqueue_packet(&CSetActorData {
actor_runtime_id: VarULong(self.entity_id as u64),
metadata: EntityMetadata(bedrock_meta.0.clone()),
synced_properties: PropertySyncData {
int_properties: std::collections::HashMap::new(),
float_properties: std::collections::HashMap::new(),
},
tick: VarULong(0),
});
}
}
}
}
}
}
@ -2744,11 +2826,16 @@ impl Entity {
self.bounding_box.store(aabb);
self.entity_dimension.store(dimension);
let pose = pose as i32;
self.send_meta_data(&[Metadata::new(
let mut bedrock_meta = EntityMetadata::new();
bedrock_meta.set(entity_data_key::POSE_INDEX, MetadataValue::Int(pose));
self.send_meta_data(
&[Metadata::new(
TrackedData::POSE,
MetaDataType::ENTITY_POSE,
VarInt(pose),
)]);
)],
Some(&bedrock_meta),
);
}
}

View File

@ -137,17 +137,23 @@ impl Mob for CatEntity {
let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)]);
)],
None,
);
}
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::CAT_VARIANT,
MetaDataType::CAT_VARIANT,
VarInt(self.variant.load(Ordering::Relaxed) as i32),
)]);
)],
None,
);
})
}
}

View File

@ -131,17 +131,23 @@ impl Mob for ChickenEntity {
let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new(
entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)]);
)],
None,
);
}
entity.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new(
entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
TrackedData::VARIANT,
MetaDataType::CHICKEN_VARIANT,
VarInt(self.variant.load(Ordering::Relaxed) as i32),
)]);
)],
None,
);
})
}

View File

@ -103,17 +103,23 @@ impl Mob for FrogEntity {
let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)]);
)],
None,
);
}
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::VARIANT,
MetaDataType::FROG_VARIANT,
VarInt(self.variant.load(Ordering::Relaxed) as i32),
)]);
)],
None,
);
})
}
}

View File

@ -80,14 +80,14 @@ impl SheepEntity {
fn set_packed_and_sync(&self, byte: u8) {
self.color_and_sheared.store(byte, Ordering::Relaxed);
self.mob_entity
.living_entity
.entity
.send_meta_data(&[Metadata::new(
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::WOOL_ID,
MetaDataType::BYTE,
byte as i8,
)]);
)],
None,
);
}
pub fn set_color(&self, color: u8) {

View File

@ -155,11 +155,14 @@ impl VillagerEntity {
};
// Send initial metadata
mob_arc.get_entity().send_meta_data(&[Metadata::new(
mob_arc.get_entity().send_meta_data(
&[Metadata::new(
TrackedData::VILLAGER_DATA,
MetaDataType::VILLAGER_DATA,
villager_data,
)]);
)],
None,
);
mob_arc
}
@ -210,11 +213,14 @@ impl VillagerEntity {
*villager_data = data;
old_profession
};
self.get_entity().send_meta_data(&[Metadata::new(
self.get_entity().send_meta_data(
&[Metadata::new(
TrackedData::VILLAGER_DATA,
MetaDataType::VILLAGER_DATA,
data,
)]);
)],
None,
);
if old_profession != data.profession {
self.generate_trades(data.profession_enum(), data.level.0)
@ -760,11 +766,14 @@ impl Mob for VillagerEntity {
if is_sleeping {
// Wake up if bed was broken
self.get_entity().set_pose(EntityPose::Standing);
self.get_entity().send_meta_data(&[Metadata::new(
self.get_entity().send_meta_data(
&[Metadata::new(
TrackedData::SLEEPING_POS_ID,
MetaDataType::OPTIONAL_BLOCK_POS,
None::<BlockPos>,
)]);
)],
None,
);
}
}
}
@ -857,11 +866,14 @@ impl Mob for VillagerEntity {
.await;
self.get_entity().set_pose(EntityPose::Sleeping);
self.get_entity().send_meta_data(&[Metadata::new(
self.get_entity().send_meta_data(
&[Metadata::new(
TrackedData::SLEEPING_POS_ID,
MetaDataType::OPTIONAL_BLOCK_POS,
Some(home_pos),
)]);
)],
None,
);
}
}
}
@ -877,11 +889,14 @@ impl Mob for VillagerEntity {
}
self.get_entity().set_pose(EntityPose::Standing);
self.get_entity().send_meta_data(&[Metadata::new(
self.get_entity().send_meta_data(
&[Metadata::new(
TrackedData::SLEEPING_POS_ID,
MetaDataType::OPTIONAL_BLOCK_POS,
None::<BlockPos>,
)]);
)],
None,
);
}
}

View File

@ -129,17 +129,23 @@ impl Mob for WolfEntity {
let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)]);
)],
None,
);
}
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::VARIANT,
MetaDataType::WOLF_VARIANT,
VarInt(self.variant.load(Ordering::Relaxed) as i32),
)]);
)],
None,
);
})
}
}

View File

@ -1592,11 +1592,14 @@ impl Player {
self.living_entity
.entity
.set_pos(bed_head_pos.to_f64().add_raw(0.5, 0.6875, 0.5));
self.get_entity().send_meta_data(&[Metadata::new(
self.get_entity().send_meta_data(
&[Metadata::new(
TrackedData::SLEEPING_POS_ID,
MetaDataType::OPTIONAL_BLOCK_POS,
Some(bed_head_pos),
)]);
)],
None,
);
self.get_entity().set_velocity(Vector3::default());
self.sleeping_since.store(Some(0));
@ -1710,11 +1713,14 @@ impl Player {
self.living_entity.entity.set_pose(EntityPose::Standing);
self.living_entity.entity.set_pos(self.position());
self.living_entity.entity.send_meta_data(&[Metadata::new(
self.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::SLEEPING_POS_ID,
MetaDataType::OPTIONAL_BLOCK_POS,
None::<BlockPos>,
)]);
)],
None,
);
self.set_stat(
statistics::StatisticCategory::Custom,
@ -2952,7 +2958,8 @@ impl Player {
/// Send the player's skin layers and used hand to all players.
pub fn send_client_information(&self) {
let config = self.config.load();
self.living_entity.entity.send_meta_data(&[
self.living_entity.entity.send_meta_data(
&[
Metadata::new(
TrackedData::PLAYER_MODE_CUSTOMISATION,
MetaDataType::BYTE,
@ -2963,7 +2970,9 @@ impl Player {
// MetaDataType::ARM,
// VarInt(config.main_hand as u8 as i32),
// ),
]);
],
None,
);
}
pub async fn can_harvest(&self, state: &BlockState, block: &'static Block) -> bool {

View File

@ -73,11 +73,14 @@ impl EntityBase for EggEntity {
let stack = self.item_stack.read().await;
// Sync the item stack so the client renders the correct color/variant
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::ITEM_STACK,
MetaDataType::ITEM_STACK,
&ItemStackSerializer::from(stack.clone()),
)]);
)],
None,
);
})
}

View File

@ -172,11 +172,14 @@ impl EntityBase for EyeOfEnder {
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async {
self.entity.send_meta_data(&[Metadata::new(
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::ITEM,
MetaDataType::ITEM_STACK,
&ItemStackSerializer::from(self.item_stack.lock().await.clone()),
)]);
)],
None,
);
})
}

View File

@ -68,11 +68,14 @@ impl FireworkRocketEntity {
};
// Set shooter metadata
rocket.entity.entity.send_meta_data(&[Metadata::new(
rocket.entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::ATTACHED_TO_TARGET,
MetaDataType::OPTIONAL_INT,
OptionalInt(Some(shooter.entity_id)),
)]);
)],
None,
);
rocket
}

View File

@ -207,11 +207,14 @@ impl FishingBobberEntity {
if ebb.intersects(&search_box) {
self.hooked_entity_id
.store(cand.get_entity().entity_id, Ordering::Relaxed);
entity.send_meta_data(&[Metadata::new(
entity.send_meta_data(
&[Metadata::new(
TrackedData::HOOKED_ENTITY,
MetaDataType::INT,
cand.get_entity().entity_id + 1,
)]);
)],
None,
);
return;
}
}

View File

@ -69,13 +69,16 @@ impl EntityBase for LingeringPotionEntity {
let stack = self.item_stack.read().await;
// Sync the item stack so the client renders the correct potion type
entity.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new(
entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
pumpkin_data::tracked_data::TrackedData::ITEM_STACK,
pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK,
&pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from(
stack.clone(),
),
)]);
)],
None,
);
})
}

View File

@ -107,13 +107,16 @@ impl EntityBase for SplashPotionEntity {
let stack = self.item_stack.read().await;
// Sync the item stack
entity.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new(
entity.send_meta_data(
&[pumpkin_protocol::java::client::play::Metadata::new(
pumpkin_data::tracked_data::TrackedData::ITEM_STACK,
pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK,
&pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from(
stack.clone(),
),
)]);
)],
None,
);
})
}

View File

@ -85,7 +85,8 @@ impl EntityBase for TNTEntity {
self.entity
.set_velocity(Vector3::new(-pos.sin() * 0.02, 0.2, -pos.cos() * 0.02));
self.entity.send_meta_data(&[
self.entity.send_meta_data(
&[
Metadata::new(
TrackedData::FUSE_ID,
MetaDataType::INTEGER,
@ -96,7 +97,9 @@ impl EntityBase for TNTEntity {
MetaDataType::BLOCK_STATE,
VarInt(i32::from(Block::TNT.default_state.id.as_u16())),
),
]);
],
None,
);
})
}

View File

@ -39,10 +39,13 @@ impl BoatEntity {
self.left_paddle_moving.store(left, Ordering::Relaxed);
self.right_paddle_moving.store(right, Ordering::Relaxed);
self.vehicle.entity.send_meta_data(&[
self.vehicle.entity.send_meta_data(
&[
Metadata::new(TrackedData::ID_PADDLE_LEFT, MetaDataType::BOOLEAN, left),
Metadata::new(TrackedData::ID_PADDLE_RIGHT, MetaDataType::BOOLEAN, right),
]);
],
None,
);
}
fn send_wobble_metadata(&self) {

View File

@ -64,7 +64,8 @@ impl VehicleEntity {
}
pub fn send_wobble_metadata(&self) {
self.entity.send_meta_data(&[
self.entity.send_meta_data(
&[
Metadata::new(
TrackedData::ID_HURT,
MetaDataType::INTEGER,
@ -75,12 +76,17 @@ impl VehicleEntity {
MetaDataType::INTEGER,
self.get_hurt_dir(),
),
]);
self.entity.send_meta_data(&[Metadata::new(
],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::ID_DAMAGE,
MetaDataType::FLOAT,
self.get_damage(),
)]);
)],
None,
);
}
pub async fn kill_and_drop_self(&self) {

View File

@ -141,11 +141,13 @@ fn find_stronghold(world: &Arc<World>, origin: BlockPos) -> Option<BlockPos> {
let generator = &level.world_gen;
let seed = level.seed.0;
let global_cache = generator.global_structure_cache()?;
find_nearest_structure(
origin,
&[&StructureSet::get("strongholds").unwrap().placement],
100, // max search radius in chunks, matches vanilla default
seed as i64,
&generator.global_structure_cache,
global_cache,
)
}

View File

@ -231,8 +231,8 @@ impl PumpkinServer {
});
}
let tcp_listener = if server.basic_config.java_edition {
let address = server.basic_config.java_edition_address;
let tcp_listener = if server.advanced_config.networking.java.enabled {
let address = server.advanced_config.networking.java.address;
// Setup the TCP server socket.
let listener = match TcpListener::bind(address).await {
Ok(l) => l,
@ -275,7 +275,7 @@ impl PumpkinServer {
let lan_broadcast = LANBroadcast::new(
&server.advanced_config.networking.lan_broadcast,
&server.basic_config,
&server.advanced_config.networking.java.motd,
);
server.spawn_task(lan_broadcast.start(addr));
}
@ -293,9 +293,9 @@ impl PumpkinServer {
});
};
let udp_socket = if server.basic_config.bedrock_edition {
let udp_socket = if server.advanced_config.networking.bedrock.enabled {
Some(Arc::new(
UdpSocket::bind(server.basic_config.bedrock_edition_address)
UdpSocket::bind(server.advanced_config.networking.bedrock.address)
.await
.expect("Failed to bind UDP Socket"),
))

View File

@ -110,34 +110,34 @@ async fn main() {
.color_named(NamedColor::Gold)
.to_pretty_console()
);
let basic_config = &pumpkin_server.server.basic_config;
let advanced_config = &pumpkin_server.server.advanced_config;
info!(
"Server is now running. Connect using port: {}{}{}",
if basic_config.java_edition {
if advanced_config.networking.java.enabled {
format!(
"{} {}",
TextComponent::text("Java Edition:")
.color_named(NamedColor::Yellow)
.to_pretty_console(),
TextComponent::text(format!("{}", basic_config.java_edition_address))
TextComponent::text(format!("{}", advanced_config.networking.java.address))
.color_named(NamedColor::DarkBlue)
.to_pretty_console()
)
} else {
TextComponent::text(String::new()).to_pretty_console()
},
if basic_config.java_edition && basic_config.bedrock_edition {
if advanced_config.networking.java.enabled && advanced_config.networking.bedrock.enabled {
" | " // Separator if both are enabled
} else {
""
},
if basic_config.bedrock_edition {
if advanced_config.networking.bedrock.enabled {
format!(
"{} {}",
TextComponent::text("Bedrock Edition:")
.color_named(NamedColor::Gold)
.to_pretty_console(),
TextComponent::text(format!("{}", basic_config.bedrock_edition_address))
TextComponent::text(format!("{}", advanced_config.networking.bedrock.address))
.color_named(NamedColor::DarkBlue)
.to_pretty_console()
)

View File

@ -107,7 +107,8 @@ impl BedrockClient {
let compression = server
.advanced_config
.networking
.bedrock_compression
.bedrock
.compression
.info
.clone();
self.send_game_packet(&CNetworkSettings::new(
@ -136,13 +137,19 @@ impl BedrockClient {
server: &Server,
) -> Result<Option<PacketHandlerResult>, LoginError> {
let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?;
let player_data = if server.basic_config.online_mode {
let player_data = if server.advanced_config.networking.bedrock.online_mode {
match auth_payload.authentication_type {
AuthenticationType::Full => {
verify_oidc_token_path(server, &auth_payload.token, false)?
}
AuthenticationType::SelfSigned => {
if server.advanced_config.networking.authentication.enabled {
if server
.advanced_config
.networking
.bedrock
.authentication
.enabled
{
return Err(LoginError::SelfSignedNotAllowed);
}
@ -181,7 +188,7 @@ impl BedrockClient {
profile_actions: None,
};
if server.basic_config.bedrock_encryption {
if server.advanced_config.networking.bedrock.encryption {
let client_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token)
.map_err(LoginError::ChainValidationFailed)?;

View File

@ -1290,7 +1290,7 @@ impl BedrockClient {
.as_ref()
.map_or(0, |p| p.online) as u32
};
if player_count >= server.basic_config.max_players {
if player_count >= server.advanced_config.networking.bedrock.max_players {
Self::send_offline_packet(
&CNoFreeIncomingConnections::new(server.server_guid),
addr,

View File

@ -3,7 +3,12 @@ use std::{
sync::{Arc, atomic::Ordering},
};
use pumpkin_data::{data_component_impl::EquipmentSlot, item_stack::ItemStack};
use pumpkin_data::{
data_component_impl::{
BlocksAttacksImpl, ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl,
},
item_stack::ItemStack,
};
use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler};
use pumpkin_macros::send_cancellable;
use pumpkin_protocol::bedrock::{
@ -37,7 +42,7 @@ use pumpkin_protocol::{
codec::{var_int::VarInt, var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong},
java::client::play::{Animation, CEntityAnimation, CSetSelectedSlot, CSystemChatMessage},
};
use pumpkin_util::{GameMode, math::position::BlockPos, text::TextComponent};
use pumpkin_util::{GameMode, Hand, math::position::BlockPos, text::TextComponent};
use pumpkin_world::inventory::Inventory;
use pumpkin_world::world::BlockFlags;
@ -47,8 +52,10 @@ use crate::{
entity::{EntityBase, player::Player},
net::{DisconnectReason, bedrock::BedrockClient},
plugin::player::{
item_held::PlayerItemHeldEvent, player_chat::PlayerChatEvent,
item_held::PlayerItemHeldEvent,
player_chat::PlayerChatEvent,
player_command_send::PlayerCommandSendEvent,
player_interact_event::{InteractAction, PlayerInteractEvent},
player_toggle_flight_event::PlayerToggleFlightEvent,
},
server::{Server, seasonal_events},
@ -152,8 +159,10 @@ impl BedrockClient {
}
let server = player.world().server.upgrade().unwrap();
let view_distance =
chunk_radius.clamp(2, NonZeroI32::from(server.basic_config.view_distance).get());
let view_distance = chunk_radius.clamp(
2,
NonZeroI32::from(server.advanced_config.networking.bedrock.view_distance).get(),
);
self.enqueue_packet(&CChunkRadiusUpdate {
chunk_radius: VarInt(view_distance),
@ -733,6 +742,105 @@ impl BedrockClient {
}
}
}
} else if data.action_type.0 == 1 {
// Click air / Use item
let is_creative = player.gamemode.load() == GameMode::Creative;
let client_stack = descriptor_to_stack(&data.item_in_hand, is_creative);
let held_item = player.inventory.held_item();
if !client_stack.is_empty() {
let mut server_stack = held_item.lock().await;
if server_stack.is_empty() || server_stack.item.id != client_stack.item.id {
*server_stack = client_stack.clone();
}
}
let event = PlayerInteractEvent::new(
player,
InteractAction::RightClickAir,
&pumpkin_data::Block::AIR,
None,
);
let stack_for_use = held_item.lock().await.clone();
{
let mut held = held_item.lock().await;
let mut cooldown_active = false;
if let Some(cooldown) = held.get_use_cooldown() {
let group = cooldown
.cooldown_group
.clone()
.unwrap_or_else(|| held.item.registry_key.to_string());
if player.is_on_cooldown(&group).await {
cooldown_active = true;
}
}
if !cooldown_active {
if held.get_data_component::<ConsumableImpl>().is_some()
|| held.get_data_component::<BlocksAttacksImpl>().is_some()
{
if let Some(food) = held.get_data_component::<FoodImpl>() {
if player.abilities.lock().await.invulnerable
|| food.can_always_eat
|| player.hunger_manager.level.load() < 20
{
player
.living_entity
.set_active_hand(
Hand::Left,
held.clone(),
held.get_max_use_time(),
)
.await;
}
} else {
player
.living_entity
.set_active_hand(
Hand::Left,
held.clone(),
held.get_max_use_time(),
)
.await;
}
}
if let Some(equippable) = held.get_data_component::<EquippableImpl>() {
let inventory = player.inventory();
if !inventory
.is_already_equipped(&held_item, equippable.slot)
.await
{
player
.enqueue_equipment_change(equippable.slot, &held)
.await;
let binding = {
let mut equipment = inventory.entity_equipment.lock().await;
equipment.get_or_insert(equippable.slot)
};
let mut equip_item = binding.lock().await;
if equip_item.is_empty() {
*equip_item = held.clone();
held.decrement_unless_creative(player.gamemode.load(), 1);
} else {
let binding = held.clone();
*held = equip_item.clone();
*equip_item = binding;
}
}
}
}
}
send_cancellable! {{
server;
event;
'after: {
server.item_registry.on_use(&stack_for_use, player).await;
}
}}
}
}
TransactionData::UseItemOnEntity(data) => {

View File

@ -30,12 +30,12 @@ impl BedrockClient {
let motd_string = ServerInfo {
edition: "MCPE",
// TODO The default motd is to long to be displayed completely
motd_line_1: "Pumpkin Server",
motd_line_1: server.advanced_config.networking.bedrock.motd.clone(),
protocol_version: CURRENT_BEDROCK_MC_PROTOCOL,
version_name: CURRENT_BEDROCK_MC_VERSION,
player_count,
// A large number looks wreird on the client worlds window
max_player_count: server.basic_config.max_players,
max_player_count: server.advanced_config.networking.bedrock.max_players,
server_unique_id: server.server_guid,
motd_line_2: server.basic_config.default_level_name.clone(),
game_mode: server.defaultgamemode.lock().await.gamemode.to_str(),

View File

@ -34,7 +34,7 @@ impl JavaClient {
// Don't allow new logons when the server is full.
// If `max_players` is set to zero, then there is no max player count enforced.
// TODO: If client is an operator or has otherwise suitable elevated permissions, allow the client to bypass this requirement.
let max_players = server.basic_config.max_players;
let max_players = server.advanced_config.networking.java.max_players;
if max_players > 0 && server.get_player_count() >= max_players as usize {
self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_SERVER_FULL,
@ -74,7 +74,7 @@ impl JavaClient {
}
}
} else {
let id = if server.basic_config.online_mode {
let id = if server.advanced_config.networking.java.online_mode {
login_start.uuid
} else {
offline_uuid(&login_start.name).expect("This is very not safe and bad")
@ -87,16 +87,19 @@ impl JavaClient {
profile_actions: None,
};
if server.advanced_config.networking.java_compression.enabled {
if server.advanced_config.networking.java.compression.enabled {
self.enable_compression(server).await;
}
if server.basic_config.encryption {
if server.advanced_config.networking.java.encryption {
let verify_token: [u8; 4] = rand::random();
// Wait until we have sent the encryption packet to the client
self.send_packet_now(
&server
.encryption_request(&verify_token, server.basic_config.online_mode)
.encryption_request(
&verify_token,
server.advanced_config.networking.java.online_mode,
)
.await,
)
.await;
@ -132,7 +135,7 @@ impl JavaClient {
return;
};
if server.basic_config.online_mode {
if server.advanced_config.networking.java.online_mode {
// Online mode auth
match self
.authenticate(server, &shared_secret, &profile.name)
@ -201,7 +204,8 @@ impl JavaClient {
let compression = server
.advanced_config
.networking
.java_compression
.java
.compression
.info
.clone();
// We want to wait until we have sent the compression packet to the client
@ -236,7 +240,7 @@ impl JavaClient {
username,
&hash,
&ip,
&server.advanced_config.networking.authentication,
&server.advanced_config.networking.java.authentication,
)?;
// Check if the player should join
@ -244,6 +248,7 @@ impl JavaClient {
if server
.advanced_config
.networking
.java
.authentication
.player_profile
.allow_banned_players
@ -251,6 +256,7 @@ impl JavaClient {
for allowed in &server
.advanced_config
.networking
.java
.authentication
.player_profile
.allowed_actions
@ -270,7 +276,12 @@ impl JavaClient {
for property in profile.properties.load().iter() {
authentication::validate_textures(
property,
&server.advanced_config.networking.authentication.textures,
&server
.advanced_config
.networking
.java
.authentication
.textures,
)
.map_err(AuthError::TextureError)?;
}

View File

@ -1,4 +1,4 @@
use pumpkin_config::{BasicConfiguration, LANBroadcastConfig};
use pumpkin_config::LANBroadcastConfig;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::atomic::Ordering;
use std::time::Duration;
@ -23,7 +23,7 @@ pub struct LANBroadcast {
impl LANBroadcast {
/// Creates a new LAN broadcast instance from the provided configuration
#[must_use]
pub fn new(config: &LANBroadcastConfig, basic_config: &BasicConfiguration) -> Self {
pub fn new(config: &LANBroadcastConfig, server_motd: &str) -> Self {
let port = config.port.unwrap_or(0);
let advanced_motd = config.motd.clone().unwrap_or_default();
@ -32,7 +32,7 @@ impl LANBroadcast {
warn!(
"Using the server MOTD as the LAN broadcast MOTD. Note that the LAN broadcast MOTD does not support multiple lines, RGB colors, or gradients so consider defining it accordingly."
);
basic_config.motd.replace('\n', " ")
server_motd.replace('\n', " ")
} else {
advanced_motd
};

View File

@ -87,6 +87,7 @@ pub async fn start_query_handler(server: Arc<Server>, query_addr: SocketAddr) {
// Errors of packets that don't meet the format aren't returned since we won't handle them anyway
// The only errors that are thrown are because of a null terminator in a CString
// since those errors need to be corrected by server owner
#[expect(clippy::too_many_lines)]
#[inline]
async fn handle_packet(
buf: Vec<u8>,
@ -156,7 +157,9 @@ async fn handle_packet(
let response = CFullStatus {
session_id: packet.session_id,
hostname: CString::new(server.basic_config.motd.as_str())?,
hostname: CString::new(
server.advanced_config.networking.java.motd.as_str(),
)?,
version: CString::new(CURRENT_MC_VERSION)?,
plugins: CString::new(plugins)?,
map: CString::new(
@ -167,7 +170,8 @@ async fn handle_packet(
.map_or("world", |w| w.get_world_name()),
)?,
num_players: server.get_player_count(),
max_players: server.basic_config.max_players as usize,
max_players: server.advanced_config.networking.java.max_players
as usize,
host_port: bound_addr.port(),
host_ip: CString::new(bound_addr.ip().to_string())?,
players,
@ -179,7 +183,9 @@ async fn handle_packet(
} else {
let response = CBasicStatus {
session_id: packet.session_id,
motd: CString::new(server.basic_config.motd.as_str())?,
motd: CString::new(
server.advanced_config.networking.java.motd.as_str(),
)?,
map: CString::new(
server
.worlds
@ -188,7 +194,8 @@ async fn handle_packet(
.map_or("world", |w| w.get_world_name()),
)?,
num_players: server.get_player_count(),
max_players: server.basic_config.max_players as usize,
max_players: server.advanced_config.networking.java.max_players
as usize,
host_port: bound_addr.port(),
host_ip: CString::new(bound_addr.ip().to_string())?,
};

View File

@ -292,7 +292,7 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.max_players)
Ok(server.advanced_config.networking.java.max_players)
}
async fn is_hardcore(&mut self, _rep: Resource<Server>) -> wasmtime::Result<bool> {
@ -310,7 +310,12 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.advanced_config.networking.authentication.enabled)
Ok(server
.advanced_config
.networking
.java
.authentication
.enabled)
}
async fn get_motd(&mut self, _rep: Resource<Server>) -> wasmtime::Result<String> {
@ -319,7 +324,7 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.motd.clone())
Ok(server.advanced_config.networking.java.motd.clone())
}
async fn has_whitelist(&mut self, _rep: Resource<Server>) -> wasmtime::Result<bool> {
@ -355,7 +360,7 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.view_distance.get())
Ok(server.advanced_config.networking.java.view_distance.get())
}
async fn get_simulation_distance(&mut self, _rep: Resource<Server>) -> wasmtime::Result<u8> {
@ -364,7 +369,12 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?;
Ok(server.basic_config.simulation_distance.get())
Ok(server
.advanced_config
.networking
.java
.simulation_distance
.get())
}
async fn get_default_gamemode(

View File

@ -68,8 +68,8 @@ impl<'a> CachedBranding {
impl CachedStatus {
#[must_use]
pub fn new(config: &BasicConfiguration) -> Self {
let status_response = Self::build_response(config);
pub fn new(config: &BasicConfiguration, motd: &str, max_players: u32) -> Self {
let status_response = Self::build_response(config, motd, max_players);
Self {
status_response,
@ -135,7 +135,11 @@ impl CachedStatus {
}
}
pub fn build_response(config: &BasicConfiguration) -> StatusResponse {
pub fn build_response(
config: &BasicConfiguration,
motd: &str,
max_players: u32,
) -> StatusResponse {
let favicon = if config.use_favicon {
config.favicon_path.as_ref().map_or_else(
|| {
@ -185,11 +189,11 @@ impl CachedStatus {
protocol: LOWEST_SUPPORTED_MC_VERSION.protocol_version() as u32,
}),
players: Some(Players {
max: config.max_players,
max: max_players,
online: 0,
sample: vec![],
}),
description: config.motd.clone(),
description: motd.to_string(),
favicon,
// This should stay true even when reports are disabled.
// It prevents the annoying popup when joining the server.
@ -200,6 +204,10 @@ impl CachedStatus {
impl Default for CachedStatus {
fn default() -> Self {
Self::new(&BasicConfiguration::default())
Self::new(
&BasicConfiguration::default(),
"A blazingly fast Pumpkin server!",
1000,
)
}
}

View File

@ -199,7 +199,11 @@ impl Server {
let seed = level_info.world_gen_settings.seed;
let level_info = Arc::new(ArcSwap::new(Arc::new(level_info)));
let listing = Mutex::new(CachedStatus::new(&basic_config));
let listing = Mutex::new(CachedStatus::new(
&basic_config,
&advanced_config.networking.java.motd,
advanced_config.networking.java.max_players,
));
let defaultgamemode = Mutex::new(DefaultGamemode {
gamemode: basic_config.default_gamemode,
});
@ -218,7 +222,7 @@ impl Server {
let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps));
let mojang_keys_task = tokio::spawn({
let auth_config = advanced_config.networking.authentication.clone();
let auth_config = advanced_config.networking.java.authentication.clone();
let allow_chat = basic_config.allow_chat_reports;
async move {
if allow_chat {
@ -351,14 +355,24 @@ impl Server {
info!("All worlds loaded successfully.");
if server.basic_config.online_mode {
if server.advanced_config.networking.bedrock.online_mode {
let server_clone = server.clone();
tokio::spawn(async move {
server_clone
.bedrock_oidc_keys
.get_or_init(|| async {
tokio::task::block_in_place(|| {
pumpkin_util::jwt::fetch_oidc_jwks().unwrap_or_else(|e| {
let auth = &server_clone
.advanced_config
.networking
.bedrock
.authentication;
pumpkin_util::jwt::fetch_oidc_jwks(
auth.url.as_deref(),
auth.connect_timeout,
auth.read_timeout,
)
.unwrap_or_else(|e| {
error!("Failed to fetch Bedrock OIDC keys: {e}");
(String::new(), pumpkin_util::jwt::Jwks { keys: Vec::new() })
})

View File

@ -14,10 +14,15 @@ use crate::{
pub fn get_view_distance(player: &Player) -> NonZeroU8 {
let server = player.world().server.upgrade().unwrap();
player.config.load().view_distance.clamp(
NonZeroU8::new(2).unwrap(),
server.basic_config.view_distance,
)
let max_view_distance = match player.client.as_ref() {
ClientPlatform::Java(_) => server.advanced_config.networking.java.view_distance,
ClientPlatform::Bedrock(_) => server.advanced_config.networking.bedrock.view_distance,
};
player
.config
.load()
.view_distance
.clamp(NonZeroU8::new(2).unwrap(), max_view_distance)
}
// Checks if the target chunk is within the view distance

View File

@ -1849,7 +1849,13 @@ impl World {
has_start_with_map_enabled: false,
// TODO Bedrock permission level are different
permission_level: VarInt(2),
server_simulation_distance: base_config.simulation_distance.get().into(),
server_simulation_distance: server
.advanced_config
.networking
.bedrock
.simulation_distance
.get()
.into(),
has_locked_behavior_pack: false,
has_locked_resource_pack: false,
is_from_locked_world_template: false,
@ -2535,9 +2541,27 @@ impl World {
entity_id,
base_config.hardcore,
dimensions,
base_config.max_players.try_into().unwrap(),
base_config.view_distance.get().into(), // TODO: view distance
base_config.simulation_distance.get().into(), // TODO: sim view dinstance
server
.advanced_config
.networking
.java
.max_players
.try_into()
.unwrap(),
server
.advanced_config
.networking
.java
.view_distance
.get()
.into(), // TODO: view distance
server
.advanced_config
.networking
.java
.simulation_distance
.get()
.into(), // TODO: sim view dinstance
false,
true,
false,
@ -2555,7 +2579,7 @@ impl World {
VarInt(player.get_entity().portal_cooldown.load(Ordering::Relaxed) as i32),
self.sea_level.into(),
),
base_config.online_mode,
server.advanced_config.networking.java.online_mode,
// This should stay true even when reports are disabled.
// It prevents the annoying popup when joining the server.
true,