From 44c162df6de9401e5d6a3eb8f773c13cf03b76de Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Wed, 6 May 2026 15:34:11 +0200 Subject: [PATCH] fix: broadcast correct protocol version in status response --- pumpkin-protocol/src/lib.rs | 8 +- pumpkin-util/src/random/mod.rs | 4 + pumpkin-world/src/generation/carver/canyon.rs | 2 +- pumpkin-world/src/generation/carver/cave.rs | 32 ++++++- pumpkin/src/entity/mob/ghast.rs | 87 ++++++++++++++++--- pumpkin/src/net/java/status.rs | 9 +- pumpkin/src/server/connection_cache.rs | 44 +++++----- 7 files changed, 142 insertions(+), 44 deletions(-) diff --git a/pumpkin-protocol/src/lib.rs b/pumpkin-protocol/src/lib.rs index 6197b80f9..b94130a65 100644 --- a/pumpkin-protocol/src/lib.rs +++ b/pumpkin-protocol/src/lib.rs @@ -373,7 +373,7 @@ impl From for PacketDecodeError { } } -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct StatusResponse { /// The version on which the server is running. (Optional) pub version: Option, @@ -386,7 +386,7 @@ pub struct StatusResponse { /// Whether players are forced to use secure chat. pub enforce_secure_chat: bool, } -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct Version { /// The name of the version (e.g. 1.21.4) pub name: String, @@ -394,7 +394,7 @@ pub struct Version { pub protocol: u32, } -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct Players { /// The maximum player count that the server allows. pub max: u32, @@ -405,7 +405,7 @@ pub struct Players { pub sample: Vec, } -#[derive(Serialize)] +#[derive(Serialize, Clone)] pub struct Sample { /// The player's name. pub name: String, diff --git a/pumpkin-util/src/random/mod.rs b/pumpkin-util/src/random/mod.rs index c7ba7fbe1..122e324bb 100644 --- a/pumpkin-util/src/random/mod.rs +++ b/pumpkin-util/src/random/mod.rs @@ -182,6 +182,10 @@ pub trait RandomImpl { self.next_bounded_i32(max - min + 1) + min } + fn next_inbetween_f32(&mut self, min: f32, max: f32) -> f32 { + self.next_f32() * (max - min) + min + } + fn next_i64(&mut self) -> i64; fn next_bool(&mut self) -> bool; diff --git a/pumpkin-world/src/generation/carver/canyon.rs b/pumpkin-world/src/generation/carver/canyon.rs index 86272e8b9..6c378eb25 100644 --- a/pumpkin-world/src/generation/carver/canyon.rs +++ b/pumpkin-world/src/generation/carver/canyon.rs @@ -177,7 +177,7 @@ impl CanyonCarver { let vertical_multiplier = 1.0 - (0.5 - current_step / distance).abs() * 2.0; let factor = canyon_config.shape.vertical_radius_default_factor + canyon_config.shape.vertical_radius_center_factor * vertical_multiplier; - factor as f64 * vertical_radius * (random.next_inbetween_i32(75, 100) as f64 / 100.0) + factor as f64 * vertical_radius * random.next_inbetween_f32(0.75, 1.0) as f64 } #[allow(clippy::too_many_arguments)] diff --git a/pumpkin-world/src/generation/carver/cave.rs b/pumpkin-world/src/generation/carver/cave.rs index aaddba41a..aec0301f7 100644 --- a/pumpkin-world/src/generation/carver/cave.rs +++ b/pumpkin-world/src/generation/carver/cave.rs @@ -312,6 +312,8 @@ impl CaveCarver { let zd = (world_z as f64 + 0.5 - z) / horizontal_radius; if xd * xd + zd * zd < 1.0 { + let mut has_grass = false; + for world_y in (min_y + 1..=max_y).rev() { let yd = (world_y as f64 - 0.5 - y) / vertical_radius; @@ -319,7 +321,15 @@ impl CaveCarver { && !chunk.carving_mask.get(world_x, world_y, world_z) { chunk.carving_mask.set(world_x, world_y, world_z); - self.carve_block(chunk, config, world_x, world_y, world_z, is_nether); + self.carve_block( + chunk, + config, + world_x, + world_y, + world_z, + is_nether, + &mut has_grass, + ); } } } @@ -344,13 +354,16 @@ impl CaveCarver { y: i32, z: i32, is_nether: bool, + has_grass: &mut bool, ) -> bool { let local_y = y - chunk.bottom_y() as i32; let state_id = chunk.get_block_state_raw(x & 15, local_y, z & 15); let block = pumpkin_data::Block::from_state_id(state_id); - if block.id == pumpkin_data::Block::WATER.id || block.id == pumpkin_data::Block::LAVA.id { - return false; + if block.id == pumpkin_data::Block::GRASS_BLOCK.id + || block.id == pumpkin_data::Block::MYCELIUM.id + { + *has_grass = true; } // Only carve if it's replaceable @@ -373,6 +386,19 @@ impl CaveCarver { chunk.set_block_state(x & 15, local_y, z & 15, air); } + // TODO: fix this + // if *has_grass { + // let below_state_id = chunk.get_block_state_raw(x & 15, local_y - 1, z & 15); + // let below_block = pumpkin_data::Block::from_state_id(below_state_id); + + // if below_block.id == pumpkin_data::Block::DIRT.id { + // // TODO: Java uses Biome top material here, defaulting to Grass for now + // let top_material = + // BlockState::from_id(pumpkin_data::Block::GRASS_BLOCK.default_state.id); + // chunk.set_block_state(x & 15, local_y - 1, z & 15, top_material); + // } + // } + return true; } false diff --git a/pumpkin/src/entity/mob/ghast.rs b/pumpkin/src/entity/mob/ghast.rs index 01e6f1492..1462e58be 100644 --- a/pumpkin/src/entity/mob/ghast.rs +++ b/pumpkin/src/entity/mob/ghast.rs @@ -1,21 +1,27 @@ +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; -use pumpkin_data::entity::EntityType; - use crate::entity::{ Entity, NBTStorage, - ai::goal::{look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal}, + ai::goal::{Controls, Goal, GoalFuture}, mob::{Mob, MobEntity}, }; pub struct GhastEntity { pub mob_entity: MobEntity, + pub is_charging: AtomicBool, + pub explosion_power: AtomicU8, } impl GhastEntity { pub async fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let ghast = Self { mob_entity }; + let ghast = Self { + mob_entity, + is_charging: AtomicBool::new(false), + explosion_power: AtomicU8::new(1), + }; + let mob_arc = Arc::new(ghast); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -25,16 +31,20 @@ impl GhastEntity { { let mut goal_selector = mob_arc.mob_entity.goals_selector.lock().await; - // TODO: GhastFlyGoal, GhastLookGoal, GhastAttackGoal - goal_selector.add_goal( - 6, - LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0), - ); - goal_selector.add_goal(6, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(7, Box::new(GhastLookGoal::new(mob_weak.clone()))); }; mob_arc } + + pub fn set_charging(&self, charging: bool) { + // You would also sync this to the client via EntityMetadata here + self.is_charging.store(charging, Ordering::Relaxed); + } + + pub fn is_charging(&self) -> bool { + self.is_charging.load(Ordering::Relaxed) + } } impl NBTStorage for GhastEntity {} @@ -45,6 +55,61 @@ impl Mob for GhastEntity { } fn get_mob_gravity(&self) -> f64 { - 0.0 + 0.0 // Ghasts fly, no gravity applied in standard travel + } +} + +#[expect(dead_code)] +pub struct GhastLookGoal { + goal_control: Controls, + mob_weak: Weak, +} + +impl GhastLookGoal { + #[must_use] + pub fn new(mob_weak: Weak) -> Self { + Self { + goal_control: Controls::LOOK, + mob_weak, + } + } +} + +impl Goal for GhastLookGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async { true }) + } + + fn should_run_every_tick(&self) -> bool { + true + } + + fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async { + let mob_entity = mob.get_mob_entity(); + let target_opt = mob_entity.target.lock().await.clone(); + + if let Some(target) = target_opt { + let mob_pos = mob_entity.living_entity.entity.pos.load(); + let target_pos = target.get_entity().pos.load(); + + if mob_pos.squared_distance_to_vec(&target_pos) < 4096.0 { + let mut look_control = mob_entity.look_control.lock().await; + look_control.look_at(mob, target_pos.x, target_pos.y, target_pos.z); + } + } else { + // If no target, face the movement direction + let velocity = mob_entity.living_entity.entity.velocity.load(); + if velocity.x != 0.0 || velocity.z != 0.0 { + let yaw = (-f64::atan2(velocity.x, velocity.z) * (180.0 / std::f64::consts::PI)) + as f32; + mob_entity.living_entity.entity.yaw.store(yaw); + } + } + }) + } + + fn controls(&self) -> Controls { + self.goal_control } } diff --git a/pumpkin/src/net/java/status.rs b/pumpkin/src/net/java/status.rs index 329bad926..91c995e3a 100644 --- a/pumpkin/src/net/java/status.rs +++ b/pumpkin/src/net/java/status.rs @@ -9,8 +9,13 @@ impl JavaClient { pub async fn handle_status_request(&self, server: &Server) { debug!("Handling status request"); let status = server.get_status(); - self.send_packet_now(&status.lock().await.get_status()) - .await; + self.send_packet_now( + &status + .lock() + .await + .get_status_packet(self.version.load().protocol_version()), + ) + .await; } pub async fn handle_ping_request(&self, ping_request: SStatusPingRequest) { diff --git a/pumpkin/src/server/connection_cache.rs b/pumpkin/src/server/connection_cache.rs index 7f4a509c3..46a60e38c 100644 --- a/pumpkin/src/server/connection_cache.rs +++ b/pumpkin/src/server/connection_cache.rs @@ -2,7 +2,7 @@ use crate::entity::player::Player; use base64::{Engine as _, engine::general_purpose}; use core::error; use pumpkin_config::BasicConfiguration; -use pumpkin_data::packet::CURRENT_MC_VERSION; +use pumpkin_data::packet::{CURRENT_MC_VERSION, LOWEST_SUPPORTED_MC_VERSION}; use pumpkin_protocol::{ Players, Sample, StatusResponse, Version, codec::var_int::VarInt, @@ -37,9 +37,6 @@ fn load_icon_from_bytes(png_data: &[u8]) -> String { pub struct CachedStatus { pub status_response: StatusResponse, - // We cache the json response here so we don't parse it every time someone makes a status request. - // Keep in mind that we must parse this again when the StatusResponse changes, which usually happen when a player joins or leaves. - status_response_json: String, player_samples: Vec<(Uuid, String)>, } @@ -73,18 +70,29 @@ impl CachedStatus { #[must_use] pub fn new(config: &BasicConfiguration) -> Self { let status_response = Self::build_response(config); - let status_response_json = serde_json::to_string(&status_response) - .expect("Failed to parse status response into JSON"); Self { status_response, - status_response_json, player_samples: Vec::new(), } } - pub fn get_status(&self) -> CStatusResponse { - CStatusResponse::new(self.status_response_json.clone()) + pub fn get_status_packet(&self, client_protocol: i32) -> CStatusResponse { + let mut response = self.status_response.clone(); + + let supported_min = LOWEST_SUPPORTED_MC_VERSION.protocol_version(); + let supported_max = CURRENT_MC_VERSION.protocol_version(); + + if client_protocol >= supported_min + && client_protocol <= supported_max + && let Some(version) = &mut response.version + { + version.protocol = client_protocol as u32; + } + + let json = serde_json::to_string(&response).expect("Failed to serialize status response"); + + CStatusResponse::new(json) } fn build_sample_list(&self) -> Vec { @@ -102,38 +110,28 @@ impl CachedStatus { let player_id = player.gameprofile.id; let player_name = player.gameprofile.name.clone(); - // Only add if player is not already in the list if !self.player_samples.iter().any(|(id, _)| *id == player_id) { self.player_samples.push((player_id, player_name)); let sample = self.build_sample_list(); - let status_response = &mut self.status_response; - if let Some(players) = &mut status_response.players { + if let Some(players) = &mut self.status_response.players { players.online = players.online.saturating_add(1); players.sample = sample; } - - self.status_response_json = serde_json::to_string(&status_response) - .expect("Failed to parse status response into JSON"); } } pub fn remove_player(&mut self, player: &Player) { let player_id = player.gameprofile.id; - // Only decrement if player was actually in the list if self.player_samples.iter().any(|(id, _)| *id == player_id) { self.player_samples.retain(|(id, _)| *id != player_id); let sample = self.build_sample_list(); - let status_response = &mut self.status_response; - if let Some(players) = &mut status_response.players { + if let Some(players) = &mut self.status_response.players { players.online = players.online.saturating_sub(1); players.sample = sample; } - - self.status_response_json = serde_json::to_string(&status_response) - .expect("Failed to parse status response into JSON"); } } @@ -183,8 +181,8 @@ impl CachedStatus { StatusResponse { version: Some(Version { - name: CURRENT_MC_VERSION.to_string(), - protocol: CURRENT_MC_VERSION.protocol_version() as u32, + name: format!("{LOWEST_SUPPORTED_MC_VERSION}-{CURRENT_MC_VERSION}"), + protocol: LOWEST_SUPPORTED_MC_VERSION.protocol_version() as u32, }), players: Some(Players { max: config.max_players,