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. and customizable experience. It prioritizes performance and player enjoyment while adhering to the core mechanics of the game.
<div align="center"> <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> </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. /// Configuration for chunk storage format.
/// ///
/// Supports multiple chunk formats, currently `Anvil` and `Linear`. /// Supports multiple chunk formats, currently `Anvil` and `Linear`.
#[derive(Deserialize, Default, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ChunkConfig { pub enum ChunkConfig {
/// Standard Anvil chunk storage. /// Standard Anvil chunk storage.
@ -16,10 +16,15 @@ pub enum ChunkConfig {
Linear, Linear,
/// Pumpkin's own optimized world format. /// Pumpkin's own optimized world format.
#[serde(rename = "pump")] #[serde(rename = "pump")]
#[default]
Pump, Pump,
} }
impl Default for ChunkConfig {
fn default() -> Self {
Self::Anvil(AnvilChunkConfig::default())
}
}
/// Configuration for Anvil chunk storage. /// Configuration for Anvil chunk storage.
#[derive(Deserialize, Serialize, Default, Clone)] #[derive(Deserialize, Serialize, Default, Clone)]
#[serde(default)] #[serde(default)]

View File

@ -5,7 +5,6 @@ use pumpkin_util::{Difficulty, GameMode, PermissionLvl, random};
use recipe::RecipeConfig; use recipe::RecipeConfig;
use serde::{Deserialize, Serialize, de::DeserializeOwned}; use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
use std::{fs, num::NonZeroU8, path::Path}; use std::{fs, num::NonZeroU8, path::Path};
use tracing::{debug, warn}; use tracing::{debug, warn};
@ -20,7 +19,9 @@ pub mod resource_pack;
pub use chat::ChatConfig; pub use chat::ChatConfig;
pub use commands::CommandsConfig; pub use commands::CommandsConfig;
pub use networking::auth::AuthenticationConfig; pub use networking::auth::AuthenticationConfig;
pub use networking::bedrock::BedrockConfig;
pub use networking::compression::CompressionConfig; pub use networking::compression::CompressionConfig;
pub use networking::java::JavaConfig;
pub use networking::lan_broadcast::LANBroadcastConfig; pub use networking::lan_broadcast::LANBroadcastConfig;
pub use networking::rcon::RCONConfig; pub use networking::rcon::RCONConfig;
pub use plugins::PluginsConfig; pub use plugins::PluginsConfig;
@ -64,6 +65,48 @@ impl LoadConfiguration for PumpkinConfig {
fn validate(&self) { fn validate(&self) {
self.basic.validate(); self.basic.validate();
self.advanced.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)] #[derive(Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct BasicConfiguration { 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. /// The seed for the world generation.
pub seed: Seed, 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. /// The default game difficulty.
pub default_difficulty: Difficulty, pub default_difficulty: Difficulty,
/// The op level assigned by the /op command. /// The op level assigned by the /op command.
@ -138,12 +165,6 @@ pub struct BasicConfiguration {
pub allow_end: bool, pub allow_end: bool,
/// Whether the server is in hardcore mode. /// Whether the server is in hardcore mode.
pub hardcore: bool, 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. /// The server's ticks per second.
pub tps: f32, pub tps: f32,
/// The default gamemode for players. /// The default gamemode for players.
@ -169,23 +190,12 @@ pub struct BasicConfiguration {
impl Default for BasicConfiguration { impl Default for BasicConfiguration {
fn default() -> Self { fn default() -> Self {
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()), seed: Seed(random::get_seed()),
max_players: 1000,
view_distance: NonZeroU8::new(16).unwrap(),
simulation_distance: NonZeroU8::new(10).unwrap(),
default_difficulty: Difficulty::Normal, default_difficulty: Difficulty::Normal,
op_permission_level: PermissionLvl::Four, op_permission_level: PermissionLvl::Four,
allow_nether: true, allow_nether: true,
allow_end: true, allow_end: true,
hardcore: false, hardcore: false,
online_mode: true,
encryption: true,
motd: "A blazingly fast Pumpkin server!".to_string(),
tps: 20.0, tps: 20.0,
default_gamemode: GameMode::Survival, default_gamemode: GameMode::Survival,
force_gamemode: false, force_gamemode: false,
@ -207,35 +217,7 @@ impl BasicConfiguration {
PathBuf::from(&self.default_level_name) PathBuf::from(&self.default_level_name)
} }
pub fn validate(&self) { pub const 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"
);
}
}
} }
impl AdvancedConfiguration { 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. /// Configuration for packet compression.
/// ///
/// Controls whether network packet compression is enabled and the compression parameters. /// Controls whether network packet compression is enabled and the compression parameters.
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize, Clone)]
#[serde(default)] #[serde(default)]
pub struct CompressionConfig { pub struct CompressionConfig {
/// Whether compression is enabled. /// 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 proxy::ProxyConfig;
use query::QueryConfig; use query::QueryConfig;
use rcon::RCONConfig; use rcon::RCONConfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{CompressionConfig, LANBroadcastConfig}; use crate::LANBroadcastConfig;
use bedrock::BedrockConfig;
use java::JavaConfig;
pub mod auth; pub mod auth;
pub mod bedrock;
pub mod compression; pub mod compression;
pub mod java;
pub mod lan_broadcast; pub mod lan_broadcast;
pub mod proxy; pub mod proxy;
pub mod query; pub mod query;
@ -18,19 +21,18 @@ pub mod rcon;
/// Covers authentication, query, RCON, proxying, packet compression, /// Covers authentication, query, RCON, proxying, packet compression,
/// and LAN broadcast behaviour. /// and LAN broadcast behaviour.
#[derive(Deserialize, Serialize, Default)] #[derive(Deserialize, Serialize, Default)]
#[serde(default)]
pub struct NetworkingConfig { pub struct NetworkingConfig {
/// Authentication settings for client connections.
pub authentication: AuthenticationConfig,
/// Query protocol settings for server status requests. /// Query protocol settings for server status requests.
pub query: QueryConfig, pub query: QueryConfig,
/// RCON (remote console) configuration. /// RCON (remote console) configuration.
pub rcon: RCONConfig, pub rcon: RCONConfig,
/// Proxy-related networking settings. /// Proxy-related networking settings.
pub proxy: ProxyConfig, pub proxy: ProxyConfig,
/// Java Edition packet compression settings.
pub java_compression: CompressionConfig,
/// Bedrock Edition packet compression settings.
pub bedrock_compression: CompressionConfig,
/// LAN broadcast settings. /// LAN broadcast settings.
pub lan_broadcast: LANBroadcastConfig, 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 { pub struct ServerInfo {
/// (BE or MCEE for Education Edition) /// (BE or MCEE for Education Edition)
pub edition: &'static str, pub edition: &'static str,
pub motd_line_1: &'static str, pub motd_line_1: String,
pub protocol_version: u32, pub protocol_version: u32,
pub version_name: &'static str, pub version_name: &'static str,
pub player_count: i32, pub player_count: i32,

View File

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

View File

@ -3,7 +3,7 @@ use std::num::NonZeroI32;
use pumpkin_data::item::{BedrockItem, JavaToBedrockItemMapping}; use pumpkin_data::item::{BedrockItem, JavaToBedrockItemMapping};
use pumpkin_data::item_stack::ItemStack; use pumpkin_data::item_stack::ItemStack;
use pumpkin_nbt::Nbt; use pumpkin_nbt::{Nbt, deserializer::NbtReadHelperBedrock};
use crate::{ use crate::{
codec::{var_int::VarInt, var_uint::VarUInt}, 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]; let mut user_data = vec![0u8; user_data_len as usize];
buf.read_exact(&mut user_data)?; 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 { Ok(Self {
id, id,
stack_size, stack_size,
aux_value, aux_value,
block_runtime_id, 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]; let mut user_data = vec![0u8; user_data_len as usize];
buf.read_exact(&mut user_data)?; 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 { Ok(Self {
id, id,
stack_size, stack_size,
aux_value, aux_value,
block_runtime_id, block_runtime_id,
nbt_data,
place_on_blocks,
destroy_blocks,
shield_blocking_tick,
net_id, net_id,
..Default::default()
}) })
} }
} }

View File

@ -58,7 +58,7 @@ impl ClientPacket for CSetEntityMetadata {
pub struct Metadata<T> { pub struct Metadata<T> {
pub index: TrackedId, pub index: TrackedId,
pub r#type: MetaDataType, pub r#type: MetaDataType,
value: T, pub value: T,
} }
impl<T> Metadata<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 = pub const OIDC_DISCOVERY_URL: &str =
"https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/1.0.0.0"; "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> { pub fn fetch_oidc_jwks(
let discovery: Value = ureq::get(OIDC_DISCOVERY_URL) 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() .call()
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))? .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
.body_mut() .body_mut()
@ -248,7 +264,8 @@ pub fn fetch_oidc_jwks() -> Result<(String, Jwks), AuthError> {
.ok_or_else(|| AuthError::PublicKeyBuild("Discovery missing serviceUri".into()))?; .ok_or_else(|| AuthError::PublicKeyBuild("Discovery missing serviceUri".into()))?;
let openid_config_url = format!("{service_uri}/.well-known/openid-configuration"); 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() .call()
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))? .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
.body_mut() .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()))? .ok_or_else(|| AuthError::PublicKeyBuild("OpenID config missing issuer".into()))?
.to_string(); .to_string();
let jwks: Jwks = ureq::get(jwks_uri) let jwks: Jwks = agent
.get(jwks_uri)
.call() .call()
.map_err(|e| AuthError::PublicKeyBuild(e.to_string()))? .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))?
.body_mut() .body_mut()

