Add Hunger

This commit is contained in:
Alexander Medvedev 2025-02-07 00:22:52 +01:00
parent 5c31e1ff62
commit 2e2fe83720
35 changed files with 212 additions and 103 deletions

View File

@ -14,7 +14,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi
![image](https://github.com/user-attachments/assets/7e2e865e-b150-4675-a2d5-b52f9900378e)
## What Pumpkin wants to achieve
## Goals
- **Performance**: Leveraging multi-threading for maximum speed and efficiency.
- **Compatibility**: Supports the latest Minecraft server version and adheres to vanilla game mechanics.
@ -22,10 +22,6 @@ and customizable experience. It prioritizes performance and player enjoyment whi
- **Flexibility**: Highly configurable, with the ability to disable unnecessary features.
- **Extensibility**: Provides a foundation for plugin development.
## What Pumpkin will not
- Be compatible with plugins or mods for other servers
> [!IMPORTANT]
> Pumpkin is currently under heavy development.
@ -48,6 +44,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi
- [x] World Loading
- [x] Lighting
- [x] Entity Spawning
- [x] Item drops
- [x] Bossbar
- [x] Chunk Loading (Vanilla, Linear)
- [x] Chunk Generation
@ -59,13 +56,15 @@ and customizable experience. It prioritizes performance and player enjoyment whi
- [ ] Redstone
- [ ] Liquid Physics
- Player
- [x] Player Skins
- [x] Player Client brand
- [x] Player Teleport
- [x] Player Movement
- [x] Player Animation
- [x] Player Inventory
- [x] Player Combat
- [x] Skins
- [x] Client brand
- [x] Teleport
- [x] Movement
- [x] Animation
- [x] Inventory
- [x] Combat
- [x] Experience
- [x] Hunger
- Entities
- [x] Non-Living (Minecart, Eggs...)
- [x] Players

View File

@ -25,7 +25,6 @@ impl Default for ChunkCompression {
}
#[derive(Deserialize, Serialize, Clone)]
#[repr(u8)]
pub enum Compression {
/// GZip Compression
GZip,
@ -38,7 +37,6 @@ pub enum Compression {
}
#[derive(Deserialize, Serialize, Clone, Default)]
#[repr(u8)]
pub enum ChunkFormat {
#[default]
Anvil,

View File

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
pub struct AuthenticationConfig {
/// Whether to use Mojang authentication.
pub enabled: bool,
pub auth_url: Option<String>,
pub url: Option<String>,
pub connect_timeout: u32,
pub read_timeout: u32,
pub prevent_proxy_connections: bool,
@ -24,7 +24,7 @@ impl Default for AuthenticationConfig {
prevent_proxy_connections: false,
player_profile: Default::default(),
textures: Default::default(),
auth_url: None,
url: None,
prevent_proxy_connection_auth_url: None,
connect_timeout: 5000,
read_timeout: 5000,

View File

@ -12,7 +12,6 @@ pub(crate) fn build() -> TokenStream {
quote! {
#[derive(Clone, Copy)]
#[repr(u8)]
pub enum Biome {
#variants
}

View File

@ -102,7 +102,6 @@ pub(crate) fn build() -> TokenStream {
#(#constants)*
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum DamageType {
#(#enum_variants,)*
}

View File

@ -12,7 +12,6 @@ pub(crate) fn build() -> TokenStream {
quote! {
#[derive(Clone, Copy)]
#[repr(u8)]
pub enum EntityPose {
#variants
}

View File

@ -4,6 +4,7 @@ use heck::ToPascalCase;
use proc_macro2::TokenStream;
use quote::quote;
use serde::Deserialize;
use syn::LitInt;
use crate::ident;
@ -22,9 +23,10 @@ pub(crate) fn build() -> TokenStream {
for (item, id) in json.iter() {
let id = id.id as u8;
let id_lit = LitInt::new(&id.to_string(), proc_macro2::Span::call_site());
let name = ident(item.to_pascal_case());
variants.extend([quote! {
#name = #id,
#name = #id_lit,
}]);
}
@ -54,7 +56,6 @@ pub(crate) fn build() -> TokenStream {
quote! {
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[repr(u8)]
pub enum EntityType {
#variants
}

View File

@ -12,7 +12,6 @@ pub(crate) fn build() -> TokenStream {
let variants = array_to_tokenstream(game_events);
quote! {
#[repr(u8)]
pub enum GameEvent {
#variants
}

View File

@ -12,7 +12,6 @@ pub(crate) fn build() -> TokenStream {
quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Particle {
#variants
}

View File

@ -12,7 +12,6 @@ pub(crate) fn build() -> TokenStream {
let variants = array_to_tokenstream(sound_categories);
quote! {
#[repr(u8)]
pub enum ScoreboardDisplaySlot {
#variants
}

View File

@ -12,7 +12,6 @@ pub(crate) fn build() -> TokenStream {
quote! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum WindowType {
#variants
}

View File

@ -13,7 +13,6 @@ pub(crate) fn build() -> TokenStream {
quote! {
#[derive(Clone, Copy)]
#[repr(u8)]
pub enum SoundCategory {
#variants
}

View File

@ -19,7 +19,6 @@ impl<T: WindowPropertyTrait> WindowProperty<T> {
(self.window_property.to_id(), self.value)
}
}
#[repr(u8)]
pub enum Furnace {
FireIcon,
MaximumFuelBurnTime,
@ -46,35 +45,29 @@ impl WindowPropertyTrait for EnchantmentTable {
}) as i16
}
}
#[repr(u8)]
pub enum Beacon {
PowerLevel,
FirstPotionEffect,
SecondPotionEffect,
}
#[repr(u8)]
pub enum Anvil {
RepairCost,
}
#[repr(u8)]
pub enum BrewingStand {
BrewTime,
FuelTime,
}
#[repr(u8)]
pub enum Stonecutter {
SelectedRecipe,
}
#[repr(u8)]
pub enum Loom {
SelectedPattern,
}
#[repr(u8)]
pub enum Lectern {
PageNumber,
}

View File

@ -22,7 +22,6 @@ impl CEntityAnimation {
}
#[derive(Debug)]
#[repr(u8)]
pub enum Animation {
SwingMainArm,
LeaveBed = 2,

View File

@ -39,7 +39,6 @@ impl<T> Metadata<T> {
}
}
#[repr(u8)]
pub enum MetaDataType {
Byte,
Integer,

View File

@ -19,7 +19,6 @@ impl CGameEvent {
}
}
#[repr(u8)]
pub enum GameEvent {
NoRespawnBlockAvailable,
BeginRaining,

View File

@ -67,12 +67,11 @@ pub struct PreviousMessage<'a> {
}
#[derive(Serialize)]
#[repr(i32)]
pub enum FilterType {
/// Message is not filtered at all
PassThrough = 0,
PassThrough,
/// Message is fully filtered
FullyFiltered = 1,
FullyFiltered,
/// Only some characters in the message are filtered
PartiallyFiltered(BitSet) = 2,
PartiallyFiltered(BitSet),
}

View File

@ -59,14 +59,12 @@ impl ClientPacket for CUpdateObjectives<'_> {
}
}
#[repr(u8)]
pub enum Mode {
Add,
Remove,
Update,
}
#[repr(i32)]
pub enum RenderType {
Integer,
Hearts,

View File

@ -2,7 +2,6 @@ use std::{ffi::CString, io::Cursor};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[repr(u8)]
pub enum PacketType {
// There could be other types but they are not documented
// Besides these types are enough to get server status

View File

@ -63,7 +63,6 @@ pub struct SyncedRegistry {
}
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum DimensionType {
Overworld,
OverworldCaves,

View File

@ -6,24 +6,23 @@ use serde::{Deserialize, Serialize};
pub struct ParseGameModeError;
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[repr(i8)]
pub enum GameMode {
Undefined = -1,
Survival,
Creative,
Adventure,
Spectator,
}
impl From<i8> for GameMode {
fn from(value: i8) -> Self {
impl TryFrom<i8> for GameMode {
type Error = ();
fn try_from(value: i8) -> Result<Self, Self::Error> {
match value {
-1 => Self::Undefined,
0 => Self::Survival,
1 => Self::Creative,
2 => Self::Adventure,
3 => Self::Spectator,
_ => Self::Undefined,
0 => Ok(Self::Survival),
1 => Ok(Self::Creative),
2 => Ok(Self::Adventure),
3 => Ok(Self::Spectator),
_ => Err(()),
}
}
}

View File

@ -10,7 +10,6 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// - `Three`: `admin`: Player or executor can use commands related to multiplayer management.
/// - `Four`: `owner`: Player or executor can use all of the commands, including commands related to server management.
#[derive(Clone, Copy, Default, PartialEq, Eq)]
#[repr(i8)]
pub enum PermissionLvl {
#[default]
Zero = 0,

View File

@ -27,7 +27,6 @@ const WORLD_DATA_VERSION: i32 = 4189;
pub struct AnvilChunkFormat;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Compression {
/// GZip Compression
GZip = 1,

View File

@ -34,16 +34,12 @@ impl ArgumentConsumer for GamemodeArgumentConsumer {
let s = args.pop()?;
if let Ok(id) = s.parse::<i8>() {
match GameMode::from(id) {
GameMode::Undefined => {}
gamemode => return Some(Arg::GameMode(gamemode)),
};
if let Ok(gamemode) = GameMode::try_from(id) {
return Some(Arg::GameMode(gamemode));
}
};
match GameMode::from_str(s) {
Err(_) | Ok(GameMode::Undefined) => None,
Ok(gamemode) => Some(Arg::GameMode(gamemode)),
}
GameMode::from_str(s).map_or_else(|_| None, |gamemode| Some(Arg::GameMode(gamemode)))
}
async fn suggest<'a>(

View File

@ -0,0 +1,65 @@
use crossbeam::atomic::AtomicCell;
use pumpkin_data::damage::DamageType;
use super::player::Player;
pub struct HungerManager {
/// The current hunger level.
pub level: AtomicCell<u32>,
/// The food saturation level.
pub saturation: AtomicCell<f32>,
exhaustion: AtomicCell<f32>,
tick_timer: AtomicCell<u32>,
}
impl Default for HungerManager {
fn default() -> Self {
Self {
level: AtomicCell::new(20),
saturation: AtomicCell::new(5.0),
exhaustion: AtomicCell::new(0.0),
tick_timer: AtomicCell::new(0),
}
}
}
impl HungerManager {
pub async fn tick(&self, player: &Player) {
let saturation = self.saturation.load();
let level = self.level.load();
let exhaustion = self.exhaustion.load();
// Decrease hunger level on exhaustion
if level != 0 && exhaustion > 4.0 {
self.exhaustion.store(exhaustion - 4.0);
if saturation > 0.0 {
self.saturation.store((saturation - 1.0).max(0.0));
} else {
self.level.store(level - 1);
player.send_health().await;
}
}
// Heal when hunger is full
if saturation > 0.0 && player.can_food_heal() && level >= 20 {
self.tick_timer.fetch_add(1);
if self.tick_timer.load() >= 10 {
let saturation = saturation.min(6.0);
player.heal(saturation / 6.0).await;
self.add_exhausten(saturation);
self.tick_timer.store(0);
}
} else if level == 0 {
self.tick_timer.fetch_add(1);
if self.tick_timer.load() >= 80 {
player.living_entity.damage(1.0, DamageType::Starve).await;
self.tick_timer.store(0);
}
} else {
self.tick_timer.store(0);
}
}
pub fn add_exhausten(&self, exhaustion: f32) {
self.exhaustion
.store((self.exhaustion.load() + exhaustion).min(40.0));
}
}

View File

@ -54,6 +54,12 @@ impl LivingEntity {
self.entity.set_pos(position);
}
pub async fn heal(&self, additional_health: f32) {
assert!(additional_health > 0.0);
self.set_health(self.health.load() + additional_health)
.await;
}
pub async fn set_health(&self, health: f32) {
self.health.store(health);
// tell everyone entities health changed

View File

@ -34,6 +34,7 @@ use serde::Serialize;
use crate::world::World;
pub mod ai;
pub mod hunger;
pub mod item;
pub mod living;
pub mod mob;
@ -447,7 +448,6 @@ pub trait NBTStorage: Send + Sync {
/// **Purpose:**
///
/// This enum provides a more type-safe and readable way to represent entity flags compared to using raw integer values.
#[repr(u8)]
pub enum Flag {
/// Indicates if the entity is on fire.
OnFire = 0,

View File

@ -68,6 +68,7 @@ use tokio::sync::{Mutex, Notify, RwLock};
use super::{
combat::{self, player_attack_sound, AttackType},
hunger::HungerManager,
item::ItemEntity,
Entity, EntityId, NBTStorage,
};
@ -98,10 +99,8 @@ pub struct Player {
pub config: Mutex<PlayerConfig>,
/// The player's current gamemode (e.g., Survival, Creative, Adventure).
pub gamemode: AtomicCell<GameMode>,
/// The player's hunger level.
pub food: AtomicI32,
/// The player's food saturation level.
pub food_saturation: AtomicCell<f32>,
/// The Hunger Manager manages Players hunger level
pub hunger_manager: HungerManager,
/// The ID of the currently open container (if any).
pub open_container: AtomicCell<Option<u64>>,
/// The item currently being held by the player.
@ -192,8 +191,7 @@ impl Player {
client,
awaiting_teleport: Mutex::new(None),
// TODO: Load this from previous instance
food: AtomicI32::new(20),
food_saturation: AtomicCell::new(20.0),
hunger_manager: HungerManager::default(),
current_block_destroy_stage: AtomicU8::new(0),
open_container: AtomicCell::new(None),
carried_item: AtomicCell::new(None),
@ -433,8 +431,10 @@ impl Player {
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
self.living_entity.tick();
self.tick_client_load_timeout();
self.hunger_manager.tick(self).await;
// timeout/keep alive handling
self.tick_client_load_timeout();
if now.duration_since(self.last_keep_alive_time.load()) >= Duration::from_secs(15) {
// We never got a response from our last keep alive we send
if self
@ -455,6 +455,29 @@ impl Player {
}
}
pub async fn jump(&self) {
if self.living_entity.entity.sprinting.load(Ordering::Relaxed) {
self.add_exhaustion(0.2).await;
} else {
self.add_exhaustion(0.05).await;
}
}
#[expect(clippy::cast_precision_loss)]
pub async fn progress_motion(&self, delta_pos: Vector3<f64>) {
// TODO: Swming, Glding...
if self.living_entity.entity.on_ground.load(Ordering::Relaxed) {
let delta = (delta_pos.horizontal_length() * 100.0).round() as i32;
if delta > 0 {
if self.living_entity.entity.sprinting.load(Ordering::Relaxed) {
self.add_exhaustion(0.1 * delta as f32 * 0.01).await;
} else {
self.add_exhaustion(0.0 * delta as f32 * 0.01).await;
}
}
}
}
pub fn has_client_loaded(&self) -> bool {
self.client_loaded.load(Ordering::Relaxed)
|| self.client_loaded_timeout.load(Ordering::Relaxed) == 0
@ -636,15 +659,40 @@ impl Player {
self.client.close();
}
pub async fn set_health(&self, health: f32, food: i32, food_saturation: f32) {
self.living_entity.set_health(health).await;
self.food.store(food, std::sync::atomic::Ordering::Relaxed);
self.food_saturation.store(food_saturation);
pub fn can_food_heal(&self) -> bool {
let health = self.living_entity.health.load();
let max_health = 20.0; // TODO
health > 0.0 && health < max_health
}
pub async fn add_exhaustion(&self, exhaustion: f32) {
let abilities = self.abilities.lock().await;
if abilities.invulnerable {
return;
}
self.hunger_manager.add_exhausten(exhaustion);
}
pub async fn heal(&self, additional_health: f32) {
self.living_entity.heal(additional_health).await;
self.send_health().await;
}
pub async fn send_health(&self) {
self.client
.send_packet(&CSetHealth::new(health, food.into(), food_saturation))
.send_packet(&CSetHealth::new(
self.living_entity.health.load(),
self.hunger_manager.level.load().into(),
self.hunger_manager.saturation.load(),
))
.await;
}
pub async fn set_health(&self, health: f32) {
self.living_entity.set_health(health).await;
self.send_health().await;
}
pub fn tick_client_load_timeout(&self) {
if !self.client_loaded.load(Ordering::Relaxed) {
let timeout = self.client_loaded_timeout.load(Ordering::Relaxed);
@ -1097,7 +1145,7 @@ impl Abilities {
pub fn set_for_gamemode(&mut self, gamemode: GameMode) {
match gamemode {
GameMode::Creative => {
self.flying = false; // Start not flying
// self.flying = false; // Start not flying
self.allow_flying = true;
self.creative = true;
self.invulnerable = true;
@ -1108,7 +1156,7 @@ impl Abilities {
self.creative = false;
self.invulnerable = true;
}
GameMode::Survival | GameMode::Adventure | GameMode::Undefined => {
_ => {
self.flying = false;
self.allow_flying = false;
self.creative = false;
@ -1120,7 +1168,6 @@ impl Abilities {
/// Represents the player's dominant hand.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Hand {
/// Usually the player's off-hand.
Left,

View File

@ -26,11 +26,9 @@ impl ThrownItemEntity {
speed: f32,
divergence: f32,
) {
let to_radians = |degrees: f32| degrees * PI / 180.0;
let yaw_rad = to_radians(yaw);
let pitch_rad = to_radians(pitch);
let roll_rad = to_radians(pitch + roll);
let yaw_rad = yaw.to_radians();
let pitch_rad = pitch.to_radians();
let roll_rad = (pitch + roll).to_radians();
let x = -yaw_rad.sin() * pitch_rad.cos();
let y = -roll_rad.sin();

View File

@ -70,7 +70,7 @@ pub async fn authenticate(
let auth_url = ADVANCED_CONFIG
.networking
.authentication
.auth_url
.url
.as_deref()
.unwrap_or(MOJANG_AUTHENTICATION_URL);

View File

@ -159,6 +159,14 @@ impl Player {
let last_pos = entity.pos.load();
self.living_entity.set_pos(position);
let height_difference = position.y - last_pos.y;
if entity.on_ground.load(std::sync::atomic::Ordering::Relaxed)
&& !packet.ground
&& height_difference > 0.0
{
self.jump().await;
}
entity
.on_ground
.store(packet.ground, std::sync::atomic::Ordering::Relaxed);
@ -195,7 +203,6 @@ impl Player {
)
.await;
if !self.abilities.lock().await.flying {
let height_difference = position.y - last_pos.y;
self.living_entity
.update_fall_distance(
height_difference,
@ -205,6 +212,12 @@ impl Player {
.await;
}
chunker::update_position(self).await;
self.progress_motion(Vector3::new(
position.x - last_pos.x,
position.y - last_pos.y,
position.z - last_pos.z,
))
.await;
}
pub async fn handle_position_rotation(self: &Arc<Self>, packet: SPlayerPositionRotation) {
@ -236,6 +249,13 @@ impl Player {
let last_pos = entity.pos.load();
self.living_entity.set_pos(position);
let height_difference = position.y - last_pos.y;
if entity.on_ground.load(std::sync::atomic::Ordering::Relaxed)
&& !packet.ground
&& height_difference > 0.0
{
self.jump().await;
}
entity
.on_ground
.store(packet.ground, std::sync::atomic::Ordering::Relaxed);
@ -287,7 +307,6 @@ impl Player {
)
.await;
if !self.abilities.lock().await.flying {
let height_difference = position.y - last_pos.y;
self.living_entity
.update_fall_distance(
height_difference,
@ -297,6 +316,12 @@ impl Player {
.await;
}
chunker::update_position(self).await;
self.progress_motion(Vector3::new(
position.x - last_pos.x,
position.y - last_pos.y,
position.z - last_pos.z,
))
.await;
}
pub async fn handle_rotation(&self, rotation: SPlayerRotation) {
@ -804,6 +829,9 @@ impl Player {
.await;
}
}
self.client
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence))
.await;
}
Status::CancelledDigging => {
if !self.can_interact_with_block_at(&player_action.location, 1.0) {
@ -816,6 +844,9 @@ impl Player {
}
self.current_block_destroy_stage
.store(0, std::sync::atomic::Ordering::Relaxed);
self.client
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence))
.await;
}
Status::FinishedDigging => {
// TODO: do validation
@ -841,6 +872,9 @@ impl Player {
.broken(block, &self, location, server)
.await;
}
self.client
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence))
.await;
}
Status::DropItemStack | Status::DropItem => {
self.drop_item(server).await;
@ -851,10 +885,6 @@ impl Player {
},
Err(_) => self.kick(TextComponent::text("Invalid status")).await,
}
self.client
.send_packet(&CAcknowledgeBlockChange::new(player_action.sequence))
.await;
}
pub async fn handle_keep_alive(&self, keep_alive: SKeepAlive) {

View File

@ -9,7 +9,6 @@ use tokio::io::AsyncReadExt;
/// Client -> Server
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum ServerboundPacket {
/// Typically, the first packet sent by the client, which is used to authenticate the connection with the server.
Auth = 2,
@ -29,7 +28,6 @@ impl ServerboundPacket {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
/// Server -> Client
pub enum ClientboundPacket {
/// This packet is a notification of the connection's current auth status. When the server receives an auth request, it will respond with an empty `SERVERDATA_RESPONSE_VALUE`,

View File

@ -61,11 +61,12 @@ impl CachedBranding {
pub fn get_branding(&self) -> CPluginMessage {
CPluginMessage::new("minecraft:brand", &self.cached_server_brand)
}
const BRAND: &str = "Pumpkin";
const BRAND_BYTES: &[u8] = Self::BRAND.as_bytes();
fn build_brand() -> Vec<u8> {
let brand = "Pumpkin";
let mut buf = Vec::new();
VarInt(brand.len() as i32).encode(&mut buf);
buf.extend_from_slice(brand.as_bytes());
VarInt(Self::BRAND.len() as i32).encode(&mut buf);
buf.extend_from_slice(Self::BRAND_BYTES);
buf
}
}

View File

@ -13,7 +13,6 @@ use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::text::TextComponent;
use pumpkin_util::GameMode;
use pumpkin_world::block::registry::Block;
use pumpkin_world::dimension::Dimension;
use pumpkin_world::entity::registry::get_entity_by_id;
@ -173,10 +172,7 @@ impl Server {
/// You still have to spawn the Player in the World to make then to let them Join and make them Visible
pub async fn add_player(&self, client: Arc<Client>) -> (Arc<Player>, Arc<World>) {
let entity_id = self.new_entity_id();
let gamemode = match BASIC_CONFIG.default_gamemode {
GameMode::Undefined => GameMode::Survival,
game_mode => game_mode,
};
let gamemode = BASIC_CONFIG.default_gamemode;
// Basically the default world
// TODO: select default from config
let world = &self.worlds.read().await[0];

View File

@ -613,7 +613,7 @@ impl World {
chunker::player_join(player).await;
// update commands
player.set_health(20.0, 20, 20.0).await;
player.set_health(20.0).await;
}
/// IMPORTANT: Chunks have to be non-empty