diff --git a/README.md b/README.md index 256e42638..86c709b84 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ and customizable experience. It prioritizes performance and player enjoyment while adhering to the core mechanics of the game.
-![chunk loading](/assets/pumpkin_chunk_loading.webp) +
diff --git a/assets/pumpkin_chunk_loading.webm b/assets/pumpkin_chunk_loading.webm new file mode 100644 index 000000000..91f189482 Binary files /dev/null and b/assets/pumpkin_chunk_loading.webm differ diff --git a/assets/pumpkin_chunk_loading.webp b/assets/pumpkin_chunk_loading.webp deleted file mode 100644 index a28912c55..000000000 Binary files a/assets/pumpkin_chunk_loading.webp and /dev/null differ diff --git a/pumpkin-config/src/chunk.rs b/pumpkin-config/src/chunk.rs index 8463ac0bd..0d24a9a3a 100644 --- a/pumpkin-config/src/chunk.rs +++ b/pumpkin-config/src/chunk.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; /// Configuration for chunk storage format. /// /// Supports multiple chunk formats, currently `Anvil` and `Linear`. -#[derive(Deserialize, Default, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone)] #[serde(tag = "type")] pub enum ChunkConfig { /// Standard Anvil chunk storage. @@ -16,10 +16,15 @@ pub enum ChunkConfig { Linear, /// Pumpkin's own optimized world format. #[serde(rename = "pump")] - #[default] Pump, } +impl Default for ChunkConfig { + fn default() -> Self { + Self::Anvil(AnvilChunkConfig::default()) + } +} + /// Configuration for Anvil chunk storage. #[derive(Deserialize, Serialize, Default, Clone)] #[serde(default)] diff --git a/pumpkin-config/src/lib.rs b/pumpkin-config/src/lib.rs index 57ce9cae5..9d87d7b79 100644 --- a/pumpkin-config/src/lib.rs +++ b/pumpkin-config/src/lib.rs @@ -5,7 +5,6 @@ use pumpkin_util::{Difficulty, GameMode, PermissionLvl, random}; use recipe::RecipeConfig; use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use std::net::SocketAddr; use std::path::PathBuf; use std::{fs, num::NonZeroU8, path::Path}; use tracing::{debug, warn}; @@ -20,7 +19,9 @@ pub mod resource_pack; pub use chat::ChatConfig; pub use commands::CommandsConfig; pub use networking::auth::AuthenticationConfig; +pub use networking::bedrock::BedrockConfig; pub use networking::compression::CompressionConfig; +pub use networking::java::JavaConfig; pub use networking::lan_broadcast::LANBroadcastConfig; pub use networking::rcon::RCONConfig; pub use plugins::PluginsConfig; @@ -64,6 +65,48 @@ impl LoadConfiguration for PumpkinConfig { fn validate(&self) { self.basic.validate(); self.advanced.validate(); + + let min_vd = NonZeroU8::new(2).unwrap(); + let max_vd = NonZeroU8::new(64).unwrap(); + + // Validate Java + assert!( + self.advanced.networking.java.view_distance >= min_vd, + "Java View distance must be at least 2" + ); + assert!( + self.advanced.networking.java.view_distance <= max_vd, + "Java View distance must be less than 64" + ); + if self.advanced.networking.java.online_mode { + assert!( + self.advanced.networking.java.encryption, + "When online mode is enabled, encryption must be enabled" + ); + } + + // Validate Bedrock + assert!( + self.advanced.networking.bedrock.view_distance >= min_vd, + "Bedrock View distance must be at least 2" + ); + assert!( + self.advanced.networking.bedrock.view_distance <= max_vd, + "Bedrock View distance must be less than 64" + ); + if self.advanced.networking.bedrock.online_mode { + assert!( + self.advanced.networking.bedrock.encryption, + "When online mode is enabled, bedrock_encryption must be enabled" + ); + } + + if self.basic.allow_chat_reports { + assert!( + self.advanced.networking.java.online_mode, + "When allow_chat_reports is enabled, java.online_mode must be enabled" + ); + } } } @@ -110,24 +153,8 @@ pub struct AdvancedConfiguration { #[derive(Serialize, Deserialize)] #[serde(default)] pub struct BasicConfiguration { - /// Whether Java Edition Clients are Accepted. - pub java_edition: bool, - /// The address and port to which the Java Edition server will bind. - pub java_edition_address: SocketAddr, - /// Whether Bedrock Edition Clients are Accepted. - pub bedrock_edition: bool, - /// Whether Bedrock Edition Clients are Accepted. - pub bedrock_edition_address: SocketAddr, - /// Whether packet encryption is enabled for Bedrock Edition. - pub bedrock_encryption: bool, /// The seed for the world generation. pub seed: Seed, - /// The maximum number of players allowed on the server. Specifying `0` disables the limit. - pub max_players: u32, - /// The maximum view distance for players. - pub view_distance: NonZeroU8, - /// The maximum simulated view distance. - pub simulation_distance: NonZeroU8, /// The default game difficulty. pub default_difficulty: Difficulty, /// The op level assigned by the /op command. @@ -138,12 +165,6 @@ pub struct BasicConfiguration { pub allow_end: bool, /// Whether the server is in hardcore mode. pub hardcore: bool, - /// Whether online mode is enabled. Requires valid Minecraft accounts. - pub online_mode: bool, - /// Whether packet encryption is enabled. Required when online mode is enabled. - pub encryption: bool, - /// Message of the Day; the server's description displayed on the status screen. - pub motd: String, /// The server's ticks per second. pub tps: f32, /// The default gamemode for players. @@ -169,23 +190,12 @@ pub struct BasicConfiguration { impl Default for BasicConfiguration { fn default() -> Self { Self { - java_edition: true, - java_edition_address: "0.0.0.0:25565".parse().unwrap(), - bedrock_edition: true, - bedrock_edition_address: "0.0.0.0:19132".parse().unwrap(), - bedrock_encryption: true, seed: Seed(random::get_seed()), - max_players: 1000, - view_distance: NonZeroU8::new(16).unwrap(), - simulation_distance: NonZeroU8::new(10).unwrap(), default_difficulty: Difficulty::Normal, op_permission_level: PermissionLvl::Four, allow_nether: true, allow_end: true, hardcore: false, - online_mode: true, - encryption: true, - motd: "A blazingly fast Pumpkin server!".to_string(), tps: 20.0, default_gamemode: GameMode::Survival, force_gamemode: false, @@ -207,35 +217,7 @@ impl BasicConfiguration { PathBuf::from(&self.default_level_name) } - pub fn validate(&self) { - let min = NonZeroU8::new(2).unwrap(); - let max = NonZeroU8::new(64).unwrap(); - - assert!( - self.view_distance.ge(&min), - "View distance must be at least 2" - ); - assert!( - self.view_distance.le(&max), - "View distance must be less than 64" - ); - if self.online_mode { - assert!( - self.encryption, - "When online mode is enabled, encryption must be enabled" - ); - assert!( - self.bedrock_encryption, - "When online mode is enabled, bedrock_encryption must be enabled" - ); - } - if self.allow_chat_reports { - assert!( - self.online_mode, - "When allow_chat_reports is enabled, online_mode must be enabled" - ); - } - } + pub const fn validate(&self) {} } impl AdvancedConfiguration { diff --git a/pumpkin-config/src/networking/bedrock.rs b/pumpkin-config/src/networking/bedrock.rs new file mode 100644 index 000000000..bd888f2cd --- /dev/null +++ b/pumpkin-config/src/networking/bedrock.rs @@ -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, + /// 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(), + } + } +} diff --git a/pumpkin-config/src/networking/compression.rs b/pumpkin-config/src/networking/compression.rs index 15dfc436f..7a8df596b 100644 --- a/pumpkin-config/src/networking/compression.rs +++ b/pumpkin-config/src/networking/compression.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; /// Configuration for packet compression. /// /// Controls whether network packet compression is enabled and the compression parameters. -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Serialize, Clone)] #[serde(default)] pub struct CompressionConfig { /// Whether compression is enabled. diff --git a/pumpkin-config/src/networking/java.rs b/pumpkin-config/src/networking/java.rs new file mode 100644 index 000000000..10f45264e --- /dev/null +++ b/pumpkin-config/src/networking/java.rs @@ -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(), + } + } +} diff --git a/pumpkin-config/src/networking/mod.rs b/pumpkin-config/src/networking/mod.rs index 91e906c11..5da592ebf 100644 --- a/pumpkin-config/src/networking/mod.rs +++ b/pumpkin-config/src/networking/mod.rs @@ -1,13 +1,16 @@ -use auth::AuthenticationConfig; use proxy::ProxyConfig; use query::QueryConfig; use rcon::RCONConfig; use serde::{Deserialize, Serialize}; -use crate::{CompressionConfig, LANBroadcastConfig}; +use crate::LANBroadcastConfig; +use bedrock::BedrockConfig; +use java::JavaConfig; pub mod auth; +pub mod bedrock; pub mod compression; +pub mod java; pub mod lan_broadcast; pub mod proxy; pub mod query; @@ -18,19 +21,18 @@ pub mod rcon; /// Covers authentication, query, RCON, proxying, packet compression, /// and LAN broadcast behaviour. #[derive(Deserialize, Serialize, Default)] +#[serde(default)] pub struct NetworkingConfig { - /// Authentication settings for client connections. - pub authentication: AuthenticationConfig, /// Query protocol settings for server status requests. pub query: QueryConfig, /// RCON (remote console) configuration. pub rcon: RCONConfig, /// Proxy-related networking settings. pub proxy: ProxyConfig, - /// Java Edition packet compression settings. - pub java_compression: CompressionConfig, - /// Bedrock Edition packet compression settings. - pub bedrock_compression: CompressionConfig, /// LAN broadcast settings. pub lan_broadcast: LANBroadcastConfig, + /// Java Edition configuration settings. + pub java: JavaConfig, + /// Bedrock Edition configuration settings. + pub bedrock: BedrockConfig, } diff --git a/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs b/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs index ae88063dd..a8ad14f5e 100644 --- a/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs +++ b/pumpkin-protocol/src/bedrock/client/raknet/unconnected_pong.rs @@ -26,7 +26,7 @@ impl PacketWrite for CUnconnectedPong { pub struct ServerInfo { /// (BE or MCEE for Education Edition) pub edition: &'static str, - pub motd_line_1: &'static str, + pub motd_line_1: String, pub protocol_version: u32, pub version_name: &'static str, pub player_count: i32, diff --git a/pumpkin-protocol/src/bedrock/client/set_actor_data.rs b/pumpkin-protocol/src/bedrock/client/set_actor_data.rs index c96a20143..1e1c69980 100644 --- a/pumpkin-protocol/src/bedrock/client/set_actor_data.rs +++ b/pumpkin-protocol/src/bedrock/client/set_actor_data.rs @@ -85,6 +85,7 @@ impl PacketWrite for EntityMetadata { } } +#[derive(Clone)] pub enum MetadataValue { Byte(i8), Short(i16), diff --git a/pumpkin-protocol/src/bedrock/network_item.rs b/pumpkin-protocol/src/bedrock/network_item.rs index 596dcac52..57208f125 100644 --- a/pumpkin-protocol/src/bedrock/network_item.rs +++ b/pumpkin-protocol/src/bedrock/network_item.rs @@ -3,7 +3,7 @@ use std::num::NonZeroI32; use pumpkin_data::item::{BedrockItem, JavaToBedrockItemMapping}; use pumpkin_data::item_stack::ItemStack; -use pumpkin_nbt::Nbt; +use pumpkin_nbt::{Nbt, deserializer::NbtReadHelperBedrock}; use crate::{ codec::{var_int::VarInt, var_uint::VarUInt}, @@ -54,12 +54,45 @@ impl PacketRead for NetworkItemDescriptor { let mut user_data = vec![0u8; user_data_len as usize]; buf.read_exact(&mut user_data)?; + let mut nbt_data = Nbt::default(); + let mut place_on_blocks = Vec::new(); + let mut destroy_blocks = Vec::new(); + let mut shield_blocking_tick = 0; + + if !user_data.is_empty() { + let mut cursor = std::io::Cursor::new(user_data); + let nbt_version = i16::read(&mut cursor)?; + if nbt_version == -1 { + let _version = i8::read(&mut cursor)?; + let mut nbt_reader = NbtReadHelperBedrock::new(&mut cursor); + nbt_data = Nbt::read(&mut nbt_reader) + .map_err(|e| Error::new(std::io::ErrorKind::InvalidData, e))?; + } + + let place_on_len = VarUInt::read(&mut cursor)?.0; + for _ in 0..place_on_len { + place_on_blocks.push(String::read(&mut cursor)?); + } + + let destroy_len = VarUInt::read(&mut cursor)?.0; + for _ in 0..destroy_len { + destroy_blocks.push(String::read(&mut cursor)?); + } + + if id.0 == (BedrockItem::SHIELD.id as i32) { + shield_blocking_tick = i64::read(&mut cursor)?; + } + } + Ok(Self { id, stack_size, aux_value, block_runtime_id, - ..Default::default() + nbt_data, + place_on_blocks, + destroy_blocks, + shield_blocking_tick, }) } } @@ -217,13 +250,46 @@ impl PacketRead for ItemStackWrapper { let mut user_data = vec![0u8; user_data_len as usize]; buf.read_exact(&mut user_data)?; + let mut nbt_data = Nbt::default(); + let mut place_on_blocks = Vec::new(); + let mut destroy_blocks = Vec::new(); + let mut shield_blocking_tick = 0; + + if !user_data.is_empty() { + let mut cursor = std::io::Cursor::new(user_data); + let nbt_version = i16::read(&mut cursor)?; + if nbt_version == -1 { + let _version = i8::read(&mut cursor)?; + let mut nbt_reader = NbtReadHelperBedrock::new(&mut cursor); + nbt_data = Nbt::read(&mut nbt_reader) + .map_err(|e| Error::new(std::io::ErrorKind::InvalidData, e))?; + } + + let place_on_len = VarUInt::read(&mut cursor)?.0; + for _ in 0..place_on_len { + place_on_blocks.push(String::read(&mut cursor)?); + } + + let destroy_len = VarUInt::read(&mut cursor)?.0; + for _ in 0..destroy_len { + destroy_blocks.push(String::read(&mut cursor)?); + } + + if id == (BedrockItem::SHIELD.id) { + shield_blocking_tick = i64::read(&mut cursor)?; + } + } + Ok(Self { id, stack_size, aux_value, block_runtime_id, + nbt_data, + place_on_blocks, + destroy_blocks, + shield_blocking_tick, net_id, - ..Default::default() }) } } diff --git a/pumpkin-protocol/src/java/client/play/entity_metadata.rs b/pumpkin-protocol/src/java/client/play/entity_metadata.rs index 4e68e3150..dcd8afecf 100644 --- a/pumpkin-protocol/src/java/client/play/entity_metadata.rs +++ b/pumpkin-protocol/src/java/client/play/entity_metadata.rs @@ -58,7 +58,7 @@ impl ClientPacket for CSetEntityMetadata { pub struct Metadata { pub index: TrackedId, pub r#type: MetaDataType, - value: T, + pub value: T, } impl Metadata { diff --git a/pumpkin-util/src/jwt/mod.rs b/pumpkin-util/src/jwt/mod.rs index 53bd20964..7b198bdf5 100644 --- a/pumpkin-util/src/jwt/mod.rs +++ b/pumpkin-util/src/jwt/mod.rs @@ -227,8 +227,24 @@ pub const OIDC_AUDIENCE: &str = "api://auth-minecraft-services/multiplayer"; pub const OIDC_DISCOVERY_URL: &str = "https://client.discovery.minecraft-services.net/api/v1.0/discovery/MinecraftPE/builds/1.0.0.0"; -pub fn fetch_oidc_jwks() -> Result<(String, Jwks), AuthError> { - let discovery: Value = ureq::get(OIDC_DISCOVERY_URL) +pub fn fetch_oidc_jwks( + discovery_url: Option<&str>, + connect_timeout_ms: u32, + read_timeout_ms: u32, +) -> Result<(String, Jwks), AuthError> { + let url = discovery_url.unwrap_or(OIDC_DISCOVERY_URL); + let config = ureq::Agent::config_builder() + .timeout_connect(Some(std::time::Duration::from_millis( + connect_timeout_ms as u64, + ))) + .timeout_recv_response(Some(std::time::Duration::from_millis( + read_timeout_ms as u64, + ))) + .build(); + let agent: ureq::Agent = config.into(); + + let discovery: Value = agent + .get(url) .call() .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))? .body_mut() @@ -248,7 +264,8 @@ pub fn fetch_oidc_jwks() -> Result<(String, Jwks), AuthError> { .ok_or_else(|| AuthError::PublicKeyBuild("Discovery missing serviceUri".into()))?; let openid_config_url = format!("{service_uri}/.well-known/openid-configuration"); - let openid_config: Value = ureq::get(&openid_config_url) + let openid_config: Value = agent + .get(&openid_config_url) .call() .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))? .body_mut() @@ -269,7 +286,8 @@ pub fn fetch_oidc_jwks() -> Result<(String, Jwks), AuthError> { .ok_or_else(|| AuthError::PublicKeyBuild("OpenID config missing issuer".into()))? .to_string(); - let jwks: Jwks = ureq::get(jwks_uri) + let jwks: Jwks = agent + .get(jwks_uri) .call() .map_err(|e| AuthError::PublicKeyBuild(e.to_string()))? .body_mut() diff --git a/pumpkin-world/benches/chunk_gen.rs b/pumpkin-world/benches/chunk_gen.rs index 6c02ac669..dc5af0433 100644 --- a/pumpkin-world/benches/chunk_gen.rs +++ b/pumpkin-world/benches/chunk_gen.rs @@ -5,7 +5,7 @@ use pumpkin_data::dimension::Dimension; use pumpkin_util::world_seed::Seed; use pumpkin_world::ProtoChunk; use pumpkin_world::chunk_system::{Cache, Chunk, StagedChunkEnum, generate_single_chunk}; -use pumpkin_world::generation::generator::VanillaGenerator; +use pumpkin_world::generation::generator::WorldGenerator; use pumpkin_world::generation::get_world_gen; use pumpkin_world::world::WorldPortalExt; use std::hint::black_box; @@ -13,7 +13,6 @@ use std::sync::Arc; const SEED: Seed = Seed(42); -// Stub portal — allows all block placements, skips mob spawning. struct BlockRegistry; impl WorldPortalExt for BlockRegistry { fn can_place_at( @@ -54,11 +53,13 @@ impl WorldPortalExt for BlockRegistry { } } -/// Build a cache containing chunk 0, 0 at the center so that features -/// and lighting stages get a 3×3 cache. +fn make_world_gen() -> Box { + get_world_gen(SEED, Dimension::OVERWORLD, false, Vec::new(), String::new()) +} + fn setup_cache( target_stage: StagedChunkEnum, - world_gen: &VanillaGenerator, + world_gen: &WorldGenerator, block_registry: &dyn WorldPortalExt, ) -> Cache { let radius = target_stage.get_direct_radius(); @@ -97,10 +98,9 @@ fn setup_cache( cache } -/// Full pipeline fn bench_full_chunk_generation(c: &mut Criterion) { let dimension = Dimension::OVERWORLD; - let world_gen = get_world_gen(SEED, dimension.clone()); + let world_gen = make_world_gen(); let block_registry = Arc::new(BlockRegistry); c.bench_function("full_chunk_generation", |b| { @@ -118,9 +118,8 @@ fn bench_full_chunk_generation(c: &mut Criterion) { }); } -/// Per-phase benchmarks fn bench_biomes_generation(c: &mut Criterion) { - let world_gen = get_world_gen(SEED, Dimension::OVERWORLD); + let world_gen = make_world_gen(); let block_registry = Arc::new(BlockRegistry); c.bench_function("biomes_generation", |b| { @@ -141,7 +140,7 @@ fn bench_biomes_generation(c: &mut Criterion) { } fn bench_structure_starts_generation(c: &mut Criterion) { - let world_gen = get_world_gen(SEED, Dimension::OVERWORLD); + let world_gen = make_world_gen(); let block_registry = Arc::new(BlockRegistry); c.bench_function("structure_starts_generation", |b| { @@ -168,7 +167,7 @@ fn bench_structure_starts_generation(c: &mut Criterion) { } fn bench_structure_references_generation(c: &mut Criterion) { - let world_gen = get_world_gen(SEED, Dimension::OVERWORLD); + let world_gen = make_world_gen(); let block_registry = Arc::new(BlockRegistry); c.bench_function("structure_references_generation", |b| { @@ -195,7 +194,7 @@ fn bench_structure_references_generation(c: &mut Criterion) { } fn bench_noise_generation(c: &mut Criterion) { - let world_gen = get_world_gen(SEED, Dimension::OVERWORLD); + let world_gen = make_world_gen(); let block_registry = Arc::new(BlockRegistry); c.bench_function("noise_generation", |b| { @@ -216,7 +215,7 @@ fn bench_noise_generation(c: &mut Criterion) { } fn bench_surface_generation(c: &mut Criterion) { - let world_gen = get_world_gen(SEED, Dimension::OVERWORLD); + let world_gen = make_world_gen(); let block_registry = Arc::new(BlockRegistry); c.bench_function("surface_generation", |b| { @@ -243,7 +242,7 @@ fn bench_surface_generation(c: &mut Criterion) { } fn bench_carvers_generation(c: &mut Criterion) { - let world_gen = get_world_gen(SEED, Dimension::OVERWORLD); + let world_gen = make_world_gen(); let block_registry = Arc::new(BlockRegistry); c.bench_function("carvers_generation", |b| { @@ -270,7 +269,7 @@ fn bench_carvers_generation(c: &mut Criterion) { } fn bench_features_generation(c: &mut Criterion) { - let world_gen = get_world_gen(SEED, Dimension::OVERWORLD); + let world_gen = make_world_gen(); let block_registry = Arc::new(BlockRegistry); c.bench_function("features_generation", |b| { @@ -297,7 +296,7 @@ fn bench_features_generation(c: &mut Criterion) { } fn bench_lighting_generation(c: &mut Criterion) { - let world_gen = get_world_gen(SEED, Dimension::OVERWORLD); + let world_gen = make_world_gen(); let block_registry = Arc::new(BlockRegistry); c.bench_function("lighting_generation", |b| { diff --git a/pumpkin-world/benches/chunk_gen_concurrent.rs b/pumpkin-world/benches/chunk_gen_concurrent.rs index 473b4ce9a..e913f186e 100644 --- a/pumpkin-world/benches/chunk_gen_concurrent.rs +++ b/pumpkin-world/benches/chunk_gen_concurrent.rs @@ -63,7 +63,13 @@ fn bench_concurrent_chunk_generation(c: &mut Criterion) { .build() .expect("Failed to build rayon thread pool"); - let world_gen = Arc::new(get_world_gen(SEED, Dimension::OVERWORLD)); + let world_gen = Arc::new(get_world_gen( + SEED, + Dimension::OVERWORLD, + false, + Vec::new(), + String::new(), + )); let block_registry = Arc::new(BlockRegistry); let bench_name = format!( diff --git a/pumpkin-world/src/biome/mod.rs b/pumpkin-world/src/biome/mod.rs index 26683c9fe..d17bea7d9 100644 --- a/pumpkin-world/src/biome/mod.rs +++ b/pumpkin-world/src/biome/mod.rs @@ -79,7 +79,7 @@ mod test { #[test] fn wide_area_surface() { - use crate::generation::generator::{GeneratorInit, VanillaGenerator}; + use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator}; use crate::generation::noise::router::multi_noise_sampler::{ MultiNoiseSampler, MultiNoiseSamplerBuilderOptions, }; @@ -96,13 +96,19 @@ mod test { read_data_from_file!("../../assets/biome_no_blend_no_beard_0.json"); let seed = 0; - let generator = VanillaGenerator::new(Seed(seed as u64), Dimension::OVERWORLD); + let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new( + Seed(seed as u64), + Dimension::OVERWORLD, + ))); + let WorldGenerator::Noise(generator) = &world_gen else { + unreachable!() + }; for data in expected_data { let chunk_x = data.x; let chunk_z = data.z; - let mut chunk = ProtoChunk::new(chunk_x, chunk_z, &generator); + let mut chunk = ProtoChunk::new(chunk_x, chunk_z, &world_gen); // Create MultiNoiseSampler for populate_biomes diff --git a/pumpkin-world/src/biome/multi_noise.rs b/pumpkin-world/src/biome/multi_noise.rs index ca85253ae..26989fb68 100644 --- a/pumpkin-world/src/biome/multi_noise.rs +++ b/pumpkin-world/src/biome/multi_noise.rs @@ -45,7 +45,7 @@ mod test { #[test] fn sample_value() { - use crate::generation::generator::{GeneratorInit, VanillaGenerator}; + use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator}; use crate::generation::noise::router::multi_noise_sampler::{ MultiNoiseSampler, MultiNoiseSamplerBuilderOptions, }; @@ -59,9 +59,16 @@ mod test { let chunk_x = 0; let chunk_z = 0; - let generator = VanillaGenerator::new(Seed(seed as u64), Dimension::OVERWORLD); + let generator = Box::new(VanillaGenerator::new( + Seed(seed as u64), + Dimension::OVERWORLD, + )); + let world_gen = WorldGenerator::Noise(generator); + let WorldGenerator::Noise(generator) = &world_gen else { + unreachable!() + }; - let _chunk = ProtoChunk::new(chunk_x, chunk_z, &generator); + let _chunk = ProtoChunk::new(chunk_x, chunk_z, &world_gen); let start_x = chunk_pos::start_block_x(chunk_x); let start_z = chunk_pos::start_block_z(chunk_z); diff --git a/pumpkin-world/src/chunk/format/anvil.rs b/pumpkin-world/src/chunk/format/anvil.rs index 21cebd50b..e126eda68 100644 --- a/pumpkin-world/src/chunk/format/anvil.rs +++ b/pumpkin-world/src/chunk/format/anvil.rs @@ -922,7 +922,7 @@ mod tests { let _ = env_logger::try_init(); - let generator = get_world_gen(Seed(0), Dimension::Overworld); + let generator = get_world_gen(Seed(0), Dimension::Overworld, false, Vec::new(), String::new()); let temp_dir = TempDir::new().unwrap(); let level_folder = LevelFolder { @@ -1198,7 +1198,7 @@ mod tests { let _ = env_logger::try_init(); - let generator = get_world_gen(Seed(0), Dimension::Overworld); + let generator = get_world_gen(Seed(0), Dimension::Overworld, false, Vec::new(), String::new()); let temp_dir = TempDir::new().unwrap(); let level_folder = LevelFolder { diff --git a/pumpkin-world/src/chunk/format/mod.rs b/pumpkin-world/src/chunk/format/mod.rs index 04b79195f..c76ca16c8 100644 --- a/pumpkin-world/src/chunk/format/mod.rs +++ b/pumpkin-world/src/chunk/format/mod.rs @@ -78,9 +78,16 @@ impl ChunkData { chunk_data: &[u8], position: Vector2, ) -> Result { - 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::(std::io::Cursor::new(chunk_data)) + } else { pumpkin_nbt::from_bytes_unnamed::(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 { return Err(ChunkParsingError::ErrorDeserializingChunk(format!( @@ -222,7 +229,7 @@ impl ChunkData { }; let mut result = Vec::new(); - pumpkin_nbt::to_bytes_unnamed(&nbt_ref, &mut result) + pumpkin_nbt::to_bytes(&nbt_ref, &mut result) .map_err(ChunkSerializingError::ErrorSerializingChunk)?; Ok(result.into()) @@ -272,9 +279,16 @@ impl ChunkEntityData { chunk_data: &[u8], position: Vector2, ) -> Result { - 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::(std::io::Cursor::new(chunk_data)) + } else { pumpkin_nbt::from_bytes_unnamed::(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 || chunk_entity_data.position[1] != position.y @@ -304,7 +318,7 @@ impl ChunkEntityData { }; let mut result = Vec::new(); - pumpkin_nbt::to_bytes_unnamed(&nbt, &mut result) + pumpkin_nbt::to_bytes(&nbt, &mut result) .map_err(ChunkSerializingError::ErrorSerializingChunk)?; Ok(result.into()) } @@ -476,12 +490,13 @@ struct ChunkNbt { status: ChunkStatus, #[serde(rename = "sections")] sections: Vec, + #[serde(default)] heightmaps: ChunkHeightmaps, - #[serde(rename = "block_ticks")] + #[serde(rename = "block_ticks", default)] block_ticks: Vec>, - #[serde(rename = "fluid_ticks")] + #[serde(rename = "fluid_ticks", default)] fluid_ticks: Vec>, - #[serde(rename = "block_entities")] + #[serde(rename = "block_entities", default)] block_entities: Vec, #[serde(rename = "isLightOn", default)] light_correct: bool, diff --git a/pumpkin-world/src/chunk_system/generation.rs b/pumpkin-world/src/chunk_system/generation.rs index b779000a3..ae0cc9218 100644 --- a/pumpkin-world/src/chunk_system/generation.rs +++ b/pumpkin-world/src/chunk_system/generation.rs @@ -1,7 +1,7 @@ use pumpkin_data::dimension::Dimension; use crate::ProtoChunk; -use crate::generation::generator::VanillaGenerator; +use crate::generation::generator::WorldGenerator; use crate::world::WorldPortalExt; use pumpkin_config::lighting::LightingEngineConfig; @@ -10,7 +10,7 @@ use super::{Cache, Chunk, StagedChunkEnum}; pub fn generate_single_chunk( _dimension: &Dimension, _biome_mixer_seed: i64, - generator: &VanillaGenerator, + generator: &WorldGenerator, block_registry: &dyn WorldPortalExt, chunk_x: i32, chunk_z: i32, @@ -117,13 +117,13 @@ mod tests { let dimension = Dimension::OVERWORLD; let seed = Seed(42); let block_registry = Arc::new(BlockRegistry); - let world_gen = get_world_gen(seed, dimension.clone()); - let biome_mixer_seed = hash_seed(world_gen.random_config.seed); + let world_gen = get_world_gen(seed, dimension.clone(), false, Vec::new(), String::new()); + let biome_mixer_seed = hash_seed(world_gen.seed()); let _ = generate_single_chunk( &dimension, biome_mixer_seed, - &world_gen, + &*world_gen, block_registry.as_ref(), 0, 0, @@ -136,13 +136,13 @@ mod tests { let dimension = Dimension::OVERWORLD; let seed = Seed(1_782_124_772_053_846_960); let block_registry = Arc::new(BlockRegistry); - let world_gen = get_world_gen(seed, dimension.clone()); - let biome_mixer_seed = hash_seed(world_gen.random_config.seed); + let world_gen = get_world_gen(seed, dimension.clone(), false, Vec::new(), String::new()); + let biome_mixer_seed = hash_seed(world_gen.seed()); let chunk = generate_single_chunk( &dimension, biome_mixer_seed, - &world_gen, + &*world_gen, block_registry.as_ref(), 31, -12, diff --git a/pumpkin-world/src/chunk_system/generation_cache.rs b/pumpkin-world/src/chunk_system/generation_cache.rs index ac797f024..acf1f200f 100644 --- a/pumpkin-world/src/chunk_system/generation_cache.rs +++ b/pumpkin-world/src/chunk_system/generation_cache.rs @@ -350,7 +350,7 @@ impl Cache { pub fn advance( &mut self, stage: StagedChunkEnum, - generator: &generator::VanillaGenerator, + generator: &generator::WorldGenerator, block_registry: &dyn WorldPortalExt, lighting_config: &LightingEngineConfig, ) { @@ -362,35 +362,74 @@ impl Cache { } match stage { StagedChunkEnum::Empty => panic!("empty stage"), - StagedChunkEnum::StructureStart => { - self.chunks[mid] - .get_proto_chunk_mut() - .set_structure_starts(generator); - } - StagedChunkEnum::StructureReferences => { - self.chunks[mid] - .get_proto_chunk_mut() - .set_structure_references(generator); - } - StagedChunkEnum::Biomes => self.chunks[mid] - .get_proto_chunk_mut() - .step_to_biomes(generator), - StagedChunkEnum::Noise => self.chunks[mid] - .get_proto_chunk_mut() - .step_to_noise(generator), - StagedChunkEnum::Surface => self.chunks[mid] - .get_proto_chunk_mut() - .step_to_surface(generator), - StagedChunkEnum::Carvers => self.chunks[mid] - .get_proto_chunk_mut() - .step_to_carvers(generator), - StagedChunkEnum::Features => { - ProtoChunk::generate_features_and_structure( - self, - block_registry, - &generator.random_config, - ); - } + StagedChunkEnum::StructureStart => match generator { + generator::WorldGenerator::Noise(noise_gen) => { + self.chunks[mid] + .get_proto_chunk_mut() + .set_structure_starts(noise_gen); + } + generator::WorldGenerator::Flat(_) => {} + }, + StagedChunkEnum::StructureReferences => match generator { + generator::WorldGenerator::Noise(noise_gen) => { + self.chunks[mid] + .get_proto_chunk_mut() + .set_structure_references(noise_gen); + } + generator::WorldGenerator::Flat(_) => {} + }, + StagedChunkEnum::Biomes => match generator { + generator::WorldGenerator::Noise(noise_gen) => { + self.chunks[mid] + .get_proto_chunk_mut() + .step_to_biomes(noise_gen); + } + generator::WorldGenerator::Flat(flat_gen) => { + flat_gen.step_to_biomes(self.chunks[mid].get_proto_chunk_mut()); + } + }, + StagedChunkEnum::Noise => match generator { + generator::WorldGenerator::Noise(noise_gen) => { + self.chunks[mid] + .get_proto_chunk_mut() + .step_to_noise(noise_gen); + } + generator::WorldGenerator::Flat(flat_gen) => { + flat_gen.step_to_noise(self.chunks[mid].get_proto_chunk_mut()); + } + }, + StagedChunkEnum::Surface => match generator { + generator::WorldGenerator::Noise(noise_gen) => { + self.chunks[mid] + .get_proto_chunk_mut() + .step_to_surface(noise_gen); + } + generator::WorldGenerator::Flat(flat_gen) => { + flat_gen.step_to_surface(self.chunks[mid].get_proto_chunk_mut()); + } + }, + StagedChunkEnum::Carvers => match generator { + generator::WorldGenerator::Noise(noise_gen) => { + self.chunks[mid] + .get_proto_chunk_mut() + .step_to_carvers(noise_gen); + } + generator::WorldGenerator::Flat(flat_gen) => { + flat_gen.step_to_carvers(self.chunks[mid].get_proto_chunk_mut()); + } + }, + StagedChunkEnum::Features => match generator { + generator::WorldGenerator::Noise(noise_gen) => { + ProtoChunk::generate_features_and_structure( + self, + block_registry, + &noise_gen.random_config, + ); + } + generator::WorldGenerator::Flat(_) => { + self.chunks[mid].get_proto_chunk_mut().stage = StagedChunkEnum::Features; + } + }, StagedChunkEnum::Lighting => { let mut engine = crate::lighting::LightEngine::new(); engine.initialize_light(self, lighting_config); @@ -410,7 +449,7 @@ impl Cache { let chunk = self.chunks[mid].get_proto_chunk_mut(); debug_assert_eq!(chunk.stage, StagedChunkEnum::Spawn); chunk.stage = StagedChunkEnum::Full; - self.chunks[mid].upgrade_to_level_chunk(&generator.dimension, lighting_config); + self.chunks[mid].upgrade_to_level_chunk(generator.dimension(), lighting_config); } StagedChunkEnum::None => {} } diff --git a/pumpkin-world/src/chunk_system/schedule.rs b/pumpkin-world/src/chunk_system/schedule.rs index ea90f0c54..b724e5be9 100644 --- a/pumpkin-world/src/chunk_system/schedule.rs +++ b/pumpkin-world/src/chunk_system/schedule.rs @@ -1272,7 +1272,7 @@ impl GenerationSchedule { let send_chunk = self.send_chunk.clone(); let level = level.clone(); let settings = - GenerationSettings::from_dimension(&level.world_gen.dimension); + GenerationSettings::from_dimension(level.world_gen.dimension()); pool.spawn(move || { let result = crate::chunk_system::worker_logic::run_generation( diff --git a/pumpkin-world/src/chunk_system/worker_logic.rs b/pumpkin-world/src/chunk_system/worker_logic.rs index a7060cc10..b7e0a9781 100644 --- a/pumpkin-world/src/chunk_system/worker_logic.rs +++ b/pumpkin-world/src/chunk_system/worker_logic.rs @@ -179,7 +179,10 @@ pub async fn io_write_work(recv: AsyncRx>, level: Arc vec.push((pos, chunk)), Chunk::Proto(chunk) => { let mut temp = Chunk::Proto(chunk); - temp.upgrade_to_level_chunk(&level.world_gen.dimension, &level.lighting_config); + temp.upgrade_to_level_chunk( + level.world_gen.dimension(), + &level.lighting_config, + ); let Chunk::Level(chunk) = temp else { panic!() }; vec.push((pos, chunk)); } @@ -262,7 +265,7 @@ pub fn generation_work( send: &crossfire::compat::MTx<(ChunkPos, RecvChunk)>, level: &Arc, ) { - let settings = GenerationSettings::from_dimension(&level.world_gen.dimension); + let settings = GenerationSettings::from_dimension(level.world_gen.dimension()); loop { let Ok((pos, cache, stage)) = recv.recv() else { diff --git a/pumpkin-world/src/generation/carver/mod.rs b/pumpkin-world/src/generation/carver/mod.rs index 7752058d8..a001b3f7f 100644 --- a/pumpkin-world/src/generation/carver/mod.rs +++ b/pumpkin-world/src/generation/carver/mod.rs @@ -365,11 +365,14 @@ fn with_carve_run_options( ) where F: FnOnce(&mut CarveRun<'_, '_>), { - use crate::generation::generator::{GeneratorInit, VanillaGenerator}; + use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator}; use pumpkin_util::world_seed::Seed; - let generator = VanillaGenerator::new(Seed(42), dimension); - let mut chunk = ProtoChunk::new(0, 0, &generator); + let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new(Seed(42), dimension))); + let WorldGenerator::Noise(generator) = &world_gen else { + unreachable!() + }; + let mut chunk = ProtoChunk::new(0, 0, &world_gen); let start_x = crate::generation::positions::chunk_pos::start_block_x(chunk.x); let start_z = crate::generation::positions::chunk_pos::start_block_z(chunk.z); diff --git a/pumpkin-world/src/generation/generator/flat.rs b/pumpkin-world/src/generation/generator/flat.rs new file mode 100644 index 000000000..37c1263cb --- /dev/null +++ b/pumpkin-world/src/generation/generator/flat.rs @@ -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, + pub biome: String, +} + +impl FlatGenerator { + #[must_use] + pub const fn new( + seed: Seed, + dimension: Dimension, + layers: Vec, + 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; + } +} diff --git a/pumpkin-world/src/generation/generator/mod.rs b/pumpkin-world/src/generation/generator/mod.rs index 669d2b596..dc4ec55cb 100644 --- a/pumpkin-world/src/generation/generator/mod.rs +++ b/pumpkin-world/src/generation/generator/mod.rs @@ -18,6 +18,47 @@ pub trait GeneratorInit { use pumpkin_data::structures::{StructurePlacementCalculator, StructureSet}; use rustc_hash::FxHashMap; +pub mod flat; + +#[derive(Clone, Debug)] +pub struct FlatLayer { + pub block: String, + pub height: i32, +} + +pub enum WorldGenerator { + Noise(Box), + Flat(flat::FlatGenerator), +} + +impl WorldGenerator { + #[must_use] + pub const fn dimension(&self) -> &Dimension { + match self { + Self::Noise(noise_gen) => &noise_gen.dimension, + Self::Flat(flat_gen) => &flat_gen.dimension, + } + } + + #[must_use] + pub const fn seed(&self) -> u64 { + match self { + Self::Noise(noise_gen) => noise_gen.random_config.seed, + Self::Flat(flat_gen) => flat_gen.seed, + } + } + + #[must_use] + pub const fn global_structure_cache( + &self, + ) -> Option<&crate::generation::structure::placement::GlobalStructureCache> { + match self { + Self::Noise(noise_gen) => Some(&noise_gen.global_structure_cache), + Self::Flat(_) => None, + } + } +} + pub struct VanillaGenerator { pub random_config: GlobalRandomConfig, pub base_router: ProtoNoiseRouters, diff --git a/pumpkin-world/src/generation/mod.rs b/pumpkin-world/src/generation/mod.rs index 12b2a7cf3..35766d674 100644 --- a/pumpkin-world/src/generation/mod.rs +++ b/pumpkin-world/src/generation/mod.rs @@ -25,9 +25,22 @@ use pumpkin_util::{ }; #[must_use] -pub fn get_world_gen(seed: Seed, dimension: Dimension) -> Box { - // TODO decide which WorldGenerator to pick based on config. - Box::new(VanillaGenerator::new(seed, dimension)) +pub fn get_world_gen( + seed: Seed, + dimension: Dimension, + is_flat: bool, + flat_layers: Vec, + flat_biome: String, +) -> Box { + 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 { diff --git a/pumpkin-world/src/generation/proto_chunk.rs b/pumpkin-world/src/generation/proto_chunk.rs index 46a8b4027..4363ad4e4 100644 --- a/pumpkin-world/src/generation/proto_chunk.rs +++ b/pumpkin-world/src/generation/proto_chunk.rs @@ -176,17 +176,28 @@ impl TerrainCache { impl ProtoChunk { #[must_use] - pub fn new(x: i32, z: i32, generator: &super::generator::VanillaGenerator) -> Self { - let dimension = &generator.dimension; + pub fn new(x: i32, z: i32, generator: &super::generator::WorldGenerator) -> Self { + let dimension = generator.dimension(); let height = dimension.logical_height as u16; let section_count = (height as usize) / 16; + let default_block = match generator { + super::generator::WorldGenerator::Noise(noise_gen) => noise_gen.default_block, + super::generator::WorldGenerator::Flat(_) => Block::AIR.default_state, + }; + let biome_mixer_seed = match generator { + super::generator::WorldGenerator::Noise(noise_gen) => noise_gen.biome_mixer_seed, + super::generator::WorldGenerator::Flat(flat_gen) => { + crate::biome::hash_seed(flat_gen.seed) + } + }; + let default_heightmap = [i16::MIN; CHUNK_AREA]; Self { x, z, - default_block: generator.default_block, - biome_mixer_seed: generator.biome_mixer_seed, + default_block, + biome_mixer_seed, flat_block_map: vec![BlockStateId::AIR; CHUNK_AREA * height as usize] .into_boxed_slice(), flat_biome_map: vec![ @@ -225,7 +236,7 @@ impl ProtoChunk { #[must_use] pub fn from_chunk_data( chunk_data: &ChunkData, - generator: &super::generator::VanillaGenerator, + generator: &super::generator::WorldGenerator, ) -> Self { let mut proto_chunk = Self::new(chunk_data.x, chunk_data.z, generator); @@ -518,7 +529,6 @@ impl ProtoChunk { #[expect(clippy::too_many_lines)] pub fn step_to_noise(&mut self, generator: &super::generator::VanillaGenerator) { debug_assert_eq!(self.stage, StagedChunkEnum::StructureReferences); - let settings = generator.settings; let generation_shape = &settings.shape; let horizontal_cell_count = CHUNK_DIM / generation_shape.horizontal_cell_block_count(); @@ -717,7 +727,6 @@ impl ProtoChunk { pub fn step_to_carvers(&mut self, generator: &super::generator::VanillaGenerator) { debug_assert_eq!(self.stage, StagedChunkEnum::Surface); - super::carver::carve(self, generator); self.stage = StagedChunkEnum::Carvers; diff --git a/pumpkin-world/src/level.rs b/pumpkin-world/src/level.rs index e598de91e..44816d9a6 100644 --- a/pumpkin-world/src/level.rs +++ b/pumpkin-world/src/level.rs @@ -1,7 +1,7 @@ use crate::chunk::format::linear::LinearV2File; use crate::chunk::format::pump::PumpFile; use crate::chunk_system::{ChunkListener, ChunkLoading, GenerationSchedule, LevelChannel}; -use crate::generation::generator::VanillaGenerator; +use crate::generation::generator::WorldGenerator; use crate::lighting::DynamicLightEngine; use crate::{ chunk::{ @@ -77,7 +77,7 @@ pub struct Level { pub chunk_saver: Arc>, entity_saver: Arc>, - pub world_gen: Arc, + pub world_gen: Arc, /// Handles runtime lighting updates pub light_engine: DynamicLightEngine, @@ -125,6 +125,7 @@ pub struct LevelFolder { impl Level { #[must_use] + #[expect(clippy::too_many_lines)] pub fn from_root_folder( level_config: &LevelConfig, root_folder: PathBuf, @@ -132,11 +133,18 @@ impl Level { dimension: Dimension, gen_pool: Option>, ) -> Arc { - let (namespace, name) = match dimension.minecraft_name.split_once(':') { - Some((ns, n)) => (ns, n), - None => ("minecraft", dimension.minecraft_name), + let dim_folder = if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name + || dimension.minecraft_name == Dimension::THE_NETHER.minecraft_name + || dimension.minecraft_name == Dimension::THE_END.minecraft_name + { + root_folder.clone() + } else { + let (namespace, name) = match dimension.minecraft_name.split_once(':') { + Some((ns, n)) => (ns, n), + None => ("minecraft", dimension.minecraft_name), + }; + root_folder.join("dimensions").join(namespace).join(name) }; - let dim_folder = root_folder.join("dimensions").join(namespace).join(name); let region_folder = dim_folder.join("region"); let entities_folder = dim_folder.join("entities"); @@ -154,8 +162,62 @@ impl Level { poi_folder, }); + let main_folder = if dimension.minecraft_name == Dimension::OVERWORLD.minecraft_name { + level_folder.root_folder.clone() + } else { + level_folder + .root_folder + .parent() + .unwrap_or(&level_folder.root_folder) + .to_path_buf() + }; + + let mut is_flat = false; + let mut flat_layers = Vec::new(); + let mut flat_biome = "minecraft:plains".to_string(); + + if let Some(wgs) = crate::world_info::data_files::read_world_gen_settings(&main_folder) + && let Some(dim_settings) = wgs.dimensions.get(dimension.minecraft_name) + && dim_settings.generator.generator_type == "minecraft:flat" + { + is_flat = true; + if let Some(crate::world_info::GeneratorSettings::Compound(nbt)) = + &dim_settings.generator.settings + { + if let Some(pumpkin_nbt::tag::NbtTag::String(b)) = nbt.child_tags.get("biome") { + flat_biome = b.to_string(); + } + if let Some(pumpkin_nbt::tag::NbtTag::List(list)) = nbt.child_tags.get("layers") { + for tag in list { + if let pumpkin_nbt::tag::NbtTag::Compound(layer_compound) = tag { + let mut block = "minecraft:air".to_string(); + let mut height = 1; + if let Some(pumpkin_nbt::tag::NbtTag::String(bl)) = + layer_compound.child_tags.get("block") + { + block = bl.to_string(); + } + if let Some(pumpkin_nbt::tag::NbtTag::Int(h)) = + layer_compound.child_tags.get("height") + { + height = *h; + } + flat_layers + .push(crate::generation::generator::FlatLayer { block, height }); + } + } + } + } + } + let seed = Seed(seed as u64); - let world_gen = get_world_gen(seed, dimension).into(); + let world_gen: Arc = Arc::from(get_world_gen( + seed, + dimension, + is_flat, + flat_layers, + flat_biome, + )); let chunk_saver: Arc> = match &level_config.chunk { ChunkConfig::Linear => Arc::new(ChunkFileManager::>::new(())), diff --git a/pumpkin-world/src/lib.rs b/pumpkin-world/src/lib.rs index b26ec39fe..c719d3ca1 100644 --- a/pumpkin-world/src/lib.rs +++ b/pumpkin-world/src/lib.rs @@ -1,3 +1,5 @@ +#![allow(clippy::unreachable)] + use pumpkin_data::{ Block, BlockState, chunk_gen_settings::GenerationSettings, dimension::Dimension, }; @@ -57,15 +59,21 @@ pub fn bench_create_and_populate_noise( _terrain_cache: &TerrainCache, _default_block: &'static BlockState, ) { - use crate::generation::generator::{GeneratorInit, VanillaGenerator}; + use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator}; use crate::generation::noise::router::surface_height_sampler::{ SurfaceHeightEstimateSampler, SurfaceHeightSamplerBuilderOptions, }; use crate::generation::proto_chunk::StandardChunkFluidLevelSampler; use pumpkin_util::world_seed::Seed; - let generator = VanillaGenerator::new(Seed(random_config.seed), Dimension::OVERWORLD); - let mut chunk = ProtoChunk::new(0, 0, &generator); + let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new( + Seed(random_config.seed), + Dimension::OVERWORLD, + ))); + let WorldGenerator::Noise(generator) = &world_gen else { + unreachable!() + }; + let mut chunk = ProtoChunk::new(0, 0, &world_gen); // Create noise sampler and other required components let settings = generator.settings; @@ -119,7 +127,7 @@ pub fn bench_create_and_populate_noise( ); chunk.populate_noise( - &generator, + generator, &mut noise_sampler, &generator.random_config.ore_random_deriver, &mut surface_height_estimate_sampler, @@ -133,15 +141,21 @@ pub fn bench_create_and_populate_biome( _terrain_cache: &TerrainCache, _default_block: &'static BlockState, ) { - use crate::generation::generator::{GeneratorInit, VanillaGenerator}; + use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator}; use crate::generation::noise::router::multi_noise_sampler::{ MultiNoiseSampler, MultiNoiseSamplerBuilderOptions, }; use crate::generation::{biome_coords, positions::chunk_pos}; use pumpkin_util::world_seed::Seed; - let generator = VanillaGenerator::new(Seed(random_config.seed), Dimension::OVERWORLD); - let mut chunk = ProtoChunk::new(0, 0, &generator); + let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new( + Seed(random_config.seed), + Dimension::OVERWORLD, + ))); + let WorldGenerator::Noise(generator) = &world_gen else { + unreachable!() + }; + let mut chunk = ProtoChunk::new(0, 0, &world_gen); // Create multi-noise sampler let start_x = chunk_pos::start_block_x(0); @@ -159,7 +173,7 @@ pub fn bench_create_and_populate_biome( let mut multi_noise_sampler = MultiNoiseSampler::generate(&generator.base_router.multi_noise, &multi_noise_config); - chunk.populate_biomes(&generator, &mut multi_noise_sampler); + chunk.populate_biomes(generator, &mut multi_noise_sampler); } pub fn bench_create_and_populate_noise_with_surface( @@ -169,7 +183,7 @@ pub fn bench_create_and_populate_noise_with_surface( _terrain_cache: &TerrainCache, _default_block: &'static BlockState, ) { - use crate::generation::generator::{GeneratorInit, VanillaGenerator}; + use crate::generation::generator::{GeneratorInit, VanillaGenerator, WorldGenerator}; use crate::generation::noise::router::{ multi_noise_sampler::{MultiNoiseSampler, MultiNoiseSamplerBuilderOptions}, surface_height_sampler::{ @@ -179,8 +193,14 @@ pub fn bench_create_and_populate_noise_with_surface( use crate::generation::proto_chunk::StandardChunkFluidLevelSampler; use pumpkin_util::world_seed::Seed; - let generator = VanillaGenerator::new(Seed(random_config.seed), Dimension::OVERWORLD); - let mut chunk = ProtoChunk::new(0, 0, &generator); + let world_gen = WorldGenerator::Noise(Box::new(VanillaGenerator::new( + Seed(random_config.seed), + Dimension::OVERWORLD, + ))); + let WorldGenerator::Noise(generator) = &world_gen else { + unreachable!() + }; + let mut chunk = ProtoChunk::new(0, 0, &world_gen); // Create all required components let settings = generator.settings; @@ -241,12 +261,12 @@ pub fn bench_create_and_populate_noise_with_surface( &surface_config, ); - chunk.populate_biomes(&generator, &mut multi_noise_sampler); + chunk.populate_biomes(generator, &mut multi_noise_sampler); chunk.populate_noise( - &generator, + generator, &mut noise_sampler, &generator.random_config.ore_random_deriver, &mut surface_height_estimate_sampler, ); - chunk.build_surface(&generator, &mut surface_height_estimate_sampler); + chunk.build_surface(generator, &mut surface_height_estimate_sampler); } diff --git a/pumpkin-world/src/poi/mod.rs b/pumpkin-world/src/poi/mod.rs index c12a6cc7c..b9498a114 100644 --- a/pumpkin-world/src/poi/mod.rs +++ b/pumpkin-world/src/poi/mod.rs @@ -177,7 +177,7 @@ impl PoiRegion { /// Compress chunk data to bytes fn compress_chunk_data(chunk_data: &PoiChunkData) -> std::io::Result> { let mut uncompressed = Vec::new(); - pumpkin_nbt::to_bytes_unnamed(chunk_data, &mut uncompressed) + pumpkin_nbt::to_bytes(chunk_data, &mut uncompressed) .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?; let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); @@ -191,8 +191,16 @@ impl PoiRegion { let mut uncompressed = Vec::new(); decoder.read_to_end(&mut uncompressed)?; - pumpkin_nbt::from_bytes_unnamed(Cursor::new(uncompressed)) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) + let is_named = uncompressed.len() >= 3 + && uncompressed[0] == 0x0a + && uncompressed[1] == 0x00 + && uncompressed[2] == 0x00; + if is_named { + pumpkin_nbt::from_bytes(Cursor::new(uncompressed)) + } else { + pumpkin_nbt::from_bytes_unnamed(Cursor::new(uncompressed)) + } + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string())) } pub fn save(&mut self, path: &Path) -> std::io::Result<()> { diff --git a/pumpkin-world/src/world_info/anvil.rs b/pumpkin-world/src/world_info/anvil.rs index 23331284a..9bd52ab20 100644 --- a/pumpkin-world/src/world_info/anvil.rs +++ b/pumpkin-world/src/world_info/anvil.rs @@ -12,6 +12,12 @@ use serde::{Deserialize, Serialize}; use crate::world_info::{ MAXIMUM_SUPPORTED_LEVEL_VERSION, MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, MINIMUM_SUPPORTED_LEVEL_VERSION, MINIMUM_SUPPORTED_WORLD_DATA_VERSION, + data_files::{ + minecraft_data_dir, read_game_rules, read_wandering_trader, read_weather, + read_world_clocks, read_world_gen_settings, write_custom_boss_events_stub, + write_game_rules, write_scheduled_events_stub, write_wandering_trader, write_weather, + write_world_clocks, write_world_gen_settings, + }, }; use super::{LevelData, WorldInfoError, WorldInfoReader, WorldInfoWriter}; @@ -89,10 +95,43 @@ impl WorldInfoReader for AnvilLevelInfo { check_file_data_version(&buf)?; check_file_level_version(&buf)?; - let info = pumpkin_nbt::from_bytes::(Cursor::new(buf)) + let mut info = pumpkin_nbt::from_bytes::(Cursor::new(buf)) .map_err(|e| WorldInfoError::DeserializationError(e.to_string()))?; - // TODO: check version + // game_rules.dat – prefer the new file; fall back to level.dat values + if minecraft_data_dir(level_folder) + .join("game_rules.dat") + .exists() + { + info.data.game_rules = read_game_rules(level_folder); + } + + // world_gen_settings.dat + if let Some(wgs) = read_world_gen_settings(level_folder) { + info.data.world_gen_settings = wgs; + } + + // world_clocks.dat – read the overworld day_time + if minecraft_data_dir(level_folder) + .join("world_clocks.dat") + .exists() + { + let clocks = read_world_clocks(level_folder); + if let Some(overworld) = clocks.clocks.get("minecraft:overworld") { + info.data.day_time = overworld.total_ticks; + } + } + + // weather.dat + if minecraft_data_dir(level_folder) + .join("weather.dat") + .exists() + { + let weather = read_weather(level_folder); + info.data.clear_weather_time = weather.clear_weather_time; + } + + // (wandering_trader.dat is not part of LevelData; stored separately when needed) Ok(info.data) } @@ -112,20 +151,76 @@ impl WorldInfoWriter for AnvilLevelInfo { level_data.last_played = since_the_epoch.as_millis() as i64; let level = LevelDat { data: level_data }; - // open file + // ── Write level.dat ─────────────────────────────────────────────────── let path = level_folder.join(LEVEL_DAT_FILE_NAME); let world_info_file = File::create(path)?; - // write compressed data into file let compression_writer = GzEncoder::new(world_info_file, Compression::best()); // TODO: Proper error handling pumpkin_nbt::to_bytes(&level, compression_writer) .expect("Failed to write level.dat to disk"); + + let data_version = info.data_version; + + // ── Write data/minecraft/*.dat files ───────────────────────────────── + + // game_rules.dat + if let Err(e) = write_game_rules(level_folder, &info.game_rules, data_version) { + error!("Failed to write game_rules.dat: {e}"); + } + + // world_gen_settings.dat + if let Err(e) = + write_world_gen_settings(level_folder, &info.world_gen_settings, data_version) + { + error!("Failed to write world_gen_settings.dat: {e}"); + } + + // world_clocks.dat – persist the overworld day_time; preserve other + let mut clocks = read_world_clocks(level_folder); + clocks.data_version = data_version; + clocks + .clocks + .entry("minecraft:overworld".to_string()) + .and_modify(|c| c.total_ticks = info.day_time) + .or_insert(crate::world_info::data_files::DimensionClock { + total_ticks: info.day_time, + }); + + if let Err(e) = write_world_clocks(level_folder, &clocks) { + error!("Failed to write world_clocks.dat: {e}"); + } + + // weather.dat + let mut weather = read_weather(level_folder); + weather.clear_weather_time = info.clear_weather_time; + weather.data_version = data_version; + if let Err(e) = write_weather(level_folder, &weather) { + error!("Failed to write weather.dat: {e}"); + } + + // wandering_trader.dat (stub / load-save) + let mut wandering_trader = read_wandering_trader(level_folder); + wandering_trader.data_version = data_version; + if let Err(e) = write_wandering_trader(level_folder, &wandering_trader) { + error!("Failed to write wandering_trader.dat: {e}"); + } + + // custom_boss_events.dat + if let Err(e) = write_custom_boss_events_stub(level_folder, data_version) { + error!("Failed to write custom_boss_events.dat: {e}"); + } + + // scheduled_events.dat + if let Err(e) = write_scheduled_events_stub(level_folder, data_version) { + error!("Failed to write scheduled_events.dat: {e}"); + } + Ok(()) } } -#[derive(Debug, PartialEq, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] pub struct LevelDat { // This tag contains all the level data. #[serde(rename = "Data")] @@ -267,7 +362,13 @@ mod test { let level_dat_again: LevelDat = from_bytes(Cursor::new(serialized)).expect("Failed to decode from bytes"); - assert_eq!(level_dat_again, *LEVEL_DAT); + let mut expected = (*LEVEL_DAT).clone(); + expected.data.game_rules = GameRuleRegistry::default(); + expected.data.world_gen_settings = WorldGenSettings::default(); + expected.data.day_time = 0; + expected.data.clear_weather_time = 0; + + assert_eq!(level_dat_again, expected); } #[test] diff --git a/pumpkin-world/src/world_info/data_files.rs b/pumpkin-world/src/world_info/data_files.rs new file mode 100644 index 000000000..2c539dfa0 --- /dev/null +++ b/pumpkin-world/src/world_info/data_files.rs @@ -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 { + #[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, + 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 `/data/minecraft/` directory exists. +pub fn ensure_minecraft_data_dir(level_folder: &Path) -> Result { + 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::, _>(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 { + 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::, _>(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::, _>(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())) +} diff --git a/pumpkin-world/src/world_info/mod.rs b/pumpkin-world/src/world_info/mod.rs index 3df962c4a..20e493261 100644 --- a/pumpkin-world/src/world_info/mod.rs +++ b/pumpkin-world/src/world_info/mod.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; pub mod anvil; +pub mod data_files; // Constraint: disk biome palette serialization changed in 1.21.5 pub const MINIMUM_SUPPORTED_WORLD_DATA_VERSION: i32 = 4435; // 1.21.9 @@ -48,21 +49,14 @@ pub struct LevelData { pub border_warning_blocks: f64, #[serde(default = "default_border_warning_time")] pub border_warning_time: f64, - #[serde(rename = "clearWeatherTime", default)] - pub clear_weather_time: i32, #[serde(default = "default_data_packs")] pub data_packs: DataPacks, pub data_version: i32, - #[serde(default)] - pub day_time: i64, #[serde(with = "serde_enum_as_integer", default = "default_difficulty")] pub difficulty: Difficulty, #[serde(default)] pub difficulty_locked: bool, #[serde(default)] - pub game_rules: GameRuleRegistry, - pub world_gen_settings: WorldGenSettings, - #[serde(default)] pub last_played: i64, #[serde(default = "default_level_name")] pub level_name: String, @@ -82,6 +76,26 @@ pub struct LevelData { pub level_version: i32, #[serde(rename = "map_id", default)] pub map_id: i32, + + // These are NOT serialized to level.dat, but are still deserialized from it if present. + // They are loaded and saved by AnvilLevelInfo via the data_files module. + /// Game rules – persisted to `data/minecraft/game_rules.dat`. + #[serde(skip_serializing, default)] + pub game_rules: GameRuleRegistry, + + /// World generation settings – persisted to `data/minecraft/world_gen_settings.dat`. + #[serde(skip_serializing, default)] + pub world_gen_settings: WorldGenSettings, + + /// In-game time of day (overworld dimension clock). + /// Persisted to `data/minecraft/world_clocks.dat`. + #[serde(skip_serializing, default)] + pub day_time: i64, + + /// Remaining ticks of forced-clear weather. + /// Persisted to `data/minecraft/weather.dat`. + #[serde(rename = "clearWeatherTime", skip_serializing, default)] + pub clear_weather_time: i32, } const DEFAULT_BORDER_DAMAGE_PER_BLOCK: f64 = 0.2; @@ -134,6 +148,13 @@ pub struct WorldGenSettings { pub dimensions: Dimensions, } +impl Default for WorldGenSettings { + fn default() -> Self { + // Use seed 0 as placeholder; actual seed comes from config or world_gen_settings.dat + Self::new(pumpkin_util::world_seed::Seed(0)) + } +} + pub type Dimensions = HashMap; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Dimension { @@ -144,7 +165,8 @@ pub struct Dimension { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Generator { - pub settings: GeneratorSettings, + #[serde(default)] + pub settings: Option, #[serde(default)] pub biome_source: Option, #[serde(rename = "type")] @@ -227,7 +249,9 @@ impl WorldGenSettings { "minecraft:overworld".to_string(), Dimension { generator: Generator { - settings: GeneratorSettings::Reference("minecraft:overworld".to_string()), + settings: Some(GeneratorSettings::Reference( + "minecraft:overworld".to_string(), + )), biome_source: Some(BiomeSource::WithPreset { preset: "minecraft:overworld".to_string(), biome_type: "minecraft:multi_noise".to_string(), @@ -241,7 +265,7 @@ impl WorldGenSettings { "minecraft:the_nether".to_string(), Dimension { generator: Generator { - settings: GeneratorSettings::Reference("minecraft:nether".to_string()), + settings: Some(GeneratorSettings::Reference("minecraft:nether".to_string())), biome_source: Some(BiomeSource::WithPreset { preset: "minecraft:nether".to_string(), biome_type: "minecraft:multi_noise".to_string(), @@ -255,7 +279,7 @@ impl WorldGenSettings { "minecraft:the_end".to_string(), Dimension { generator: Generator { - settings: GeneratorSettings::Reference("minecraft:end".to_string()), + settings: Some(GeneratorSettings::Reference("minecraft:end".to_string())), biome_source: Some(BiomeSource::Simple { biome_type: "minecraft:the_end".to_string(), }), @@ -310,14 +334,10 @@ impl LevelData { border_size_lerp_time: 0, border_warning_blocks: DEFAULT_BORDER_WARNING_BLOCKS, border_warning_time: DEFAULT_BORDER_WARNING_TIME, - clear_weather_time: -1, data_packs: default_data_packs(), data_version: MAXIMUM_SUPPORTED_WORLD_DATA_VERSION, - day_time: 0, difficulty: DEFAULT_DIFFICULTY, difficulty_locked: false, - game_rules: GameRuleRegistry::default(), - world_gen_settings: WorldGenSettings::new(seed), last_played: -1, level_name: DEFAULT_LEVEL_NAME.to_string(), spawn_x: 0, @@ -328,6 +348,11 @@ impl LevelData { world_version: WorldVersion::default(), level_version: MAXIMUM_SUPPORTED_LEVEL_VERSION, map_id: 0, + // fields now in data/minecraft/*.dat + game_rules: GameRuleRegistry::default(), + world_gen_settings: WorldGenSettings::new(seed), + day_time: 0, + clear_weather_time: -1, } } @@ -345,6 +370,8 @@ pub enum WorldInfoError { InfoNotFound, #[error("Deserialization error: {0}")] DeserializationError(String), + #[error("Serialization error: {0}")] + SerializationError(String), #[error("Unsupported world data version: {0}")] UnsupportedDataVersion(i32), #[error("Unsupported world level version: {0}")] diff --git a/pumpkin/src/block/blocks/redstone/dropper.rs b/pumpkin/src/block/blocks/redstone/dropper.rs index 710e81d36..5f75a52ad 100644 --- a/pumpkin/src/block/blocks/redstone/dropper.rs +++ b/pumpkin/src/block/blocks/redstone/dropper.rs @@ -153,7 +153,6 @@ impl BlockBehaviour for DropperBlock { fn on_scheduled_tick<'a>(&'a self, args: OnScheduledTickArgs<'a>) -> BlockFuture<'a, ()> { Box::pin(async move { if let Some(block_entity) = args.world.get_block_entity(args.position) { - // Safely extract the dropper instead of unwrap() let Some(dropper) = block_entity.as_any().downcast_ref::() else { return; diff --git a/pumpkin/src/command/args/gameprofile.rs b/pumpkin/src/command/args/gameprofile.rs index 0560486c4..45ec10192 100644 --- a/pumpkin/src/command/args/gameprofile.rs +++ b/pumpkin/src/command/args/gameprofile.rs @@ -242,10 +242,10 @@ async fn resolve_profiles_from_token( return Ok(vec![profile]); } - if server.basic_config.online_mode { + if server.advanced_config.networking.java.online_mode { match lookup_profile_by_name( raw_arg.value, - &server.advanced_config.networking.authentication, + &server.advanced_config.networking.java.authentication, ) { Ok(Some((uuid, resolved_name))) => { server diff --git a/pumpkin/src/command/argument_types/game_profile.rs b/pumpkin/src/command/argument_types/game_profile.rs index 4c474439f..cbd670063 100644 --- a/pumpkin/src/command/argument_types/game_profile.rs +++ b/pumpkin/src/command/argument_types/game_profile.rs @@ -70,10 +70,10 @@ impl GameProfileResult { return Ok(vec![profile]); } - if server.basic_config.online_mode { + if server.advanced_config.networking.java.online_mode { return match lookup_profile_by_name( name, - &server.advanced_config.networking.authentication, + &server.advanced_config.networking.java.authentication, ) { Ok(Some((uuid, resolved_name))) => { server diff --git a/pumpkin/src/command/commands/list.rs b/pumpkin/src/command/commands/list.rs index d55a3fb4f..580ae3ae5 100644 --- a/pumpkin/src/command/commands/list.rs +++ b/pumpkin/src/command/commands/list.rs @@ -37,6 +37,23 @@ impl CommandExecutor for ListCommandExecutor { ListMode::Uuids => get_player_names_and_ids(&players), }; + let max_players = context.source.output.as_player().map_or_else( + || context.server().advanced_config.networking.java.max_players, + |player| match player.client.as_ref() { + crate::net::ClientPlatform::Java(_) => { + context.server().advanced_config.networking.java.max_players + } + crate::net::ClientPlatform::Bedrock(_) => { + context + .server() + .advanced_config + .networking + .bedrock + .max_players + } + }, + ); + context .source .send_feedback( @@ -45,9 +62,7 @@ impl CommandExecutor for ListCommandExecutor { COMMANDS_LIST_PLAYERS, [ TextComponent::text(players_len.to_string()), - TextComponent::text( - context.server().basic_config.max_players.to_string(), - ), + TextComponent::text(max_players.to_string()), list, ], ), diff --git a/pumpkin/src/entity/ai/goal/beg.rs b/pumpkin/src/entity/ai/goal/beg.rs index bde49528d..016b07c4a 100644 --- a/pumpkin/src/entity/ai/goal/beg.rs +++ b/pumpkin/src/entity/ai/goal/beg.rs @@ -51,14 +51,14 @@ impl BegGoal { } fn set_begging(mob: &dyn Mob, begging: bool) { - mob.get_mob_entity() - .living_entity - .entity - .send_meta_data(&[Metadata::new( + mob.get_mob_entity().living_entity.entity.send_meta_data( + &[Metadata::new( TrackedData::INTERESTED_ID, MetaDataType::BOOLEAN, begging, - )]); + )], + None, + ); } } diff --git a/pumpkin/src/entity/area_effect_cloud.rs b/pumpkin/src/entity/area_effect_cloud.rs index 53f568196..c943cadf9 100644 --- a/pumpkin/src/entity/area_effect_cloud.rs +++ b/pumpkin/src/entity/area_effect_cloud.rs @@ -173,29 +173,35 @@ impl EntityBase for AreaEffectCloudEntity { }; // Send initial particle and radius - self.entity - .send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new( + self.entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( pumpkin_data::tracked_data::TrackedData::PARTICLE, pumpkin_data::meta_data_type::MetaDataType::PARTICLE, &meta, - )]); + )], + None, + ); - self.entity - .send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new( + self.entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( pumpkin_data::tracked_data::TrackedData::RADIUS, pumpkin_data::meta_data_type::MetaDataType::FLOAT, radius, - )]); + )], + None, + ); // Initial waiting flag let wait_time = *self.wait_time.lock().await; let is_waiting = 0 < wait_time; - self.entity - .send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new( + self.entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( pumpkin_data::tracked_data::TrackedData::WAITING, pumpkin_data::meta_data_type::MetaDataType::BOOLEAN, is_waiting, - )]); + )], + None, + ); }) } @@ -225,12 +231,14 @@ impl EntityBase for AreaEffectCloudEntity { // When the waiting period ends, notify clients so they render full particles if age == wait_time && wait_time > 0 { - self.entity - .send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new( + self.entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( pumpkin_data::tracked_data::TrackedData::WAITING, pumpkin_data::meta_data_type::MetaDataType::BOOLEAN, false, - )]); + )], + None, + ); } if age < wait_time { @@ -251,12 +259,14 @@ impl EntityBase for AreaEffectCloudEntity { // Send new radius drop(radius); - self.entity - .send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new( + self.entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( pumpkin_data::tracked_data::TrackedData::RADIUS, pumpkin_data::meta_data_type::MetaDataType::FLOAT, current_radius, - )]); + )], + None, + ); } // Tick down reapplication map @@ -380,13 +390,14 @@ impl EntityBase for AreaEffectCloudEntity { drop(radius_lock); // Send updated radius to clients - self.entity.send_meta_data(&[ - pumpkin_protocol::java::client::play::Metadata::new( + self.entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( pumpkin_data::tracked_data::TrackedData::RADIUS, pumpkin_data::meta_data_type::MetaDataType::FLOAT, current_radius, - ), - ]); + )], + None, + ); } // Apply duration-on-use (shorten lifespan) diff --git a/pumpkin/src/entity/breath.rs b/pumpkin/src/entity/breath.rs index e3047a4f6..b2d4eae9d 100644 --- a/pumpkin/src/entity/breath.rs +++ b/pumpkin/src/entity/breath.rs @@ -152,11 +152,21 @@ impl BreathManager { pub fn send_air_supply(&self, player: &Player) { let air = self.air_supply.load(Ordering::Relaxed).clamp(0, MAX_AIR); - player.get_entity().send_meta_data(&[Metadata::new( - TrackedData::AIR_SUPPLY_ID, - MetaDataType::INT, - VarInt(air), - )]); + let mut bedrock_meta = + pumpkin_protocol::bedrock::client::set_actor_data::EntityMetadata::new(); + bedrock_meta.set( + pumpkin_protocol::bedrock::client::set_actor_data::entity_data_key::AIR_SUPPLY, + pumpkin_protocol::bedrock::client::set_actor_data::MetadataValue::Short(air as i16), + ); + + player.get_entity().send_meta_data( + &[Metadata::new( + TrackedData::AIR_SUPPLY_ID, + MetaDataType::INT, + VarInt(air), + )], + Some(&bedrock_meta), + ); } pub fn reset(&self, player: &Player) { diff --git a/pumpkin/src/entity/decoration/end_crystal.rs b/pumpkin/src/entity/decoration/end_crystal.rs index 342cd8308..ef7ed5fd4 100644 --- a/pumpkin/src/entity/decoration/end_crystal.rs +++ b/pumpkin/src/entity/decoration/end_crystal.rs @@ -22,11 +22,14 @@ impl EndCrystalEntity { impl EndCrystalEntity { pub fn set_show_bottom(&self, show_bottom: bool) { - self.entity.send_meta_data(&[Metadata::new( - TrackedData::SHOW_BOTTOM, - MetaDataType::BOOLEAN, - show_bottom, - )]); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::SHOW_BOTTOM, + MetaDataType::BOOLEAN, + show_bottom, + )], + None, + ); } } diff --git a/pumpkin/src/entity/falling.rs b/pumpkin/src/entity/falling.rs index 85cee8cbf..fa1c6e4d4 100644 --- a/pumpkin/src/entity/falling.rs +++ b/pumpkin/src/entity/falling.rs @@ -93,11 +93,14 @@ impl EntityBase for FallingEntity { fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async move { - self.entity.send_meta_data(&[Metadata::new( - TrackedData::START_POS, - MetaDataType::BLOCK_POS, - self.entity.block_pos.load(), - )]); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::START_POS, + MetaDataType::BLOCK_POS, + self.entity.block_pos.load(), + )], + None, + ); }) } diff --git a/pumpkin/src/entity/item.rs b/pumpkin/src/entity/item.rs index 816652c3e..30657548d 100644 --- a/pumpkin/src/entity/item.rs +++ b/pumpkin/src/entity/item.rs @@ -470,11 +470,14 @@ impl EntityBase for ItemEntity { fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async { - self.entity.send_meta_data(&[Metadata::new( - TrackedData::ITEM, - MetaDataType::ITEM_STACK, - &ItemStackSerializer::from(self.item_stack.lock().await.clone()), - )]); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::ITEM, + MetaDataType::ITEM_STACK, + &ItemStackSerializer::from(self.item_stack.lock().await.clone()), + )], + None, + ); }) } diff --git a/pumpkin/src/entity/living.rs b/pumpkin/src/entity/living.rs index 610a044dd..8a588b9f1 100644 --- a/pumpkin/src/entity/living.rs +++ b/pumpkin/src/entity/living.rs @@ -237,11 +237,26 @@ impl LivingEntity { b &= !index; } self.livings_flags.store(b, Ordering::Relaxed); - self.entity.send_meta_data(&[Metadata::new( - TrackedData::LIVING_ENTITY_FLAGS, - MetaDataType::BYTE, - b, - )]); + + let bedrock_meta = (flag == Self::USING_ITEM_FLAG).then(|| { + let mut meta = pumpkin_protocol::bedrock::client::set_actor_data::EntityMetadata::new(); + meta.set_flag( + pumpkin_protocol::bedrock::client::set_actor_data::entity_data_key::FLAGS, + pumpkin_protocol::bedrock::client::set_actor_data::entity_data_flag::USING_ITEM + as u8, + value, + ); + meta + }); + + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::LIVING_ENTITY_FLAGS, + MetaDataType::BYTE, + b, + )], + bedrock_meta.as_ref(), + ); } pub async fn clear_active_hand(&self) { @@ -274,11 +289,14 @@ impl LivingEntity { let clamped = health.max(0.0).min(max_health); self.health.store(clamped); // tell everyone entities health changed - self.entity.send_meta_data(&[Metadata::new( - TrackedData::HEALTH_ID, - MetaDataType::FLOAT, - clamped, - )]); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::HEALTH_ID, + MetaDataType::FLOAT, + clamped, + )], + None, + ); } /// Returns the current maximum health for this entity @@ -328,8 +346,10 @@ impl LivingEntity { // Send absorption metadata for players (visual yellow hearts) if let Some(tracked_id) = self.player_absorption_id() { - self.entity - .send_meta_data(&[Metadata::new(tracked_id, MetaDataType::FLOAT, new_abs)]); + self.entity.send_meta_data( + &[Metadata::new(tracked_id, MetaDataType::FLOAT, new_abs)], + None, + ); } } @@ -1791,11 +1811,14 @@ impl LivingEntity { // Clear any absorption self.absorption.store(0.0); // Send health metadata - self.entity.send_meta_data(&[Metadata::new( - TrackedData::HEALTH_ID, - MetaDataType::FLOAT, - max_health, - )]); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::HEALTH_ID, + MetaDataType::FLOAT, + max_health, + )], + None, + ); self.reset_effects_and_attributes().await; diff --git a/pumpkin/src/entity/mob/creeper.rs b/pumpkin/src/entity/mob/creeper.rs index f6c2e36d7..4d2673152 100644 --- a/pumpkin/src/entity/mob/creeper.rs +++ b/pumpkin/src/entity/mob/creeper.rs @@ -86,14 +86,14 @@ impl CreeperEntity { pub fn set_fuse_speed(&self, speed: i32) { self.fuse_speed.store(speed, Ordering::Relaxed); - self.mob_entity - .living_entity - .entity - .send_meta_data(&[Metadata::new( + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( TrackedData::FUSE_ID, MetaDataType::INTEGER, VarInt(speed), - )]); + )], + None, + ); } async fn explode(&self) { @@ -219,11 +219,14 @@ impl Mob for CreeperEntity { ); self.ignited.store(true, Ordering::Relaxed); - entity.send_meta_data(&[Metadata::new( - TrackedData::IS_IGNITED, - MetaDataType::BOOLEAN, - true, - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::IS_IGNITED, + MetaDataType::BOOLEAN, + true, + )], + None, + ); if player.gamemode.load() != pumpkin_util::GameMode::Creative { // TODO: Handle DamageResult::Broken to broadcast item break and update player slot. diff --git a/pumpkin/src/entity/mob/enderman.rs b/pumpkin/src/entity/mob/enderman.rs index dff700973..1f78fb453 100644 --- a/pumpkin/src/entity/mob/enderman.rs +++ b/pumpkin/src/entity/mob/enderman.rs @@ -282,14 +282,14 @@ impl EndermanEntity { pub fn set_angry(&self, angry: bool) { self.angry.store(angry, Ordering::Relaxed); - self.mob_entity - .living_entity - .entity - .send_meta_data(&[Metadata::new( + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( TrackedData::CREEPY, MetaDataType::BOOLEAN, angry, - )]); + )], + None, + ); } pub fn is_angry(&self) -> bool { @@ -298,27 +298,27 @@ impl EndermanEntity { pub fn set_provoked(&self, provoked: bool) { self.provoked.store(provoked, Ordering::Relaxed); - self.mob_entity - .living_entity - .entity - .send_meta_data(&[Metadata::new( + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( TrackedData::STARED_AT, MetaDataType::BOOLEAN, provoked, - )]); + )], + None, + ); } pub fn set_carried_block(&self, block_state: Option) { self.carried_block.store(block_state); let value = block_state.map_or(VarInt(0), |id| VarInt(id.as_u16() as i32)); - self.mob_entity - .living_entity - .entity - .send_meta_data(&[Metadata::new( + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( TrackedData::CARRY_STATE, MetaDataType::OPTIONAL_BLOCK_STATE, value, - )]); + )], + None, + ); } pub fn get_carried_block(&self) -> Option { diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index 1c72324b1..f04dcfadf 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -165,11 +165,14 @@ impl MobEntity { if new_b != old_b { self.mob_flags.store(new_b, Ordering::Relaxed); - self.living_entity.entity.send_meta_data(&[Metadata::new( - TrackedData::MOB_FLAGS_ID, - MetaDataType::BYTE, - new_b, - )]); + self.living_entity.entity.send_meta_data( + &[Metadata::new( + TrackedData::MOB_FLAGS_ID, + MetaDataType::BYTE, + new_b, + )], + None, + ); } } @@ -493,11 +496,14 @@ pub trait Mob: EntityBase + Send + Sync { let entity = self.get_entity(); let is_baby = entity.age.load(std::sync::atomic::Ordering::Relaxed) < 0; if is_baby { - entity.send_meta_data(&[Metadata::new( - TrackedData::BABY_ID, - MetaDataType::BOOLEAN, - true, - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); } }) } diff --git a/pumpkin/src/entity/mob/shulker.rs b/pumpkin/src/entity/mob/shulker.rs index cf54ec62c..32f8f235b 100644 --- a/pumpkin/src/entity/mob/shulker.rs +++ b/pumpkin/src/entity/mob/shulker.rs @@ -110,11 +110,14 @@ impl ShulkerEntity { fn set_attach_face(&self, face: BlockDirection) { self.attach_face.store(face as u8, Ordering::Relaxed); let entity = &self.mob_entity.living_entity.entity; - entity.send_meta_data(&[Metadata::new( - TrackedData::ATTACH_FACE_ID, - MetaDataType::DIRECTION, - VarInt(face as i32), - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::ATTACH_FACE_ID, + MetaDataType::DIRECTION, + VarInt(face as i32), + )], + None, + ); } pub fn get_raw_peek(&self) -> u8 { @@ -148,11 +151,14 @@ impl ShulkerEntity { self.peek_amount.store(amount, Ordering::Relaxed); - entity.send_meta_data(&[Metadata::new( - TrackedData::PEEK_ID, - MetaDataType::BYTE, - amount, - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::PEEK_ID, + MetaDataType::BYTE, + amount, + )], + None, + ); } pub fn is_closed(&self) -> bool { diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index d7d38d8d0..4a2627f54 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -179,11 +179,16 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any { let is_baby = entity.age.load(Ordering::Relaxed) < 0; if is_baby { - entity.send_meta_data(&[Metadata::new( - TrackedData::BABY_ID, - MetaDataType::BOOLEAN, - true, - )]); + let mut bedrock_meta = EntityMetadata::new(); + bedrock_meta.set_flag(entity_data_key::FLAGS, entity_data_flag::BABY as u8, true); + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + Some(&bedrock_meta), + ); } }) } @@ -1015,20 +1020,59 @@ impl Entity { /// Sets a custom name for the entity, typically used with nametags pub fn set_custom_name(&self, name: TextComponent) { self.custom_name.store(Arc::new(Some(name.clone()))); - self.send_meta_data(&[Metadata::new( - TrackedData::CUSTOM_NAME, - MetaDataType::OPTIONAL_TEXT_COMPONENT, - Some(name), - )]); + let mut bedrock_meta = EntityMetadata::new(); + bedrock_meta.set( + entity_data_key::NAME, + MetadataValue::String(name.clone().get_text()), + ); + let visible = self.custom_name_visible.load(Ordering::Relaxed); + bedrock_meta.set_flag( + entity_data_key::FLAGS, + entity_data_flag::SHOW_NAME as u8, + visible, + ); + bedrock_meta.set_flag( + entity_data_key::FLAGS, + entity_data_flag::ALWAYS_SHOW_NAME as u8, + visible, + ); + self.send_meta_data( + &[Metadata::new( + TrackedData::CUSTOM_NAME, + MetaDataType::OPTIONAL_TEXT_COMPONENT, + Some(name), + )], + Some(&bedrock_meta), + ); } pub fn set_custom_name_visible(&self, visible: bool) { self.custom_name_visible.store(visible, Ordering::Relaxed); - self.send_meta_data(&[Metadata::new( - TrackedData::CUSTOM_NAME_VISIBLE, - MetaDataType::BOOLEAN, + let mut bedrock_meta = EntityMetadata::new(); + if let Some(name) = &**self.custom_name.load() { + bedrock_meta.set( + entity_data_key::NAME, + MetadataValue::String(name.clone().get_text()), + ); + } + bedrock_meta.set_flag( + entity_data_key::FLAGS, + entity_data_flag::SHOW_NAME as u8, visible, - )]); + ); + bedrock_meta.set_flag( + entity_data_key::FLAGS, + entity_data_flag::ALWAYS_SHOW_NAME as u8, + visible, + ); + self.send_meta_data( + &[Metadata::new( + TrackedData::CUSTOM_NAME_VISIBLE, + MetaDataType::BOOLEAN, + visible, + )], + Some(&bedrock_meta), + ); } pub fn send_velocity(&self) { @@ -2383,11 +2427,19 @@ impl Entity { // Only update and send metadata if the value changed if new_frozen_ticks != old_frozen_ticks { self.frozen_ticks.store(new_frozen_ticks, Ordering::Relaxed); - self.send_meta_data(&[Metadata::new( - TrackedData::TICKS_FROZEN, - MetaDataType::INTEGER, - VarInt(new_frozen_ticks), - )]); + let mut bedrock_meta = EntityMetadata::new(); + bedrock_meta.set( + entity_data_key::FREEZING_EFFECT_STRENGTH, + MetadataValue::Float(new_frozen_ticks as f32), + ); + self.send_meta_data( + &[Metadata::new( + TrackedData::TICKS_FROZEN, + MetaDataType::INTEGER, + VarInt(new_frozen_ticks), + )], + Some(&bedrock_meta), + ); } // Vanilla parity: full-freeze damage is tick-phase based. @@ -2639,11 +2691,14 @@ impl Entity { self.flags.fetch_and(!mask, Ordering::Relaxed) & !mask }; - self.send_meta_data(&[Metadata::new( - TrackedData::SHARED_FLAGS_ID, - MetaDataType::BYTE, - new_je_flags, - )]); + self.send_meta_data( + &[Metadata::new( + TrackedData::SHARED_FLAGS_ID, + MetaDataType::BYTE, + new_je_flags, + )], + None, + ); if let Some(bedrock_flag) = flag.to_bedrock() { let (key, index) = if bedrock_flag >= 64 { @@ -2710,25 +2765,52 @@ impl Entity { .play_sound(sound, SoundCategory::Neutral, &self.pos.load()); } - pub fn send_meta_data(&self, meta: &[Metadata]) { + pub fn send_meta_data( + &self, + meta: &[Metadata], + bedrock_meta: Option<&EntityMetadata>, + ) { let world = self.world.load(); let chunk_pos = self.chunk_pos.load(); - for player in world.players.load().iter() { - if let ClientPlatform::Java(client) = player.client.as_ref() { - // Apply Chebyshev distance check - let center = player.get_entity().chunk_pos.load(); - let view_distance = crate::world::chunker::get_view_distance(player).get() as i32; - if is_within_view_distance(chunk_pos, center, view_distance) { - let mut buf = Vec::new(); - for m in meta { - m.write(&mut buf, &client.version.load()).unwrap(); + for player in world.players.load().iter() { + match player.client.as_ref() { + ClientPlatform::Java(client) => { + // Apply Chebyshev distance check + let center = player.get_entity().chunk_pos.load(); + let view_distance = + crate::world::chunker::get_view_distance(player).get() as i32; + + if is_within_view_distance(chunk_pos, center, view_distance) { + let mut buf = Vec::new(); + for m in meta { + m.write(&mut buf, &client.version.load()).unwrap(); + } + buf.put_u8(255); + player.client.try_enqueue_packet(&CSetEntityMetadata::new( + self.entity_id.into(), + buf.into(), + )); + } + } + 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), + }); + } } - buf.put_u8(255); - player.client.try_enqueue_packet(&CSetEntityMetadata::new( - self.entity_id.into(), - buf.into(), - )); } } } @@ -2744,11 +2826,16 @@ impl Entity { self.bounding_box.store(aabb); self.entity_dimension.store(dimension); let pose = pose as i32; - self.send_meta_data(&[Metadata::new( - TrackedData::POSE, - MetaDataType::ENTITY_POSE, - VarInt(pose), - )]); + let mut bedrock_meta = EntityMetadata::new(); + bedrock_meta.set(entity_data_key::POSE_INDEX, MetadataValue::Int(pose)); + self.send_meta_data( + &[Metadata::new( + TrackedData::POSE, + MetaDataType::ENTITY_POSE, + VarInt(pose), + )], + Some(&bedrock_meta), + ); } } diff --git a/pumpkin/src/entity/passive/cat.rs b/pumpkin/src/entity/passive/cat.rs index c963af14b..c5c850164 100644 --- a/pumpkin/src/entity/passive/cat.rs +++ b/pumpkin/src/entity/passive/cat.rs @@ -137,17 +137,23 @@ impl Mob for CatEntity { let entity = self.get_entity(); let is_baby = entity.age.load(Ordering::Relaxed) < 0; if is_baby { - entity.send_meta_data(&[Metadata::new( - TrackedData::BABY_ID, - MetaDataType::BOOLEAN, - true, - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); } - entity.send_meta_data(&[Metadata::new( - TrackedData::CAT_VARIANT, - MetaDataType::CAT_VARIANT, - VarInt(self.variant.load(Ordering::Relaxed) as i32), - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::CAT_VARIANT, + MetaDataType::CAT_VARIANT, + VarInt(self.variant.load(Ordering::Relaxed) as i32), + )], + None, + ); }) } } diff --git a/pumpkin/src/entity/passive/chicken.rs b/pumpkin/src/entity/passive/chicken.rs index f3cbcdeff..a0b18727e 100644 --- a/pumpkin/src/entity/passive/chicken.rs +++ b/pumpkin/src/entity/passive/chicken.rs @@ -131,17 +131,23 @@ impl Mob for ChickenEntity { let entity = self.get_entity(); let is_baby = entity.age.load(Ordering::Relaxed) < 0; if is_baby { - entity.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new( - TrackedData::BABY_ID, - MetaDataType::BOOLEAN, - true, - )]); + entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); } - entity.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new( - TrackedData::VARIANT, - MetaDataType::CHICKEN_VARIANT, - VarInt(self.variant.load(Ordering::Relaxed) as i32), - )]); + entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( + TrackedData::VARIANT, + MetaDataType::CHICKEN_VARIANT, + VarInt(self.variant.load(Ordering::Relaxed) as i32), + )], + None, + ); }) } diff --git a/pumpkin/src/entity/passive/frog.rs b/pumpkin/src/entity/passive/frog.rs index 633fb3bba..47dda3bad 100644 --- a/pumpkin/src/entity/passive/frog.rs +++ b/pumpkin/src/entity/passive/frog.rs @@ -103,17 +103,23 @@ impl Mob for FrogEntity { let entity = self.get_entity(); let is_baby = entity.age.load(Ordering::Relaxed) < 0; if is_baby { - entity.send_meta_data(&[Metadata::new( - TrackedData::BABY_ID, - MetaDataType::BOOLEAN, - true, - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); } - entity.send_meta_data(&[Metadata::new( - TrackedData::VARIANT, - MetaDataType::FROG_VARIANT, - VarInt(self.variant.load(Ordering::Relaxed) as i32), - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::VARIANT, + MetaDataType::FROG_VARIANT, + VarInt(self.variant.load(Ordering::Relaxed) as i32), + )], + None, + ); }) } } diff --git a/pumpkin/src/entity/passive/sheep.rs b/pumpkin/src/entity/passive/sheep.rs index ef21d3a2a..1751a7af5 100644 --- a/pumpkin/src/entity/passive/sheep.rs +++ b/pumpkin/src/entity/passive/sheep.rs @@ -80,14 +80,14 @@ impl SheepEntity { fn set_packed_and_sync(&self, byte: u8) { self.color_and_sheared.store(byte, Ordering::Relaxed); - self.mob_entity - .living_entity - .entity - .send_meta_data(&[Metadata::new( + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( TrackedData::WOOL_ID, MetaDataType::BYTE, byte as i8, - )]); + )], + None, + ); } pub fn set_color(&self, color: u8) { diff --git a/pumpkin/src/entity/passive/villager/mod.rs b/pumpkin/src/entity/passive/villager/mod.rs index 9a07ec22a..d87a92b24 100644 --- a/pumpkin/src/entity/passive/villager/mod.rs +++ b/pumpkin/src/entity/passive/villager/mod.rs @@ -155,11 +155,14 @@ impl VillagerEntity { }; // Send initial metadata - mob_arc.get_entity().send_meta_data(&[Metadata::new( - TrackedData::VILLAGER_DATA, - MetaDataType::VILLAGER_DATA, - villager_data, - )]); + mob_arc.get_entity().send_meta_data( + &[Metadata::new( + TrackedData::VILLAGER_DATA, + MetaDataType::VILLAGER_DATA, + villager_data, + )], + None, + ); mob_arc } @@ -210,11 +213,14 @@ impl VillagerEntity { *villager_data = data; old_profession }; - self.get_entity().send_meta_data(&[Metadata::new( - TrackedData::VILLAGER_DATA, - MetaDataType::VILLAGER_DATA, - data, - )]); + self.get_entity().send_meta_data( + &[Metadata::new( + TrackedData::VILLAGER_DATA, + MetaDataType::VILLAGER_DATA, + data, + )], + None, + ); if old_profession != data.profession { self.generate_trades(data.profession_enum(), data.level.0) @@ -760,11 +766,14 @@ impl Mob for VillagerEntity { if is_sleeping { // Wake up if bed was broken self.get_entity().set_pose(EntityPose::Standing); - self.get_entity().send_meta_data(&[Metadata::new( - TrackedData::SLEEPING_POS_ID, - MetaDataType::OPTIONAL_BLOCK_POS, - None::, - )]); + self.get_entity().send_meta_data( + &[Metadata::new( + TrackedData::SLEEPING_POS_ID, + MetaDataType::OPTIONAL_BLOCK_POS, + None::, + )], + None, + ); } } } @@ -857,11 +866,14 @@ impl Mob for VillagerEntity { .await; self.get_entity().set_pose(EntityPose::Sleeping); - self.get_entity().send_meta_data(&[Metadata::new( - TrackedData::SLEEPING_POS_ID, - MetaDataType::OPTIONAL_BLOCK_POS, - Some(home_pos), - )]); + self.get_entity().send_meta_data( + &[Metadata::new( + TrackedData::SLEEPING_POS_ID, + MetaDataType::OPTIONAL_BLOCK_POS, + Some(home_pos), + )], + None, + ); } } } @@ -877,11 +889,14 @@ impl Mob for VillagerEntity { } self.get_entity().set_pose(EntityPose::Standing); - self.get_entity().send_meta_data(&[Metadata::new( - TrackedData::SLEEPING_POS_ID, - MetaDataType::OPTIONAL_BLOCK_POS, - None::, - )]); + self.get_entity().send_meta_data( + &[Metadata::new( + TrackedData::SLEEPING_POS_ID, + MetaDataType::OPTIONAL_BLOCK_POS, + None::, + )], + None, + ); } } diff --git a/pumpkin/src/entity/passive/wolf.rs b/pumpkin/src/entity/passive/wolf.rs index 2d1cee189..912ceb9ef 100644 --- a/pumpkin/src/entity/passive/wolf.rs +++ b/pumpkin/src/entity/passive/wolf.rs @@ -129,17 +129,23 @@ impl Mob for WolfEntity { let entity = self.get_entity(); let is_baby = entity.age.load(Ordering::Relaxed) < 0; if is_baby { - entity.send_meta_data(&[Metadata::new( - TrackedData::BABY_ID, - MetaDataType::BOOLEAN, - true, - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); } - entity.send_meta_data(&[Metadata::new( - TrackedData::VARIANT, - MetaDataType::WOLF_VARIANT, - VarInt(self.variant.load(Ordering::Relaxed) as i32), - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::VARIANT, + MetaDataType::WOLF_VARIANT, + VarInt(self.variant.load(Ordering::Relaxed) as i32), + )], + None, + ); }) } } diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index e2cc0779e..478624865 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -1592,11 +1592,14 @@ impl Player { self.living_entity .entity .set_pos(bed_head_pos.to_f64().add_raw(0.5, 0.6875, 0.5)); - self.get_entity().send_meta_data(&[Metadata::new( - TrackedData::SLEEPING_POS_ID, - MetaDataType::OPTIONAL_BLOCK_POS, - Some(bed_head_pos), - )]); + self.get_entity().send_meta_data( + &[Metadata::new( + TrackedData::SLEEPING_POS_ID, + MetaDataType::OPTIONAL_BLOCK_POS, + Some(bed_head_pos), + )], + None, + ); self.get_entity().set_velocity(Vector3::default()); self.sleeping_since.store(Some(0)); @@ -1710,11 +1713,14 @@ impl Player { self.living_entity.entity.set_pose(EntityPose::Standing); self.living_entity.entity.set_pos(self.position()); - self.living_entity.entity.send_meta_data(&[Metadata::new( - TrackedData::SLEEPING_POS_ID, - MetaDataType::OPTIONAL_BLOCK_POS, - None::, - )]); + self.living_entity.entity.send_meta_data( + &[Metadata::new( + TrackedData::SLEEPING_POS_ID, + MetaDataType::OPTIONAL_BLOCK_POS, + None::, + )], + None, + ); self.set_stat( statistics::StatisticCategory::Custom, @@ -2952,18 +2958,21 @@ impl Player { /// Send the player's skin layers and used hand to all players. pub fn send_client_information(&self) { let config = self.config.load(); - self.living_entity.entity.send_meta_data(&[ - Metadata::new( - TrackedData::PLAYER_MODE_CUSTOMISATION, - MetaDataType::BYTE, - config.skin_parts, - ), - // Metadata::new( - // TrackedData::DATA_MAIN_ARM_ID, - // MetaDataType::ARM, - // VarInt(config.main_hand as u8 as i32), - // ), - ]); + self.living_entity.entity.send_meta_data( + &[ + Metadata::new( + TrackedData::PLAYER_MODE_CUSTOMISATION, + MetaDataType::BYTE, + config.skin_parts, + ), + // Metadata::new( + // TrackedData::DATA_MAIN_ARM_ID, + // MetaDataType::ARM, + // VarInt(config.main_hand as u8 as i32), + // ), + ], + None, + ); } pub async fn can_harvest(&self, state: &BlockState, block: &'static Block) -> bool { diff --git a/pumpkin/src/entity/projectile/egg.rs b/pumpkin/src/entity/projectile/egg.rs index d144d745b..c5b44c727 100644 --- a/pumpkin/src/entity/projectile/egg.rs +++ b/pumpkin/src/entity/projectile/egg.rs @@ -73,11 +73,14 @@ impl EntityBase for EggEntity { let stack = self.item_stack.read().await; // Sync the item stack so the client renders the correct color/variant - entity.send_meta_data(&[Metadata::new( - TrackedData::ITEM_STACK, - MetaDataType::ITEM_STACK, - &ItemStackSerializer::from(stack.clone()), - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::ITEM_STACK, + MetaDataType::ITEM_STACK, + &ItemStackSerializer::from(stack.clone()), + )], + None, + ); }) } diff --git a/pumpkin/src/entity/projectile/eye_of_ender.rs b/pumpkin/src/entity/projectile/eye_of_ender.rs index d79cc7986..3a203e50a 100644 --- a/pumpkin/src/entity/projectile/eye_of_ender.rs +++ b/pumpkin/src/entity/projectile/eye_of_ender.rs @@ -172,11 +172,14 @@ impl EntityBase for EyeOfEnder { fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { Box::pin(async { - self.entity.send_meta_data(&[Metadata::new( - TrackedData::ITEM, - MetaDataType::ITEM_STACK, - &ItemStackSerializer::from(self.item_stack.lock().await.clone()), - )]); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::ITEM, + MetaDataType::ITEM_STACK, + &ItemStackSerializer::from(self.item_stack.lock().await.clone()), + )], + None, + ); }) } diff --git a/pumpkin/src/entity/projectile/firework_rocket.rs b/pumpkin/src/entity/projectile/firework_rocket.rs index 6477320b4..33e40aaf3 100644 --- a/pumpkin/src/entity/projectile/firework_rocket.rs +++ b/pumpkin/src/entity/projectile/firework_rocket.rs @@ -68,11 +68,14 @@ impl FireworkRocketEntity { }; // Set shooter metadata - rocket.entity.entity.send_meta_data(&[Metadata::new( - TrackedData::ATTACHED_TO_TARGET, - MetaDataType::OPTIONAL_INT, - OptionalInt(Some(shooter.entity_id)), - )]); + rocket.entity.entity.send_meta_data( + &[Metadata::new( + TrackedData::ATTACHED_TO_TARGET, + MetaDataType::OPTIONAL_INT, + OptionalInt(Some(shooter.entity_id)), + )], + None, + ); rocket } diff --git a/pumpkin/src/entity/projectile/fishing_bobber.rs b/pumpkin/src/entity/projectile/fishing_bobber.rs index 44666380b..dae55b7bc 100644 --- a/pumpkin/src/entity/projectile/fishing_bobber.rs +++ b/pumpkin/src/entity/projectile/fishing_bobber.rs @@ -207,11 +207,14 @@ impl FishingBobberEntity { if ebb.intersects(&search_box) { self.hooked_entity_id .store(cand.get_entity().entity_id, Ordering::Relaxed); - entity.send_meta_data(&[Metadata::new( - TrackedData::HOOKED_ENTITY, - MetaDataType::INT, - cand.get_entity().entity_id + 1, - )]); + entity.send_meta_data( + &[Metadata::new( + TrackedData::HOOKED_ENTITY, + MetaDataType::INT, + cand.get_entity().entity_id + 1, + )], + None, + ); return; } } diff --git a/pumpkin/src/entity/projectile/lingering_potion.rs b/pumpkin/src/entity/projectile/lingering_potion.rs index b97a222ce..d34d1e4a5 100644 --- a/pumpkin/src/entity/projectile/lingering_potion.rs +++ b/pumpkin/src/entity/projectile/lingering_potion.rs @@ -69,13 +69,16 @@ impl EntityBase for LingeringPotionEntity { let stack = self.item_stack.read().await; // Sync the item stack so the client renders the correct potion type - entity.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new( - pumpkin_data::tracked_data::TrackedData::ITEM_STACK, - pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK, - &pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from( - stack.clone(), - ), - )]); + entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( + pumpkin_data::tracked_data::TrackedData::ITEM_STACK, + pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK, + &pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from( + stack.clone(), + ), + )], + None, + ); }) } diff --git a/pumpkin/src/entity/projectile/splash_potion.rs b/pumpkin/src/entity/projectile/splash_potion.rs index 104d37cd9..7c797e327 100644 --- a/pumpkin/src/entity/projectile/splash_potion.rs +++ b/pumpkin/src/entity/projectile/splash_potion.rs @@ -107,13 +107,16 @@ impl EntityBase for SplashPotionEntity { let stack = self.item_stack.read().await; // Sync the item stack - entity.send_meta_data(&[pumpkin_protocol::java::client::play::Metadata::new( - pumpkin_data::tracked_data::TrackedData::ITEM_STACK, - pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK, - &pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from( - stack.clone(), - ), - )]); + entity.send_meta_data( + &[pumpkin_protocol::java::client::play::Metadata::new( + pumpkin_data::tracked_data::TrackedData::ITEM_STACK, + pumpkin_data::meta_data_type::MetaDataType::ITEM_STACK, + &pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer::from( + stack.clone(), + ), + )], + None, + ); }) } diff --git a/pumpkin/src/entity/tnt.rs b/pumpkin/src/entity/tnt.rs index e9a92baae..5c4c5f940 100644 --- a/pumpkin/src/entity/tnt.rs +++ b/pumpkin/src/entity/tnt.rs @@ -85,18 +85,21 @@ impl EntityBase for TNTEntity { self.entity .set_velocity(Vector3::new(-pos.sin() * 0.02, 0.2, -pos.cos() * 0.02)); - self.entity.send_meta_data(&[ - Metadata::new( - TrackedData::FUSE_ID, - MetaDataType::INTEGER, - VarInt(self.fuse.load(Relaxed) as i32), - ), - Metadata::new( - TrackedData::BLOCK_STATE_ID, - MetaDataType::BLOCK_STATE, - VarInt(i32::from(Block::TNT.default_state.id.as_u16())), - ), - ]); + self.entity.send_meta_data( + &[ + Metadata::new( + TrackedData::FUSE_ID, + MetaDataType::INTEGER, + VarInt(self.fuse.load(Relaxed) as i32), + ), + Metadata::new( + TrackedData::BLOCK_STATE_ID, + MetaDataType::BLOCK_STATE, + VarInt(i32::from(Block::TNT.default_state.id.as_u16())), + ), + ], + None, + ); }) } diff --git a/pumpkin/src/entity/vehicle/boat.rs b/pumpkin/src/entity/vehicle/boat.rs index c28e6dc40..387626a1a 100644 --- a/pumpkin/src/entity/vehicle/boat.rs +++ b/pumpkin/src/entity/vehicle/boat.rs @@ -39,10 +39,13 @@ impl BoatEntity { self.left_paddle_moving.store(left, Ordering::Relaxed); self.right_paddle_moving.store(right, Ordering::Relaxed); - self.vehicle.entity.send_meta_data(&[ - Metadata::new(TrackedData::ID_PADDLE_LEFT, MetaDataType::BOOLEAN, left), - Metadata::new(TrackedData::ID_PADDLE_RIGHT, MetaDataType::BOOLEAN, right), - ]); + self.vehicle.entity.send_meta_data( + &[ + Metadata::new(TrackedData::ID_PADDLE_LEFT, MetaDataType::BOOLEAN, left), + Metadata::new(TrackedData::ID_PADDLE_RIGHT, MetaDataType::BOOLEAN, right), + ], + None, + ); } fn send_wobble_metadata(&self) { diff --git a/pumpkin/src/entity/vehicle/vehicle.rs b/pumpkin/src/entity/vehicle/vehicle.rs index c26824a8a..142306587 100644 --- a/pumpkin/src/entity/vehicle/vehicle.rs +++ b/pumpkin/src/entity/vehicle/vehicle.rs @@ -64,23 +64,29 @@ impl VehicleEntity { } pub fn send_wobble_metadata(&self) { - self.entity.send_meta_data(&[ - Metadata::new( - TrackedData::ID_HURT, - MetaDataType::INTEGER, - self.get_hurt_time(), - ), - Metadata::new( - TrackedData::ID_HURTDIR, - MetaDataType::INTEGER, - self.get_hurt_dir(), - ), - ]); - self.entity.send_meta_data(&[Metadata::new( - TrackedData::ID_DAMAGE, - MetaDataType::FLOAT, - self.get_damage(), - )]); + self.entity.send_meta_data( + &[ + Metadata::new( + TrackedData::ID_HURT, + MetaDataType::INTEGER, + self.get_hurt_time(), + ), + Metadata::new( + TrackedData::ID_HURTDIR, + MetaDataType::INTEGER, + self.get_hurt_dir(), + ), + ], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::ID_DAMAGE, + MetaDataType::FLOAT, + self.get_damage(), + )], + None, + ); } pub async fn kill_and_drop_self(&self) { diff --git a/pumpkin/src/item/items/ender_eye.rs b/pumpkin/src/item/items/ender_eye.rs index 3806ad4c3..19184c7a4 100644 --- a/pumpkin/src/item/items/ender_eye.rs +++ b/pumpkin/src/item/items/ender_eye.rs @@ -141,11 +141,13 @@ fn find_stronghold(world: &Arc, origin: BlockPos) -> Option { let generator = &level.world_gen; let seed = level.seed.0; + let global_cache = generator.global_structure_cache()?; + find_nearest_structure( origin, &[&StructureSet::get("strongholds").unwrap().placement], 100, // max search radius in chunks, matches vanilla default seed as i64, - &generator.global_structure_cache, + global_cache, ) } diff --git a/pumpkin/src/lib.rs b/pumpkin/src/lib.rs index adca28979..60e795dab 100644 --- a/pumpkin/src/lib.rs +++ b/pumpkin/src/lib.rs @@ -231,8 +231,8 @@ impl PumpkinServer { }); } - let tcp_listener = if server.basic_config.java_edition { - let address = server.basic_config.java_edition_address; + let tcp_listener = if server.advanced_config.networking.java.enabled { + let address = server.advanced_config.networking.java.address; // Setup the TCP server socket. let listener = match TcpListener::bind(address).await { Ok(l) => l, @@ -275,7 +275,7 @@ impl PumpkinServer { let lan_broadcast = LANBroadcast::new( &server.advanced_config.networking.lan_broadcast, - &server.basic_config, + &server.advanced_config.networking.java.motd, ); server.spawn_task(lan_broadcast.start(addr)); } @@ -293,9 +293,9 @@ impl PumpkinServer { }); }; - let udp_socket = if server.basic_config.bedrock_edition { + let udp_socket = if server.advanced_config.networking.bedrock.enabled { Some(Arc::new( - UdpSocket::bind(server.basic_config.bedrock_edition_address) + UdpSocket::bind(server.advanced_config.networking.bedrock.address) .await .expect("Failed to bind UDP Socket"), )) diff --git a/pumpkin/src/main.rs b/pumpkin/src/main.rs index 10adc9c6e..313f69bb9 100644 --- a/pumpkin/src/main.rs +++ b/pumpkin/src/main.rs @@ -110,34 +110,34 @@ async fn main() { .color_named(NamedColor::Gold) .to_pretty_console() ); - let basic_config = &pumpkin_server.server.basic_config; + let advanced_config = &pumpkin_server.server.advanced_config; info!( "Server is now running. Connect using port: {}{}{}", - if basic_config.java_edition { + if advanced_config.networking.java.enabled { format!( "{} {}", TextComponent::text("Java Edition:") .color_named(NamedColor::Yellow) .to_pretty_console(), - TextComponent::text(format!("{}", basic_config.java_edition_address)) + TextComponent::text(format!("{}", advanced_config.networking.java.address)) .color_named(NamedColor::DarkBlue) .to_pretty_console() ) } else { TextComponent::text(String::new()).to_pretty_console() }, - if basic_config.java_edition && basic_config.bedrock_edition { + if advanced_config.networking.java.enabled && advanced_config.networking.bedrock.enabled { " | " // Separator if both are enabled } else { "" }, - if basic_config.bedrock_edition { + if advanced_config.networking.bedrock.enabled { format!( "{} {}", TextComponent::text("Bedrock Edition:") .color_named(NamedColor::Gold) .to_pretty_console(), - TextComponent::text(format!("{}", basic_config.bedrock_edition_address)) + TextComponent::text(format!("{}", advanced_config.networking.bedrock.address)) .color_named(NamedColor::DarkBlue) .to_pretty_console() ) diff --git a/pumpkin/src/net/bedrock/login.rs b/pumpkin/src/net/bedrock/login.rs index 500c6eb32..7b2ced3dd 100644 --- a/pumpkin/src/net/bedrock/login.rs +++ b/pumpkin/src/net/bedrock/login.rs @@ -107,7 +107,8 @@ impl BedrockClient { let compression = server .advanced_config .networking - .bedrock_compression + .bedrock + .compression .info .clone(); self.send_game_packet(&CNetworkSettings::new( @@ -136,13 +137,19 @@ impl BedrockClient { server: &Server, ) -> Result, LoginError> { let auth_payload: AuthPayload = serde_json::from_slice(&packet.jwt)?; - let player_data = if server.basic_config.online_mode { + let player_data = if server.advanced_config.networking.bedrock.online_mode { match auth_payload.authentication_type { AuthenticationType::Full => { verify_oidc_token_path(server, &auth_payload.token, false)? } AuthenticationType::SelfSigned => { - if server.advanced_config.networking.authentication.enabled { + if server + .advanced_config + .networking + .bedrock + .authentication + .enabled + { return Err(LoginError::SelfSignedNotAllowed); } @@ -181,7 +188,7 @@ impl BedrockClient { profile_actions: None, }; - if server.basic_config.bedrock_encryption { + if server.advanced_config.networking.bedrock.encryption { let client_public_key = pumpkin_util::jwt::extract_cpk_from_token(&auth_payload.token) .map_err(LoginError::ChainValidationFailed)?; diff --git a/pumpkin/src/net/bedrock/mod.rs b/pumpkin/src/net/bedrock/mod.rs index 034a1b3f9..7a2fc6df8 100644 --- a/pumpkin/src/net/bedrock/mod.rs +++ b/pumpkin/src/net/bedrock/mod.rs @@ -1290,7 +1290,7 @@ impl BedrockClient { .as_ref() .map_or(0, |p| p.online) as u32 }; - if player_count >= server.basic_config.max_players { + if player_count >= server.advanced_config.networking.bedrock.max_players { Self::send_offline_packet( &CNoFreeIncomingConnections::new(server.server_guid), addr, diff --git a/pumpkin/src/net/bedrock/play.rs b/pumpkin/src/net/bedrock/play.rs index 9a5924923..435038844 100644 --- a/pumpkin/src/net/bedrock/play.rs +++ b/pumpkin/src/net/bedrock/play.rs @@ -3,7 +3,12 @@ use std::{ sync::{Arc, atomic::Ordering}, }; -use pumpkin_data::{data_component_impl::EquipmentSlot, item_stack::ItemStack}; +use pumpkin_data::{ + data_component_impl::{ + BlocksAttacksImpl, ConsumableImpl, EquipmentSlot, EquippableImpl, FoodImpl, + }, + item_stack::ItemStack, +}; use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler}; use pumpkin_macros::send_cancellable; use pumpkin_protocol::bedrock::{ @@ -37,7 +42,7 @@ use pumpkin_protocol::{ codec::{var_int::VarInt, var_long::VarLong, var_uint::VarUInt, var_ulong::VarULong}, java::client::play::{Animation, CEntityAnimation, CSetSelectedSlot, CSystemChatMessage}, }; -use pumpkin_util::{GameMode, math::position::BlockPos, text::TextComponent}; +use pumpkin_util::{GameMode, Hand, math::position::BlockPos, text::TextComponent}; use pumpkin_world::inventory::Inventory; use pumpkin_world::world::BlockFlags; @@ -47,8 +52,10 @@ use crate::{ entity::{EntityBase, player::Player}, net::{DisconnectReason, bedrock::BedrockClient}, plugin::player::{ - item_held::PlayerItemHeldEvent, player_chat::PlayerChatEvent, + item_held::PlayerItemHeldEvent, + player_chat::PlayerChatEvent, player_command_send::PlayerCommandSendEvent, + player_interact_event::{InteractAction, PlayerInteractEvent}, player_toggle_flight_event::PlayerToggleFlightEvent, }, server::{Server, seasonal_events}, @@ -152,8 +159,10 @@ impl BedrockClient { } let server = player.world().server.upgrade().unwrap(); - let view_distance = - chunk_radius.clamp(2, NonZeroI32::from(server.basic_config.view_distance).get()); + let view_distance = chunk_radius.clamp( + 2, + NonZeroI32::from(server.advanced_config.networking.bedrock.view_distance).get(), + ); self.enqueue_packet(&CChunkRadiusUpdate { chunk_radius: VarInt(view_distance), @@ -733,6 +742,105 @@ impl BedrockClient { } } } + } else if data.action_type.0 == 1 { + // Click air / Use item + let is_creative = player.gamemode.load() == GameMode::Creative; + let client_stack = descriptor_to_stack(&data.item_in_hand, is_creative); + + let held_item = player.inventory.held_item(); + if !client_stack.is_empty() { + let mut server_stack = held_item.lock().await; + if server_stack.is_empty() || server_stack.item.id != client_stack.item.id { + *server_stack = client_stack.clone(); + } + } + + let event = PlayerInteractEvent::new( + player, + InteractAction::RightClickAir, + &pumpkin_data::Block::AIR, + None, + ); + + let stack_for_use = held_item.lock().await.clone(); + + { + let mut held = held_item.lock().await; + let mut cooldown_active = false; + if let Some(cooldown) = held.get_use_cooldown() { + let group = cooldown + .cooldown_group + .clone() + .unwrap_or_else(|| held.item.registry_key.to_string()); + if player.is_on_cooldown(&group).await { + cooldown_active = true; + } + } + + if !cooldown_active { + if held.get_data_component::().is_some() + || held.get_data_component::().is_some() + { + if let Some(food) = held.get_data_component::() { + 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::() { + 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) => { diff --git a/pumpkin/src/net/bedrock/unconnected.rs b/pumpkin/src/net/bedrock/unconnected.rs index 9128eb538..33d8bf5d1 100644 --- a/pumpkin/src/net/bedrock/unconnected.rs +++ b/pumpkin/src/net/bedrock/unconnected.rs @@ -30,12 +30,12 @@ impl BedrockClient { let motd_string = ServerInfo { edition: "MCPE", // TODO The default motd is to long to be displayed completely - motd_line_1: "Pumpkin Server", + motd_line_1: server.advanced_config.networking.bedrock.motd.clone(), protocol_version: CURRENT_BEDROCK_MC_PROTOCOL, version_name: CURRENT_BEDROCK_MC_VERSION, player_count, // A large number looks wreird on the client worlds window - max_player_count: server.basic_config.max_players, + max_player_count: server.advanced_config.networking.bedrock.max_players, server_unique_id: server.server_guid, motd_line_2: server.basic_config.default_level_name.clone(), game_mode: server.defaultgamemode.lock().await.gamemode.to_str(), diff --git a/pumpkin/src/net/java/login.rs b/pumpkin/src/net/java/login.rs index 3546105cb..acf8c2aaf 100644 --- a/pumpkin/src/net/java/login.rs +++ b/pumpkin/src/net/java/login.rs @@ -34,7 +34,7 @@ impl JavaClient { // Don't allow new logons when the server is full. // If `max_players` is set to zero, then there is no max player count enforced. // TODO: If client is an operator or has otherwise suitable elevated permissions, allow the client to bypass this requirement. - let max_players = server.basic_config.max_players; + let max_players = server.advanced_config.networking.java.max_players; if max_players > 0 && server.get_player_count() >= max_players as usize { self.kick(TextComponent::translate_cross( translation::java::MULTIPLAYER_DISCONNECT_SERVER_FULL, @@ -74,7 +74,7 @@ impl JavaClient { } } } else { - let id = if server.basic_config.online_mode { + let id = if server.advanced_config.networking.java.online_mode { login_start.uuid } else { offline_uuid(&login_start.name).expect("This is very not safe and bad") @@ -87,16 +87,19 @@ impl JavaClient { profile_actions: None, }; - if server.advanced_config.networking.java_compression.enabled { + if server.advanced_config.networking.java.compression.enabled { self.enable_compression(server).await; } - if server.basic_config.encryption { + if server.advanced_config.networking.java.encryption { let verify_token: [u8; 4] = rand::random(); // Wait until we have sent the encryption packet to the client self.send_packet_now( &server - .encryption_request(&verify_token, server.basic_config.online_mode) + .encryption_request( + &verify_token, + server.advanced_config.networking.java.online_mode, + ) .await, ) .await; @@ -132,7 +135,7 @@ impl JavaClient { return; }; - if server.basic_config.online_mode { + if server.advanced_config.networking.java.online_mode { // Online mode auth match self .authenticate(server, &shared_secret, &profile.name) @@ -201,7 +204,8 @@ impl JavaClient { let compression = server .advanced_config .networking - .java_compression + .java + .compression .info .clone(); // We want to wait until we have sent the compression packet to the client @@ -236,7 +240,7 @@ impl JavaClient { username, &hash, &ip, - &server.advanced_config.networking.authentication, + &server.advanced_config.networking.java.authentication, )?; // Check if the player should join @@ -244,6 +248,7 @@ impl JavaClient { if server .advanced_config .networking + .java .authentication .player_profile .allow_banned_players @@ -251,6 +256,7 @@ impl JavaClient { for allowed in &server .advanced_config .networking + .java .authentication .player_profile .allowed_actions @@ -270,7 +276,12 @@ impl JavaClient { for property in profile.properties.load().iter() { authentication::validate_textures( property, - &server.advanced_config.networking.authentication.textures, + &server + .advanced_config + .networking + .java + .authentication + .textures, ) .map_err(AuthError::TextureError)?; } diff --git a/pumpkin/src/net/lan_broadcast.rs b/pumpkin/src/net/lan_broadcast.rs index d7b9fc590..d7111a668 100644 --- a/pumpkin/src/net/lan_broadcast.rs +++ b/pumpkin/src/net/lan_broadcast.rs @@ -1,4 +1,4 @@ -use pumpkin_config::{BasicConfiguration, LANBroadcastConfig}; +use pumpkin_config::LANBroadcastConfig; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::sync::atomic::Ordering; use std::time::Duration; @@ -23,7 +23,7 @@ pub struct LANBroadcast { impl LANBroadcast { /// Creates a new LAN broadcast instance from the provided configuration #[must_use] - pub fn new(config: &LANBroadcastConfig, basic_config: &BasicConfiguration) -> Self { + pub fn new(config: &LANBroadcastConfig, server_motd: &str) -> Self { let port = config.port.unwrap_or(0); let advanced_motd = config.motd.clone().unwrap_or_default(); @@ -32,7 +32,7 @@ impl LANBroadcast { warn!( "Using the server MOTD as the LAN broadcast MOTD. Note that the LAN broadcast MOTD does not support multiple lines, RGB colors, or gradients so consider defining it accordingly." ); - basic_config.motd.replace('\n', " ") + server_motd.replace('\n', " ") } else { advanced_motd }; diff --git a/pumpkin/src/net/query.rs b/pumpkin/src/net/query.rs index b0dbfb488..d8a9b5831 100644 --- a/pumpkin/src/net/query.rs +++ b/pumpkin/src/net/query.rs @@ -87,6 +87,7 @@ pub async fn start_query_handler(server: Arc, query_addr: SocketAddr) { // Errors of packets that don't meet the format aren't returned since we won't handle them anyway // The only errors that are thrown are because of a null terminator in a CString // since those errors need to be corrected by server owner +#[expect(clippy::too_many_lines)] #[inline] async fn handle_packet( buf: Vec, @@ -156,7 +157,9 @@ async fn handle_packet( let response = CFullStatus { session_id: packet.session_id, - hostname: CString::new(server.basic_config.motd.as_str())?, + hostname: CString::new( + server.advanced_config.networking.java.motd.as_str(), + )?, version: CString::new(CURRENT_MC_VERSION)?, plugins: CString::new(plugins)?, map: CString::new( @@ -167,7 +170,8 @@ async fn handle_packet( .map_or("world", |w| w.get_world_name()), )?, num_players: server.get_player_count(), - max_players: server.basic_config.max_players as usize, + max_players: server.advanced_config.networking.java.max_players + as usize, host_port: bound_addr.port(), host_ip: CString::new(bound_addr.ip().to_string())?, players, @@ -179,7 +183,9 @@ async fn handle_packet( } else { let response = CBasicStatus { session_id: packet.session_id, - motd: CString::new(server.basic_config.motd.as_str())?, + motd: CString::new( + server.advanced_config.networking.java.motd.as_str(), + )?, map: CString::new( server .worlds @@ -188,7 +194,8 @@ async fn handle_packet( .map_or("world", |w| w.get_world_name()), )?, num_players: server.get_player_count(), - max_players: server.basic_config.max_players as usize, + max_players: server.advanced_config.networking.java.max_players + as usize, host_port: bound_addr.port(), host_ip: CString::new(bound_addr.ip().to_string())?, }; diff --git a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs index 76379b7bf..0935f8239 100644 --- a/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs +++ b/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/server.rs @@ -292,7 +292,7 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { .as_ref() .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; - Ok(server.basic_config.max_players) + Ok(server.advanced_config.networking.java.max_players) } async fn is_hardcore(&mut self, _rep: Resource) -> wasmtime::Result { @@ -310,7 +310,12 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { .as_ref() .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; - Ok(server.advanced_config.networking.authentication.enabled) + Ok(server + .advanced_config + .networking + .java + .authentication + .enabled) } async fn get_motd(&mut self, _rep: Resource) -> wasmtime::Result { @@ -319,7 +324,7 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { .as_ref() .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; - Ok(server.basic_config.motd.clone()) + Ok(server.advanced_config.networking.java.motd.clone()) } async fn has_whitelist(&mut self, _rep: Resource) -> wasmtime::Result { @@ -355,7 +360,7 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { .as_ref() .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; - Ok(server.basic_config.view_distance.get()) + Ok(server.advanced_config.networking.java.view_distance.get()) } async fn get_simulation_distance(&mut self, _rep: Resource) -> wasmtime::Result { @@ -364,7 +369,12 @@ impl pumpkin::plugin::server::HostServer for PluginHostState { .as_ref() .ok_or_else(|| wasmtime::Error::msg("Server not available"))?; - Ok(server.basic_config.simulation_distance.get()) + Ok(server + .advanced_config + .networking + .java + .simulation_distance + .get()) } async fn get_default_gamemode( diff --git a/pumpkin/src/server/connection_cache.rs b/pumpkin/src/server/connection_cache.rs index 46a60e38c..25fc7b89e 100644 --- a/pumpkin/src/server/connection_cache.rs +++ b/pumpkin/src/server/connection_cache.rs @@ -68,8 +68,8 @@ impl<'a> CachedBranding { impl CachedStatus { #[must_use] - pub fn new(config: &BasicConfiguration) -> Self { - let status_response = Self::build_response(config); + pub fn new(config: &BasicConfiguration, motd: &str, max_players: u32) -> Self { + let status_response = Self::build_response(config, motd, max_players); Self { status_response, @@ -135,7 +135,11 @@ impl CachedStatus { } } - pub fn build_response(config: &BasicConfiguration) -> StatusResponse { + pub fn build_response( + config: &BasicConfiguration, + motd: &str, + max_players: u32, + ) -> StatusResponse { let favicon = if config.use_favicon { config.favicon_path.as_ref().map_or_else( || { @@ -185,11 +189,11 @@ impl CachedStatus { protocol: LOWEST_SUPPORTED_MC_VERSION.protocol_version() as u32, }), players: Some(Players { - max: config.max_players, + max: max_players, online: 0, sample: vec![], }), - description: config.motd.clone(), + description: motd.to_string(), favicon, // This should stay true even when reports are disabled. // It prevents the annoying popup when joining the server. @@ -200,6 +204,10 @@ impl CachedStatus { impl Default for CachedStatus { fn default() -> Self { - Self::new(&BasicConfiguration::default()) + Self::new( + &BasicConfiguration::default(), + "A blazingly fast Pumpkin server!", + 1000, + ) } } diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 40c86be17..e2cab8778 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -199,7 +199,11 @@ impl Server { let seed = level_info.world_gen_settings.seed; let level_info = Arc::new(ArcSwap::new(Arc::new(level_info))); - let listing = Mutex::new(CachedStatus::new(&basic_config)); + let listing = Mutex::new(CachedStatus::new( + &basic_config, + &advanced_config.networking.java.motd, + advanced_config.networking.java.max_players, + )); let defaultgamemode = Mutex::new(DefaultGamemode { gamemode: basic_config.default_gamemode, }); @@ -218,7 +222,7 @@ impl Server { let tick_rate_manager = Arc::new(ServerTickRateManager::new(basic_config.tps)); let mojang_keys_task = tokio::spawn({ - let auth_config = advanced_config.networking.authentication.clone(); + let auth_config = advanced_config.networking.java.authentication.clone(); let allow_chat = basic_config.allow_chat_reports; async move { if allow_chat { @@ -351,14 +355,24 @@ impl Server { info!("All worlds loaded successfully."); - if server.basic_config.online_mode { + if server.advanced_config.networking.bedrock.online_mode { let server_clone = server.clone(); tokio::spawn(async move { server_clone .bedrock_oidc_keys .get_or_init(|| async { tokio::task::block_in_place(|| { - pumpkin_util::jwt::fetch_oidc_jwks().unwrap_or_else(|e| { + let auth = &server_clone + .advanced_config + .networking + .bedrock + .authentication; + pumpkin_util::jwt::fetch_oidc_jwks( + auth.url.as_deref(), + auth.connect_timeout, + auth.read_timeout, + ) + .unwrap_or_else(|e| { error!("Failed to fetch Bedrock OIDC keys: {e}"); (String::new(), pumpkin_util::jwt::Jwks { keys: Vec::new() }) }) diff --git a/pumpkin/src/world/chunker.rs b/pumpkin/src/world/chunker.rs index 202c8562c..6c0ef9e42 100644 --- a/pumpkin/src/world/chunker.rs +++ b/pumpkin/src/world/chunker.rs @@ -14,10 +14,15 @@ use crate::{ pub fn get_view_distance(player: &Player) -> NonZeroU8 { let server = player.world().server.upgrade().unwrap(); - player.config.load().view_distance.clamp( - NonZeroU8::new(2).unwrap(), - server.basic_config.view_distance, - ) + let max_view_distance = match player.client.as_ref() { + ClientPlatform::Java(_) => server.advanced_config.networking.java.view_distance, + ClientPlatform::Bedrock(_) => server.advanced_config.networking.bedrock.view_distance, + }; + player + .config + .load() + .view_distance + .clamp(NonZeroU8::new(2).unwrap(), max_view_distance) } // Checks if the target chunk is within the view distance diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index d4b4fc7bb..859c5a950 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -1849,7 +1849,13 @@ impl World { has_start_with_map_enabled: false, // TODO Bedrock permission level are different permission_level: VarInt(2), - server_simulation_distance: base_config.simulation_distance.get().into(), + server_simulation_distance: server + .advanced_config + .networking + .bedrock + .simulation_distance + .get() + .into(), has_locked_behavior_pack: false, has_locked_resource_pack: false, is_from_locked_world_template: false, @@ -2535,9 +2541,27 @@ impl World { entity_id, base_config.hardcore, dimensions, - base_config.max_players.try_into().unwrap(), - base_config.view_distance.get().into(), // TODO: view distance - base_config.simulation_distance.get().into(), // TODO: sim view dinstance + server + .advanced_config + .networking + .java + .max_players + .try_into() + .unwrap(), + server + .advanced_config + .networking + .java + .view_distance + .get() + .into(), // TODO: view distance + server + .advanced_config + .networking + .java + .simulation_distance + .get() + .into(), // TODO: sim view dinstance false, true, false, @@ -2555,7 +2579,7 @@ impl World { VarInt(player.get_entity().portal_cooldown.load(Ordering::Relaxed) as i32), self.sea_level.into(), ), - base_config.online_mode, + server.advanced_config.networking.java.online_mode, // This should stay true even when reports are disabled. // It prevents the annoying popup when joining the server. true,