View File

@ -5,7 +5,7 @@ use pumpkin_data::dimension::Dimension;
use pumpkin_util::world_seed::Seed; use pumpkin_util::world_seed::Seed;
use pumpkin_world::ProtoChunk; use pumpkin_world::ProtoChunk;
use pumpkin_world::chunk_system::{Cache, Chunk, StagedChunkEnum, generate_single_chunk}; 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::generation::get_world_gen;
use pumpkin_world::world::WorldPortalExt; use pumpkin_world::world::WorldPortalExt;
use std::hint::black_box; use std::hint::black_box;
@ -13,7 +13,6 @@ use std::sync::Arc;
const SEED: Seed = Seed(42); const SEED: Seed = Seed(42);
// Stub portal — allows all block placements, skips mob spawning.
struct BlockRegistry; struct BlockRegistry;
impl WorldPortalExt for BlockRegistry { impl WorldPortalExt for BlockRegistry {
fn can_place_at( 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 fn make_world_gen() -> Box<WorldGenerator> {
/// and lighting stages get a 3×3 cache. get_world_gen(SEED, Dimension::OVERWORLD, false, Vec::new(), String::new())
}
fn setup_cache( fn setup_cache(
target_stage: StagedChunkEnum, target_stage: StagedChunkEnum,
world_gen: &VanillaGenerator, world_gen: &WorldGenerator,
block_registry: &dyn WorldPortalExt, block_registry: &dyn WorldPortalExt,
) -> Cache { ) -> Cache {
let radius = target_stage.get_direct_radius(); let radius = target_stage.get_direct_radius();
@ -97,10 +98,9 @@ fn setup_cache(
cache cache
} }
/// Full pipeline
fn bench_full_chunk_generation(c: &mut Criterion) { fn bench_full_chunk_generation(c: &mut Criterion) {
let dimension = Dimension::OVERWORLD; 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); let block_registry = Arc::new(BlockRegistry);
c.bench_function("full_chunk_generation", |b| { 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) { 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); let block_registry = Arc::new(BlockRegistry);
c.bench_function("biomes_generation", |b| { 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) { 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); let block_registry = Arc::new(BlockRegistry);
c.bench_function("structure_starts_generation", |b| { 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) { 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); let block_registry = Arc::new(BlockRegistry);
c.bench_function("structure_references_generation", |b| { 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) { 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); let block_registry = Arc::new(BlockRegistry);
c.bench_function("noise_generation", |b| { c.bench_function("noise_generation", |b| {
@ -216,7 +215,7 @@ fn bench_noise_generation(c: &mut Criterion) {
} }
fn bench_surface_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); let block_registry = Arc::new(BlockRegistry);
c.bench_function("surface_generation", |b| { c.bench_function("surface_generation", |b| {
@ -243,7 +242,7 @@ fn bench_surface_generation(c: &mut Criterion) {
} }
fn bench_carvers_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); let block_registry = Arc::new(BlockRegistry);
c.bench_function("carvers_generation", |b| { c.bench_function("carvers_generation", |b| {
@ -270,7 +269,7 @@ fn bench_carvers_generation(c: &mut Criterion) {
} }
fn bench_features_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); let block_registry = Arc::new(BlockRegistry);
c.bench_function("features_generation", |b| { c.bench_function("features_generation", |b| {
@ -297,7 +296,7 @@ fn bench_features_generation(c: &mut Criterion) {
} }
fn bench_lighting_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); let block_registry = Arc::new(BlockRegistry);
c.bench_function("lighting_generation", |b| { c.bench_function("lighting_generation", |b| {

View File

@ -63,7 +63,13 @@ fn bench_concurrent_chunk_generation(c: &mut Criterion) {
.build() .build()
.expect("Failed to build rayon thread pool"); .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 block_registry = Arc::new(BlockRegistry);
let bench_name = format!( let bench_name = format!(

View File

@ -79,7 +79,7 @@ mod test {
#[test] #[test]
fn wide_area_surface() { 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::{ use crate::generation::noise::router::multi_noise_sampler::{
MultiNoiseSampler, MultiNoiseSamplerBuilderOptions, MultiNoiseSampler, MultiNoiseSamplerBuilderOptions,
}; };
@ -96,13 +96,19 @@ mod test {
read_data_from_file!("../../assets/biome_no_blend_no_beard_0.json"); read_data_from_file!("../../assets/biome_no_blend_no_beard_0.json");
let seed = 0; 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 { for data in expected_data {
let chunk_x = data.x; let chunk_x = data.x;
let chunk_z = data.z; 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 // Create MultiNoiseSampler for populate_biomes

View File

@ -45,7 +45,7 @@ mod test {
#[test] #[test]
fn sample_value() { fn sample_value() {
use crate::generation::generator::{GeneratorInit, VanillaGenerator}; use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator};
use crate::generation::noise::router::multi_noise_sampler::{ use crate::generation::noise::router::multi_noise_sampler::{
MultiNoiseSampler, MultiNoiseSamplerBuilderOptions, MultiNoiseSampler, MultiNoiseSamplerBuilderOptions,
}; };
@ -59,9 +59,16 @@ mod test {
let chunk_x = 0; let chunk_x = 0;
let chunk_z = 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_x = chunk_pos::start_block_x(chunk_x);
let start_z = chunk_pos::start_block_z(chunk_z); let start_z = chunk_pos::start_block_z(chunk_z);

View File

@ -922,7 +922,7 @@ mod tests {
let _ = env_logger::try_init(); 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 temp_dir = TempDir::new().unwrap();
let level_folder = LevelFolder { let level_folder = LevelFolder {
@ -1198,7 +1198,7 @@ mod tests {
let _ = env_logger::try_init(); 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 temp_dir = TempDir::new().unwrap();
let level_folder = LevelFolder { let level_folder = LevelFolder {

View File

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

View File

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

View File

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

View File

@ -1272,7 +1272,7 @@ impl GenerationSchedule {
let send_chunk = self.send_chunk.clone(); let send_chunk = self.send_chunk.clone();
let level = level.clone(); let level = level.clone();
let settings = let settings =
GenerationSettings::from_dimension(&level.world_gen.dimension); GenerationSettings::from_dimension(level.world_gen.dimension());
pool.spawn(move || { pool.spawn(move || {
let result = crate::chunk_system::worker_logic::run_generation( 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::Level(chunk) => vec.push((pos, chunk)),
Chunk::Proto(chunk) => { Chunk::Proto(chunk) => {
let mut temp = 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!() }; let Chunk::Level(chunk) = temp else { panic!() };
vec.push((pos, chunk)); vec.push((pos, chunk));
} }
@ -262,7 +265,7 @@ pub fn generation_work(
send: &crossfire::compat::MTx<(ChunkPos, RecvChunk)>, send: &crossfire::compat::MTx<(ChunkPos, RecvChunk)>,
level: &Arc<Level>, level: &Arc<Level>,
) { ) {
let settings = GenerationSettings::from_dimension(&level.world_gen.dimension); let settings = GenerationSettings::from_dimension(level.world_gen.dimension());
loop { loop {
let Ok((pos, cache, stage)) = recv.recv() else { let Ok((pos, cache, stage)) = recv.recv() else {

View File

@ -365,11 +365,14 @@ fn with_carve_run_options<F>(
) where ) where
F: FnOnce(&mut CarveRun<'_, '_>), F: FnOnce(&mut CarveRun<'_, '_>),
{ {
use crate::generation::generator::{GeneratorInit, VanillaGenerator}; use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator};
use pumpkin_util::world_seed::Seed; use pumpkin_util::world_seed::Seed;
let generator = VanillaGenerator::new(Seed(42), dimension); let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(Seed(42), dimension)));
let mut chunk = ProtoChunk::new(0, 0, &generator); 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_x = crate::generation::positions::chunk_pos::start_block_x(chunk.x);
let start_z = crate::generation::positions::chunk_pos::start_block_z(chunk.z); 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 pumpkin_data::structures::{StructurePlacementCalculator, StructureSet};
use rustc_hash::FxHashMap; 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 struct VanillaGenerator {
pub random_config: GlobalRandomConfig, pub random_config: GlobalRandomConfig,
pub base_router: ProtoNoiseRouters, pub base_router: ProtoNoiseRouters,

View File

@ -25,9 +25,22 @@ use pumpkin_util::{
}; };
#[must_use] #[must_use]
pub fn get_world_gen(seed: Seed, dimension: Dimension) -> Box<VanillaGenerator> { pub fn get_world_gen(
// TODO decide which WorldGenerator to pick based on config. seed: Seed,
Box::new(VanillaGenerator::new(seed, dimension)) 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 { pub struct GlobalRandomConfig {

View File

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

View File

@ -1,7 +1,7 @@
use crate::chunk::format::linear::LinearV2File; use crate::chunk::format::linear::LinearV2File;
use crate::chunk::format::pump::PumpFile; use crate::chunk::format::pump::PumpFile;
use crate::chunk_system::{ChunkListener, ChunkLoading, GenerationSchedule, LevelChannel}; use crate::chunk_system::{ChunkListener, ChunkLoading, GenerationSchedule, LevelChannel};
use crate::generation::generator::VanillaGenerator; use crate::generation::generator::WorldGenerator;
use crate::lighting::DynamicLightEngine; use crate::lighting::DynamicLightEngine;
use crate::{ use crate::{
chunk::{ chunk::{
@ -77,7 +77,7 @@ pub struct Level {
pub chunk_saver: Arc<dyn FileIO<Data = SyncChunk>>, pub chunk_saver: Arc<dyn FileIO<Data = SyncChunk>>,
entity_saver: Arc<dyn FileIO<Data = SyncEntityChunk>>, entity_saver: Arc<dyn FileIO<Data = SyncEntityChunk>>,
pub world_gen: Arc<VanillaGenerator>, pub world_gen: Arc<WorldGenerator>,
/// Handles runtime lighting updates /// Handles runtime lighting updates
pub light_engine: DynamicLightEngine, pub light_engine: DynamicLightEngine,
@ -125,6 +125,7 @@ pub struct LevelFolder {
impl Level { impl Level {
#[must_use] #[must_use]
#[expect(clippy::too_many_lines)]
pub fn from_root_folder( pub fn from_root_folder(
level_config: &LevelConfig, level_config: &LevelConfig,
root_folder: PathBuf, root_folder: PathBuf,
@ -132,11 +133,18 @@ impl Level {
dimension: Dimension, dimension: Dimension,
gen_pool: Option<Arc<rayon::ThreadPool>>, gen_pool: Option<Arc<rayon::ThreadPool>>,
) -> Arc<Self> { ) -> 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(':') { let (namespace, name) = match dimension.minecraft_name.split_once(':') {
Some((ns, n)) => (ns, n), Some((ns, n)) => (ns, n),
None => ("minecraft", dimension.minecraft_name), 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 region_folder = dim_folder.join("region");
let entities_folder = dim_folder.join("entities"); let entities_folder = dim_folder.join("entities");
@ -154,8 +162,62 @@ impl Level {
poi_folder, 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 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 { let chunk_saver: Arc<dyn FileIO<Data = SyncChunk>> = match &level_config.chunk {
ChunkConfig::Linear => Arc::new(ChunkFileManager::<LinearV2File<ChunkData>>::new(())), ChunkConfig::Linear => Arc::new(ChunkFileManager::<LinearV2File<ChunkData>>::new(())),

View File

@ -1,3 +1,5 @@
#![allow(clippy::unreachable)]
use pumpkin_data::{ use pumpkin_data::{
Block, BlockState, chunk_gen_settings::GenerationSettings, dimension::Dimension, Block, BlockState, chunk_gen_settings::GenerationSettings, dimension::Dimension,
}; };
@ -57,15 +59,21 @@ pub fn bench_create_and_populate_noise(
_terrain_cache: &TerrainCache, _terrain_cache: &TerrainCache,
_default_block: &'static BlockState, _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::{ use crate::generation::noise::router::surface_height_sampler::{
SurfaceHeightEstimateSampler, SurfaceHeightSamplerBuilderOptions, SurfaceHeightEstimateSampler, SurfaceHeightSamplerBuilderOptions,
}; };
use crate::generation::proto_chunk::StandardChunkFluidLevelSampler; use crate::generation::proto_chunk::StandardChunkFluidLevelSampler;
use pumpkin_util::world_seed::Seed; use pumpkin_util::world_seed::Seed;
let generator = VanillaGenerator::new(Seed(random_config.seed), Dimension::OVERWORLD); let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(
let mut chunk = ProtoChunk::new(0, 0, &generator); 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 // Create noise sampler and other required components
let settings = generator.settings; let settings = generator.settings;
@ -119,7 +127,7 @@ pub fn bench_create_and_populate_noise(
); );
chunk.populate_noise( chunk.populate_noise(
&generator, generator,
&mut noise_sampler, &mut noise_sampler,
&generator.random_config.ore_random_deriver, &generator.random_config.ore_random_deriver,
&mut surface_height_estimate_sampler, &mut surface_height_estimate_sampler,
@ -133,15 +141,21 @@ pub fn bench_create_and_populate_biome(
_terrain_cache: &TerrainCache, _terrain_cache: &TerrainCache,
_default_block: &'static BlockState, _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::{ use crate::generation::noise::router::multi_noise_sampler::{
MultiNoiseSampler, MultiNoiseSamplerBuilderOptions, MultiNoiseSampler, MultiNoiseSamplerBuilderOptions,
}; };
use crate::generation::{biome_coords, positions::chunk_pos}; use crate::generation::{biome_coords, positions::chunk_pos};
use pumpkin_util::world_seed::Seed; use pumpkin_util::world_seed::Seed;
let generator = VanillaGenerator::new(Seed(random_config.seed), Dimension::OVERWORLD); let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(
let mut chunk = ProtoChunk::new(0, 0, &generator); 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 // Create multi-noise sampler
let start_x = chunk_pos::start_block_x(0); 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 = let mut multi_noise_sampler =
MultiNoiseSampler::generate(&generator.base_router.multi_noise, &multi_noise_config); 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( 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, _terrain_cache: &TerrainCache,
_default_block: &'static BlockState, _default_block: &'static BlockState,
) { ) {
use crate::generation::generator::{GeneratorInit, VanillaGenerator}; use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator};
use crate::generation::noise::router::{ use crate::generation::noise::router::{
multi_noise_sampler::{MultiNoiseSampler, MultiNoiseSamplerBuilderOptions}, multi_noise_sampler::{MultiNoiseSampler, MultiNoiseSamplerBuilderOptions},
surface_height_sampler::{ surface_height_sampler::{
@ -179,8 +193,14 @@ pub fn bench_create_and_populate_noise_with_surface(
use crate::generation::proto_chunk::StandardChunkFluidLevelSampler; use crate::generation::proto_chunk::StandardChunkFluidLevelSampler;
use pumpkin_util::world_seed::Seed; use pumpkin_util::world_seed::Seed;
let generator = VanillaGenerator::new(Seed(random_config.seed), Dimension::OVERWORLD); let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(
let mut chunk = ProtoChunk::new(0, 0, &generator); 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 // Create all required components
let settings = generator.settings; let settings = generator.settings;
@ -241,12 +261,12 @@ pub fn bench_create_and_populate_noise_with_surface(
&surface_config, &surface_config,
); );
chunk.populate_biomes(&generator, &mut multi_noise_sampler); chunk.populate_biomes(generator, &mut multi_noise_sampler);
chunk.populate_noise( chunk.populate_noise(
&generator, generator,
&mut noise_sampler, &mut noise_sampler,
&generator.random_config.ore_random_deriver, &generator.random_config.ore_random_deriver,
&mut surface_height_estimate_sampler, &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 /// Compress chunk data to bytes
fn compress_chunk_data(chunk_data: &PoiChunkData) -> std::io::Result<Vec<u8>> { fn compress_chunk_data(chunk_data: &PoiChunkData) -> std::io::Result<Vec<u8>> {
let mut uncompressed = Vec::new(); 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()))?; .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
@ -191,7 +191,15 @@ impl PoiRegion {
let mut uncompressed = Vec::new(); let mut uncompressed = Vec::new();
decoder.read_to_end(&mut uncompressed)?; 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)) pumpkin_nbt::from_bytes_unnamed(Cursor::new(uncompressed))
}
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) .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::{ use crate::world_info::{
MAXIMUM_SUPPORTED_LEVEL_VERSION, MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MAXIMUM_SUPPORTED_LEVEL_VERSION, MAXIMUM_SUPPORTED_WORLD_DATA_VERSION,
MINIMUM_SUPPORTED_LEVEL_VERSION, MINIMUM_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}; use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter};
@ -89,10 +95,43 @@ impl WorldInfoReader for AnvilLevelInfo {
check_file_data_version(&buf)?; check_file_data_version(&buf)?;
check_file_level_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()))?; .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) Ok(info.data)
} }
@ -112,20 +151,76 @@ impl WorldInfoWriter for AnvilLevelInfo {
level_data.last_played = since_the_epoch.as_millis() as i64; level_data.last_played = since_the_epoch.as_millis() as i64;
let level = LevelDat { data: level_data }; let level = LevelDat { data: level_data };
// open file // ── Write level.dat ───────────────────────────────────────────────────
let path = level_folder.join(LEVEL_DAT_FILE_NAME); let path = level_folder.join(LEVEL_DAT_FILE_NAME);
let world_info_file = File::create(path)?; let world_info_file = File::create(path)?;
// write compressed data into file
let compression_writer = GzEncoder::new(world_info_file, Compression::best()); let compression_writer = GzEncoder::new(world_info_file, Compression::best());
// TODO: Proper error handling // TODO: Proper error handling
pumpkin_nbt::to_bytes(&level, compression_writer) pumpkin_nbt::to_bytes(&level, compression_writer)
.expect("Failed to write level.dat to disk"); .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(()) Ok(())
} }
} }
#[derive(Debug, PartialEq, Serialize, Deserialize)] #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct LevelDat { pub struct LevelDat {
// This tag contains all the level data. // This tag contains all the level data.
#[serde(rename = "Data")] #[serde(rename = "Data")]
@ -267,7 +362,13 @@ mod test {
let level_dat_again: LevelDat = let level_dat_again: LevelDat =
from_bytes(Cursor::new(serialized)).expect("Failed to decode from bytes"); 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] #[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; use thiserror::Error;
pub mod anvil; pub mod anvil;
pub mod data_files;
// Constraint: disk biome palette serialization changed in 1.21.5 // Constraint: disk biome palette serialization changed in 1.21.5
pub const MINIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4435; // 1.21.9 pub const MINIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4435; // 1.21.9
@ -48,21 +49,14 @@ pub struct LevelData {
pub border_warning_blocks: f64, pub border_warning_blocks: f64,
#[serde(default = "default_border_warning_time")] #[serde(default = "default_border_warning_time")]
pub border_warning_time: f64, pub border_warning_time: f64,
#[serde(rename = "clearWeatherTime", default)]
pub clear_weather_time: i32,
#[serde(default = "default_data_packs")] #[serde(default = "default_data_packs")]
pub data_packs: DataPacks, pub data_packs: DataPacks,
pub data_version: i32, pub data_version: i32,
#[serde(default)]
pub day_time: i64,
#[serde(with = "serde_enum_as_integer", default = "default_difficulty")] #[serde(with = "serde_enum_as_integer", default = "default_difficulty")]
pub difficulty: Difficulty, pub difficulty: Difficulty,
#[serde(default)] #[serde(default)]
pub difficulty_locked: bool, pub difficulty_locked: bool,
#[serde(default)] #[serde(default)]
pub game_rules: GameRuleRegistry,
pub world_gen_settings: WorldGenSettings,
#[serde(default)]
pub last_played: i64, pub last_played: i64,
#[serde(default = "default_level_name")] #[serde(default = "default_level_name")]
pub level_name: String, pub level_name: String,
@ -82,6 +76,26 @@ pub struct LevelData {
pub level_version: i32, pub level_version: i32,
#[serde(rename = "map_id", default)] #[serde(rename = "map_id", default)]
pub map_id: i32, 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; const DEFAULT_BORDER_DAMAGE_PER_BLOCK: f64 = 0.2;
@ -134,6 +148,13 @@ pub struct WorldGenSettings {
pub dimensions: Dimensions, 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>; pub type Dimensions = HashMap<String, Dimension>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Dimension { pub struct Dimension {
@ -144,7 +165,8 @@ pub struct Dimension {
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct Generator { pub struct Generator {
pub settings: GeneratorSettings, #[serde(default)]
pub settings: Option<GeneratorSettings>,
#[serde(default)] #[serde(default)]
pub biome_source: Option<BiomeSource>, pub biome_source: Option<BiomeSource>,
#[serde(rename = "type")] #[serde(rename = "type")]
@ -227,7 +249,9 @@ impl WorldGenSettings {
"minecraft:overworld".to_string(), "minecraft:overworld".to_string(),
Dimension { Dimension {
generator: Generator { generator: Generator {
settings: GeneratorSettings::Reference("minecraft:overworld".to_string()), settings: Some(GeneratorSettings::Reference(
"minecraft:overworld".to_string(),
)),
biome_source: Some(BiomeSource::WithPreset { biome_source: Some(BiomeSource::WithPreset {
preset: "minecraft:overworld".to_string(), preset: "minecraft:overworld".to_string(),
biome_type: "minecraft:multi_noise".to_string(), biome_type: "minecraft:multi_noise".to_string(),
@ -241,7 +265,7 @@ impl WorldGenSettings {
"minecraft:the_nether".to_string(), "minecraft:the_nether".to_string(),
Dimension { Dimension {
generator: Generator { generator: Generator {
settings: GeneratorSettings::Reference("minecraft:nether".to_string()), settings: Some(GeneratorSettings::Reference("minecraft:nether".to_string())),
biome_source: Some(BiomeSource::WithPreset { biome_source: Some(BiomeSource::WithPreset {
preset: "minecraft:nether".to_string(), preset: "minecraft:nether".to_string(),
biome_type: "minecraft:multi_noise".to_string(), biome_type: "minecraft:multi_noise".to_string(),
@ -255,7 +279,7 @@ impl WorldGenSettings {
"minecraft:the_end".to_string(), "minecraft:the_end".to_string(),
Dimension { Dimension {
generator: Generator { generator: Generator {
settings: GeneratorSettings::Reference("minecraft:end".to_string()), settings: Some(GeneratorSettings::Reference("minecraft:end".to_string())),
biome_source: Some(BiomeSource::Simple { biome_source: Some(BiomeSource::Simple {
biome_type: "minecraft:the_end".to_string(), biome_type: "minecraft:the_end".to_string(),
}), }),
@ -310,14 +334,10 @@ impl LevelData {
border_size_lerp_time: 0, border_size_lerp_time: 0,
border_warning_blocks: DEFAULT_BORDER_WARNING_BLOCKS, border_warning_blocks: DEFAULT_BORDER_WARNING_BLOCKS,
border_warning_time: DEFAULT_BORDER_WARNING_TIME, border_warning_time: DEFAULT_BORDER_WARNING_TIME,
clear_weather_time: -1,
data_packs: default_data_packs(), data_packs: default_data_packs(),
data_version: MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, data_version: MAXIMUM_SUPPORTED_WORLD_DATA_VERSION,
day_time: 0,
difficulty: DEFAULT_DIFFICULTY, difficulty: DEFAULT_DIFFICULTY,
difficulty_locked: false, difficulty_locked: false,
game_rules: GameRuleRegistry::default(),
world_gen_settings: WorldGenSettings::new(seed),
last_played: -1, last_played: -1,
level_name: DEFAULT_LEVEL_NAME.to_string(), level_name: DEFAULT_LEVEL_NAME.to_string(),
spawn_x: 0, spawn_x: 0,
@ -328,6 +348,11 @@ impl LevelData {
world_version: WorldVersion::default(), world_version: WorldVersion::default(),
level_version: MAXIMUM_SUPPORTED_LEVEL_VERSION, level_version: MAXIMUM_SUPPORTED_LEVEL_VERSION,
map_id: 0, 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, InfoNotFound,
#[error("Deserialization error: {0}")] #[error("Deserialization error: {0}")]
DeserializationError(String), DeserializationError(String),
#[error("Serialization error: {0}")]
SerializationError(String),
#[error("Unsupported world data version: {0}")] #[error("Unsupported world data version: {0}")]
UnsupportedDataVersion(i32), UnsupportedDataVersion(i32),
#[error("Unsupported world level version: {0}")] #[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, ()> { fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
if let Some(block_entity) = args.world.get_block_entity(args.position) { 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>() let Some(dropper) = block_entity.as_any().downcast_ref::<DropperBlockEntity>()
else { else {
return; return;

View File

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

View File

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

View File

@ -37,6 +37,23 @@ impl CommandExecutor for ListCommandExecutor {
ListMode::Uuids => get_player_names_and_ids(&players), 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 context
.source .source
.send_feedback( .send_feedback(
@ -45,9 +62,7 @@ impl CommandExecutor for ListCommandExecutor {
COMMANDS_LIST_PLAYERS, COMMANDS_LIST_PLAYERS,
[ [
TextComponent::text(players_len.to_string()), TextComponent::text(players_len.to_string()),
TextComponent::text( TextComponent::text(max_players.to_string()),
context.server().basic_config.max_players.to_string(),
),
list, list,
], ],
), ),

View File

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

View File

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

View File

@ -152,11 +152,21 @@ impl BreathManager {
pub fn send_air_supply(&self, player: &Player) { pub fn send_air_supply(&self, player: &Player) {
let air = self.air_supply.load(Ordering::Relaxed).clamp(0, MAX_AIR); 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, TrackedData::AIR_SUPPLY_ID,
MetaDataType::INT, MetaDataType::INT,
VarInt(air), VarInt(air),
)]); )],
Some(&bedrock_meta),
);
} }
pub fn reset(&self, player: &Player) { pub fn reset(&self, player: &Player) {

View File

@ -22,11 +22,14 @@ impl EndCrystalEntity {
impl EndCrystalEntity { impl EndCrystalEntity {
pub fn set_show_bottom(&self, show_bottom: bool) { 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, TrackedData::SHOW_BOTTOM,
MetaDataType::BOOLEAN, MetaDataType::BOOLEAN,
show_bottom, show_bottom,
)]); )],
None,
);
} }
} }

View File

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

View File

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

View File

@ -237,11 +237,26 @@ impl LivingEntity {
b &= !index; b &= !index;
} }
self.livings_flags.store(b, Ordering::Relaxed); 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, TrackedData::LIVING_ENTITY_FLAGS,
MetaDataType::BYTE, MetaDataType::BYTE,
b, b,
)]); )],
bedrock_meta.as_ref(),
);
} }
pub async fn clear_active_hand(&self) { pub async fn clear_active_hand(&self) {
@ -274,11 +289,14 @@ impl LivingEntity {
let clamped = health.max(0.0).min(max_health); let clamped = health.max(0.0).min(max_health);
self.health.store(clamped); self.health.store(clamped);
// tell everyone entities health changed // tell everyone entities health changed
self.entity.send_meta_data(&[Metadata::new( self.entity.send_meta_data(
&[Metadata::new(
TrackedData::HEALTH_ID, TrackedData::HEALTH_ID,
MetaDataType::FLOAT, MetaDataType::FLOAT,
clamped, clamped,
)]); )],
None,
);
} }
/// Returns the current maximum health for this entity /// Returns the current maximum health for this entity
@ -328,8 +346,10 @@ impl LivingEntity {
// Send absorption metadata for players (visual yellow hearts) // Send absorption metadata for players (visual yellow hearts)
if let Some(tracked_id) = self.player_absorption_id() { if let Some(tracked_id) = self.player_absorption_id() {
self.entity self.entity.send_meta_data(
.send_meta_data(&[Metadata::new(tracked_id, MetaDataType::FLOAT, new_abs)]); &[Metadata::new(tracked_id, MetaDataType::FLOAT, new_abs)],
None,
);
} }
} }
@ -1791,11 +1811,14 @@ impl LivingEntity {
// Clear any absorption // Clear any absorption
self.absorption.store(0.0); self.absorption.store(0.0);
// Send health metadata // Send health metadata
self.entity.send_meta_data(&[Metadata::new( self.entity.send_meta_data(
&[Metadata::new(
TrackedData::HEALTH_ID, TrackedData::HEALTH_ID,
MetaDataType::FLOAT, MetaDataType::FLOAT,
max_health, max_health,
)]); )],
None,
);
self.reset_effects_and_attributes().await; self.reset_effects_and_attributes().await;

View File

@ -86,14 +86,14 @@ impl CreeperEntity {
pub fn set_fuse_speed(&self, speed: i32) { pub fn set_fuse_speed(&self, speed: i32) {
self.fuse_speed.store(speed, Ordering::Relaxed); self.fuse_speed.store(speed, Ordering::Relaxed);
self.mob_entity self.mob_entity.living_entity.entity.send_meta_data(
.living_entity &[Metadata::new(
.entity
.send_meta_data(&[Metadata::new(
TrackedData::FUSE_ID, TrackedData::FUSE_ID,
MetaDataType::INTEGER, MetaDataType::INTEGER,
VarInt(speed), VarInt(speed),
)]); )],
None,
);
} }
async fn explode(&self) { async fn explode(&self) {
@ -219,11 +219,14 @@ impl Mob for CreeperEntity {
); );
self.ignited.store(true, Ordering::Relaxed); self.ignited.store(true, Ordering::Relaxed);
entity.send_meta_data(&[Metadata::new( entity.send_meta_data(
&[Metadata::new(
TrackedData::IS_IGNITED, TrackedData::IS_IGNITED,
MetaDataType::BOOLEAN, MetaDataType::BOOLEAN,
true, true,
)]); )],
None,
);
if player.gamemode.load() != pumpkin_util::GameMode::Creative { if player.gamemode.load() != pumpkin_util::GameMode::Creative {
// TODO: Handle DamageResult::Broken to broadcast item break and update player slot. // 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) { pub fn set_angry(&self, angry: bool) {
self.angry.store(angry, Ordering::Relaxed); self.angry.store(angry, Ordering::Relaxed);
self.mob_entity self.mob_entity.living_entity.entity.send_meta_data(
.living_entity &[Metadata::new(
.entity
.send_meta_data(&[Metadata::new(
TrackedData::CREEPY, TrackedData::CREEPY,
MetaDataType::BOOLEAN, MetaDataType::BOOLEAN,
angry, angry,
)]); )],
None,
);
} }
pub fn is_angry(&self) -> bool { pub fn is_angry(&self) -> bool {
@ -298,27 +298,27 @@ impl EndermanEntity {
pub fn set_provoked(&self, provoked: bool) { pub fn set_provoked(&self, provoked: bool) {
self.provoked.store(provoked, Ordering::Relaxed); self.provoked.store(provoked, Ordering::Relaxed);
self.mob_entity self.mob_entity.living_entity.entity.send_meta_data(
.living_entity &[Metadata::new(
.entity
.send_meta_data(&[Metadata::new(
TrackedData::STARED_AT, TrackedData::STARED_AT,
MetaDataType::BOOLEAN, MetaDataType::BOOLEAN,
provoked, provoked,
)]); )],
None,
);
} }
pub fn set_carried_block(&self, block_state: Option<BlockStateId>) { pub fn set_carried_block(&self, block_state: Option<BlockStateId>) {
self.carried_block.store(block_state); self.carried_block.store(block_state);
let value = block_state.map_or(VarInt(0), |id| VarInt(id.as_u16() as i32)); let value = block_state.map_or(VarInt(0), |id| VarInt(id.as_u16() as i32));
self.mob_entity self.mob_entity.living_entity.entity.send_meta_data(
.living_entity &[Metadata::new(
.entity
.send_meta_data(&[Metadata::new(
TrackedData::CARRY_STATE, TrackedData::CARRY_STATE,
MetaDataType::OPTIONAL_BLOCK_STATE, MetaDataType::OPTIONAL_BLOCK_STATE,
value, value,
)]); )],
None,
);
} }
pub fn get_carried_block(&self) -> Option<BlockStateId> { pub fn get_carried_block(&self) -> Option<BlockStateId> {

View File

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

View File

@ -110,11 +110,14 @@ impl ShulkerEntity {
fn set_attach_face(&self, face: BlockDirection) { fn set_attach_face(&self, face: BlockDirection) {
self.attach_face.store(face as u8, Ordering::Relaxed); self.attach_face.store(face as u8, Ordering::Relaxed);
let entity = &self.mob_entity.living_entity.entity; let entity = &self.mob_entity.living_entity.entity;
entity.send_meta_data(&[Metadata::new( entity.send_meta_data(
&[Metadata::new(
TrackedData::ATTACH_FACE_ID, TrackedData::ATTACH_FACE_ID,
MetaDataType::DIRECTION, MetaDataType::DIRECTION,
VarInt(face as i32), VarInt(face as i32),
)]); )],
None,
);
} }
pub fn get_raw_peek(&self) -> u8 { pub fn get_raw_peek(&self) -> u8 {
@ -148,11 +151,14 @@ impl ShulkerEntity {
self.peek_amount.store(amount, Ordering::Relaxed); self.peek_amount.store(amount, Ordering::Relaxed);
entity.send_meta_data(&[Metadata::new( entity.send_meta_data(
&[Metadata::new(
TrackedData::PEEK_ID, TrackedData::PEEK_ID,
MetaDataType::BYTE, MetaDataType::BYTE,
amount, amount,
)]); )],
None,
);
} }
pub fn is_closed(&self) -> bool { 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; let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby { 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, TrackedData::BABY_ID,
MetaDataType::BOOLEAN, MetaDataType::BOOLEAN,
true, true,
)]); )],
Some(&bedrock_meta),
);
} }
}) })
} }
@ -1015,20 +1020,59 @@ impl Entity {
/// Sets a custom name for the entity, typically used with nametags /// Sets a custom name for the entity, typically used with nametags
pub fn set_custom_name(&self, name: TextComponent) { pub fn set_custom_name(&self, name: TextComponent) {
self.custom_name.store(Arc::new(Some(name.clone()))); 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, TrackedData::CUSTOM_NAME,
MetaDataType::OPTIONAL_TEXT_COMPONENT, MetaDataType::OPTIONAL_TEXT_COMPONENT,
Some(name), Some(name),
)]); )],
Some(&bedrock_meta),
);
} }
pub fn set_custom_name_visible(&self, visible: bool) { pub fn set_custom_name_visible(&self, visible: bool) {
self.custom_name_visible.store(visible, Ordering::Relaxed); 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, TrackedData::CUSTOM_NAME_VISIBLE,
MetaDataType::BOOLEAN, MetaDataType::BOOLEAN,
visible, visible,
)]); )],
Some(&bedrock_meta),
);
} }
pub fn send_velocity(&self) { pub fn send_velocity(&self) {
@ -2383,11 +2427,19 @@ impl Entity {
// Only update and send metadata if the value changed // Only update and send metadata if the value changed
if new_frozen_ticks != old_frozen_ticks { if new_frozen_ticks != old_frozen_ticks {
self.frozen_ticks.store(new_frozen_ticks, Ordering::Relaxed); 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, TrackedData::TICKS_FROZEN,
MetaDataType::INTEGER, MetaDataType::INTEGER,
VarInt(new_frozen_ticks), VarInt(new_frozen_ticks),
)]); )],
Some(&bedrock_meta),
);
} }
// Vanilla parity: full-freeze damage is tick-phase based. // Vanilla parity: full-freeze damage is tick-phase based.
@ -2639,11 +2691,14 @@ impl Entity {
self.flags.fetch_and(!mask, Ordering::Relaxed) & !mask self.flags.fetch_and(!mask, Ordering::Relaxed) & !mask
}; };
self.send_meta_data(&[Metadata::new( self.send_meta_data(
&[Metadata::new(
TrackedData::SHARED_FLAGS_ID, TrackedData::SHARED_FLAGS_ID,
MetaDataType::BYTE, MetaDataType::BYTE,
new_je_flags, new_je_flags,
)]); )],
None,
);
if let Some(bedrock_flag) = flag.to_bedrock() { if let Some(bedrock_flag) = flag.to_bedrock() {
let (key, index) = if bedrock_flag >= 64 { let (key, index) = if bedrock_flag >= 64 {
@ -2710,14 +2765,21 @@ impl Entity {
.play_sound(sound, SoundCategory::Neutral, &self.pos.load()); .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 world = self.world.load();
let chunk_pos = self.chunk_pos.load(); let chunk_pos = self.chunk_pos.load();
for player in world.players.load().iter() { 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 // Apply Chebyshev distance check
let center = player.get_entity().chunk_pos.load(); 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) { if is_within_view_distance(chunk_pos, center, view_distance) {
let mut buf = Vec::new(); 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.bounding_box.store(aabb);
self.entity_dimension.store(dimension); self.entity_dimension.store(dimension);
let pose = pose as i32; 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, TrackedData::POSE,
MetaDataType::ENTITY_POSE, MetaDataType::ENTITY_POSE,
VarInt(pose), VarInt(pose),
)]); )],
Some(&bedrock_meta),
);
} }
} }

View File

@ -137,17 +137,23 @@ impl Mob for CatEntity {
let entity = self.get_entity(); let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0; let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby { if is_baby {
entity.send_meta_data(&[Metadata::new( entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID, TrackedData::BABY_ID,
MetaDataType::BOOLEAN, MetaDataType::BOOLEAN,
true, true,
)]); )],
None,
);
} }
entity.send_meta_data(&[Metadata::new( entity.send_meta_data(
&[Metadata::new(
TrackedData::CAT_VARIANT, TrackedData::CAT_VARIANT,
MetaDataType::CAT_VARIANT, MetaDataType::CAT_VARIANT,
VarInt(self.variant.load(Ordering::Relaxed) as i32), 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 entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0; let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby { 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, TrackedData::BABY_ID,
MetaDataType::BOOLEAN, MetaDataType::BOOLEAN,
true, 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, TrackedData::VARIANT,
MetaDataType::CHICKEN_VARIANT, MetaDataType::CHICKEN_VARIANT,
VarInt(self.variant.load(Ordering::Relaxed) as i32), 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 entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0; let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby { if is_baby {
entity.send_meta_data(&[Metadata::new( entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID, TrackedData::BABY_ID,
MetaDataType::BOOLEAN, MetaDataType::BOOLEAN,
true, true,
)]); )],
None,
);
} }
entity.send_meta_data(&[Metadata::new( entity.send_meta_data(
&[Metadata::new(
TrackedData::VARIANT, TrackedData::VARIANT,
MetaDataType::FROG_VARIANT, MetaDataType::FROG_VARIANT,
VarInt(self.variant.load(Ordering::Relaxed) as i32), 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) { fn set_packed_and_sync(&self, byte: u8) {
self.color_and_sheared.store(byte, Ordering::Relaxed); self.color_and_sheared.store(byte, Ordering::Relaxed);
self.mob_entity self.mob_entity.living_entity.entity.send_meta_data(
.living_entity &[Metadata::new(
.entity
.send_meta_data(&[Metadata::new(
TrackedData::WOOL_ID, TrackedData::WOOL_ID,
MetaDataType::BYTE, MetaDataType::BYTE,
byte as i8, byte as i8,
)]); )],
None,
);
} }
pub fn set_color(&self, color: u8) { pub fn set_color(&self, color: u8) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -69,13 +69,16 @@ impl EntityBase for LingeringPotionEntity {
let stack = self.item_stack.read().await; let stack = self.item_stack.read().await;
// Sync the item stack so the client renders the correct potion type // 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::tracked_data::TrackedData::ITEM_STACK,
pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK, pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK,
&pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from( &pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from(
stack.clone(), stack.clone(),
), ),
)]); )],
None,
);
}) })
} }

View File

@ -107,13 +107,16 @@ impl EntityBase for SplashPotionEntity {
let stack = self.item_stack.read().await; let stack = self.item_stack.read().await;
// Sync the item stack // 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::tracked_data::TrackedData::ITEM_STACK,
pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK, pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK,
&pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from( &pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from(
stack.clone(), stack.clone(),
), ),
)]); )],
None,
);
}) })
} }

View File

@ -85,7 +85,8 @@ impl EntityBase for TNTEntity {
self.entity self.entity
.set_velocity(Vector3::new(-pos.sin() * 0.02, 0.2, -pos.cos() * 0.02)); .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( Metadata::new(
TrackedData::FUSE_ID, TrackedData::FUSE_ID,
MetaDataType::INTEGER, MetaDataType::INTEGER,
@ -96,7 +97,9 @@ impl EntityBase for TNTEntity {
MetaDataType::BLOCK_STATE, MetaDataType::BLOCK_STATE,
VarInt(i32::from(Block::TNT.default_state.id.as_u16())), 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.left_paddle_moving.store(left, Ordering::Relaxed);
self.right_paddle_moving.store(right, 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_LEFT, MetaDataType::BOOLEAN, left),
Metadata::new(TrackedData::ID_PADDLE_RIGHT, MetaDataType::BOOLEAN, right), Metadata::new(TrackedData::ID_PADDLE_RIGHT, MetaDataType::BOOLEAN, right),
]); ],
None,
);
} }
fn send_wobble_metadata(&self) { fn send_wobble_metadata(&self) {

View File

@ -64,7 +64,8 @@ impl VehicleEntity {
} }
pub fn send_wobble_metadata(&self) { pub fn send_wobble_metadata(&self) {
self.entity.send_meta_data(&[ self.entity.send_meta_data(
&[
Metadata::new( Metadata::new(
TrackedData::ID_HURT, TrackedData::ID_HURT,
MetaDataType::INTEGER, MetaDataType::INTEGER,
@ -75,12 +76,17 @@ impl VehicleEntity {
MetaDataType::INTEGER, MetaDataType::INTEGER,
self.get_hurt_dir(), self.get_hurt_dir(),
), ),
]); ],
self.entity.send_meta_data(&[Metadata::new( None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::ID_DAMAGE, TrackedData::ID_DAMAGE,
MetaDataType::FLOAT, MetaDataType::FLOAT,
self.get_damage(), self.get_damage(),
)]); )],
None,
);
} }
pub async fn kill_and_drop_self(&self) { 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 generator = &level.world_gen;
let seed = level.seed.0; let seed = level.seed.0;
let global_cache = generator.global_structure_cache()?;
find_nearest_structure( find_nearest_structure(
origin, origin,
&[&StructureSet::get("strongholds").unwrap().placement], &[&StructureSet::get("strongholds").unwrap().placement],
100, // max search radius in chunks, matches vanilla default 100, // max search radius in chunks, matches vanilla default
seed as i64, 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 tcp_listener = if server.advanced_config.networking.java.enabled {
let address = server.basic_config.java_edition_address; let address = server.advanced_config.networking.java.address;
// Setup the TCP server socket. // Setup the TCP server socket.
let listener = match TcpListener::bind(address).await { let listener = match TcpListener::bind(address).await {
Ok(l) => l, Ok(l) => l,
@ -275,7 +275,7 @@ impl PumpkinServer {
let lan_broadcast = LANBroadcast::new( let lan_broadcast = LANBroadcast::new(
&server.advanced_config.networking.lan_broadcast, &server.advanced_config.networking.lan_broadcast,
&server.basic_config, &server.advanced_config.networking.java.motd,
); );
server.spawn_task(lan_broadcast.start(addr)); 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( Some(Arc::new(
UdpSocket::bind(server.basic_config.bedrock_edition_address) UdpSocket::bind(server.advanced_config.networking.bedrock.address)
.await .await
.expect("Failed to bind UDP Socket"), .expect("Failed to bind UDP Socket"),
)) ))

View File

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

View File

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

View File

@ -1290,7 +1290,7 @@ impl BedrockClient {
.as_ref() .as_ref()
.map_or(0, |p| p.online) as u32 .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( Self::send_offline_packet(
&CNoFreeIncomingConnections::new(server.server_guid), &CNoFreeIncomingConnections::new(server.server_guid),
addr, addr,

View File

@ -3,7 +3,12 @@ use std::{
sync::{Arc, atomic::Ordering}, 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_inventory::screen_handler::{InventoryPlayer, ScreenHandler};
use pumpkin_macros::send_cancellable; use pumpkin_macros::send_cancellable;
use pumpkin_protocol::bedrock::{ use pumpkin_protocol::bedrock::{
@ -37,7 +42,7 @@ use pumpkin_protocol::{
codec::{var_int::VarInt, var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong}, codec::{var_int::VarInt, var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong},
java::client::play::{Animation, CEntityAnimation, CSetSelectedSlot, CSystemChatMessage}, 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::inventory::Inventory;
use pumpkin_world::world::BlockFlags; use pumpkin_world::world::BlockFlags;
@ -47,8 +52,10 @@ use crate::{
entity::{EntityBase, player::Player}, entity::{EntityBase, player::Player},
net::{DisconnectReason, bedrock::BedrockClient}, net::{DisconnectReason, bedrock::BedrockClient},
plugin::player::{ plugin::player::{
item_held::PlayerItemHeldEvent, player_chat::PlayerChatEvent, item_held::PlayerItemHeldEvent,
player_chat::PlayerChatEvent,
player_command_send::PlayerCommandSendEvent, player_command_send::PlayerCommandSendEvent,
player_interact_event::{InteractAction, PlayerInteractEvent},
player_toggle_flight_event::PlayerToggleFlightEvent, player_toggle_flight_event::PlayerToggleFlightEvent,
}, },
server::{Server, seasonal_events}, server::{Server, seasonal_events},
@ -152,8 +159,10 @@ impl BedrockClient {
} }
let server = player.world().server.upgrade().unwrap(); let server = player.world().server.upgrade().unwrap();
let view_distance = let view_distance = chunk_radius.clamp(
chunk_radius.clamp(2, NonZeroI32::from(server.basic_config.view_distance).get()); 2,
NonZeroI32::from(server.advanced_config.networking.bedrock.view_distance).get(),
);
self.enqueue_packet(&CChunkRadiusUpdate { self.enqueue_packet(&CChunkRadiusUpdate {
chunk_radius: VarInt(view_distance), 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) => { TransactionData::UseItemOnEntity(data) => {

View File

@ -30,12 +30,12 @@ impl BedrockClient {
let motd_string = ServerInfo { let motd_string = ServerInfo {
edition: "MCPE", edition: "MCPE",
// TODO The default motd is to long to be displayed completely // 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, protocol_version: CURRENT_BEDROCK_MC_PROTOCOL,
version_name: CURRENT_BEDROCK_MC_VERSION, version_name: CURRENT_BEDROCK_MC_VERSION,
player_count, player_count,
// A large number looks wreird on the client worlds window // 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, server_unique_id: server.server_guid,
motd_line_2: server.basic_config.default_level_name.clone(), motd_line_2: server.basic_config.default_level_name.clone(),
game_mode: server.defaultgamemode.lock().await.gamemode.to_str(), 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. // 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. // 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. // 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 { if max_players > 0 && server.get_player_count() >= max_players as usize {
self.kick(TextComponent::translate_cross( self.kick(TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_SERVER_FULL, translation::java::MULTIPLAYER_DISCONNECT_SERVER_FULL,
@ -74,7 +74,7 @@ impl JavaClient {
} }
} }
} else { } else {
let id = if server.basic_config.online_mode { let id = if server.advanced_config.networking.java.online_mode {
login_start.uuid login_start.uuid
} else { } else {
offline_uuid(&login_start.name).expect("This is very not safe and bad") offline_uuid(&login_start.name).expect("This is very not safe and bad")
@ -87,16 +87,19 @@ impl JavaClient {
profile_actions: None, profile_actions: None,
}; };
if server.advanced_config.networking.java_compression.enabled { if server.advanced_config.networking.java.compression.enabled {
self.enable_compression(server).await; self.enable_compression(server).await;
} }
if server.basic_config.encryption { if server.advanced_config.networking.java.encryption {
let verify_token: [u8; 4] = rand::random(); let verify_token: [u8; 4] = rand::random();
// Wait until we have sent the encryption packet to the client // Wait until we have sent the encryption packet to the client
self.send_packet_now( self.send_packet_now(
&server &server
.encryption_request(&verify_token, server.basic_config.online_mode) .encryption_request(
&verify_token,
server.advanced_config.networking.java.online_mode,
)
.await, .await,
) )
.await; .await;
@ -132,7 +135,7 @@ impl JavaClient {
return; return;
}; };
if server.basic_config.online_mode { if server.advanced_config.networking.java.online_mode {
// Online mode auth // Online mode auth
match self match self
.authenticate(server, &shared_secret, &profile.name) .authenticate(server, &shared_secret, &profile.name)
@ -201,7 +204,8 @@ impl JavaClient {
let compression = server let compression = server
.advanced_config .advanced_config
.networking .networking
.java_compression .java
.compression
.info .info
.clone(); .clone();
// We want to wait until we have sent the compression packet to the client // We want to wait until we have sent the compression packet to the client
@ -236,7 +240,7 @@ impl JavaClient {
username, username,
&hash, &hash,
&ip, &ip,
&server.advanced_config.networking.authentication, &server.advanced_config.networking.java.authentication,
)?; )?;
// Check if the player should join // Check if the player should join
@ -244,6 +248,7 @@ impl JavaClient {
if server if server
.advanced_config .advanced_config
.networking .networking
.java
.authentication .authentication
.player_profile .player_profile
.allow_banned_players .allow_banned_players
@ -251,6 +256,7 @@ impl JavaClient {
for allowed in &server for allowed in &server
.advanced_config .advanced_config
.networking .networking
.java
.authentication .authentication
.player_profile .player_profile
.allowed_actions .allowed_actions
@ -270,7 +276,12 @@ impl JavaClient {
for property in profile.properties.load().iter() { for property in profile.properties.load().iter() {
authentication::validate_textures( authentication::validate_textures(
property, property,
&server.advanced_config.networking.authentication.textures, &server
.advanced_config
.networking
.java
.authentication
.textures,
) )
.map_err(AuthError::TextureError)?; .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::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::time::Duration; use std::time::Duration;
@ -23,7 +23,7 @@ pub struct LANBroadcast {
impl LANBroadcast { impl LANBroadcast {
/// Creates a new LAN broadcast instance from the provided configuration /// Creates a new LAN broadcast instance from the provided configuration
#[must_use] #[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 port = config.port.unwrap_or(0);
let advanced_motd = config.motd.clone().unwrap_or_default(); let advanced_motd = config.motd.clone().unwrap_or_default();
@ -32,7 +32,7 @@ impl LANBroadcast {
warn!( 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." "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 { } else {
advanced_motd 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 // 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 // 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 // since those errors need to be corrected by server owner
#[expect(clippy::too_many_lines)]
#[inline] #[inline]
async fn handle_packet( async fn handle_packet(
buf: Vec<u8>, buf: Vec<u8>,
@ -156,7 +157,9 @@ async fn handle_packet(
let response = CFullStatus { let response = CFullStatus {
session_id: packet.session_id, 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)?, version: CString::new(CURRENT_MC_VERSION)?,
plugins: CString::new(plugins)?, plugins: CString::new(plugins)?,
map: CString::new( map: CString::new(
@ -167,7 +170,8 @@ async fn handle_packet(
.map_or("world", |w| w.get_world_name()), .map_or("world", |w| w.get_world_name()),
)?, )?,
num_players: server.get_player_count(), 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_port: bound_addr.port(),
host_ip: CString::new(bound_addr.ip().to_string())?, host_ip: CString::new(bound_addr.ip().to_string())?,
players, players,
@ -179,7 +183,9 @@ async fn handle_packet(
} else { } else {
let response = CBasicStatus { let response = CBasicStatus {
session_id: packet.session_id, 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( map: CString::new(
server server
.worlds .worlds
@ -188,7 +194,8 @@ async fn handle_packet(
.map_or("world", |w| w.get_world_name()), .map_or("world", |w| w.get_world_name()),
)?, )?,
num_players: server.get_player_count(), 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_port: bound_addr.port(),
host_ip: CString::new(bound_addr.ip().to_string())?, host_ip: CString::new(bound_addr.ip().to_string())?,
}; };

View File

@ -292,7 +292,7 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
.as_ref() .as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?; .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> { 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() .as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?; .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> { 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() .as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?; .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> { 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() .as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?; .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> { 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() .as_ref()
.ok_or_else(|| wasmtime::Error::msg("Server not available"))?; .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( async fn get_default_gamemode(

View File

@ -68,8 +68,8 @@ impl<'a> CachedBranding {
impl CachedStatus { impl CachedStatus {
#[must_use] #[must_use]
pub fn new(config: &BasicConfiguration) -> Self { pub fn new(config: &BasicConfiguration, motd: &str, max_players: u32) -> Self {
let status_response = Self::build_response(config); let status_response = Self::build_response(config, motd, max_players);
Self { Self {
status_response, 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 { let favicon = if config.use_favicon {
config.favicon_path.as_ref().map_or_else( config.favicon_path.as_ref().map_or_else(
|| { || {
@ -185,11 +189,11 @@ impl CachedStatus {
protocol: LOWEST_SUPPORTED_MC_VERSION.protocol_version() as u32, protocol: LOWEST_SUPPORTED_MC_VERSION.protocol_version() as u32,
}), }),
players: Some(Players { players: Some(Players {
max: config.max_players, max: max_players,
online: 0, online: 0,
sample: vec![], sample: vec![],
}), }),
description: config.motd.clone(), description: motd.to_string(),
favicon, favicon,
// This should stay true even when reports are disabled. // This should stay true even when reports are disabled.
// It prevents the annoying popup when joining the server. // It prevents the annoying popup when joining the server.
@ -200,6 +204,10 @@ impl CachedStatus {
impl Default for CachedStatus { impl Default for CachedStatus {
fn default() -> Self { 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 seed = level_info.world_gen_settings.seed;
let level_info = Arc::new(ArcSwap::new(Arc::new(level_info))); 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 { let defaultgamemode = Mutex::new(DefaultGamemode {
gamemode: basic_config.default_gamemode, gamemode: basic_config.default_gamemode,
}); });
@ -218,7 +222,7 @@ impl Server {
let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps)); let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps));
let mojang_keys_task = tokio::spawn({ 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; let allow_chat = basic_config.allow_chat_reports;
async move { async move {
if allow_chat { if allow_chat {
@ -351,14 +355,24 @@ impl Server {
info!("All worlds loaded successfully."); info!("All worlds loaded successfully.");
if server.basic_config.online_mode { if server.advanced_config.networking.bedrock.online_mode {
let server_clone = server.clone(); let server_clone = server.clone();
tokio::spawn(async move { tokio::spawn(async move {
server_clone server_clone
.bedrock_oidc_keys .bedrock_oidc_keys
.get_or_init(|| async { .get_or_init(|| async {
tokio::task::block_in_place(|| { 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}"); error!("Failed to fetch Bedrock OIDC keys: {e}");
(String::new(), pumpkin_util::jwt::Jwks { keys: Vec::new() }) (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 { pub fn get_view_distance(player: &Player) -> NonZeroU8 {
let server = player.world().server.upgrade().unwrap(); let server = player.world().server.upgrade().unwrap();
player.config.load().view_distance.clamp( let max_view_distance = match player.client.as_ref() {
NonZeroU8::new(2).unwrap(), ClientPlatform::Java(_) => server.advanced_config.networking.java.view_distance,
server.basic_config.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 // Checks if the target chunk is within the view distance

View File

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