From fe0c442a02f905c55f2383879c78fd15e6eabfa8 Mon Sep 17 00:00:00 2001 From: "Eshan I." <2027eiyer@tjhsst.edu> Date: Thu, 30 Jul 2026 08:21:39 -0400 Subject: [PATCH] fix(player): apply food exhaustion when attacking and mining (#2578) Vanilla Player#attack ends its successful-hit branch with causeFoodExhaustion(0.1F), and Block#playerDestroy calls player.causeFoodExhaustion(0.005F) for every block destroyed. Pumpkin never called add_exhaustion from either path: the only callers were the Hunger status effect in entity/living.rs and jump/sprint movement in entity/player.rs. Combat and mining were therefore free, so the hunger bar was effectively cosmetic for a player who was not sprinting. Player::attack now adds 0.1 exhaustion at the end of the hit path, after the damage_with_context early-return, so only hits that actually deal damage cost hunger. The Java and Bedrock block-break handlers now add MINE_BLOCK_EXHAUSTION (0.005) per block broken. Mining exhaustion is gated exactly where vanilla gates Block#playerDestroy: ServerPlayerGameMode#destroyBlock skips it in creative and when the block was broken without a tool that can harvest it, so the call sites reuse the existing can_harvest / block_drop checks rather than adding new conditions. Creative and spectator players are additionally covered by the existing abilities.invulnerable guard in Player::add_exhaustion, which mirrors vanilla Player#causeFoodExhaustion. Limitation: swimming exhaustion (0.01 per metre) is still missing. Player::progress_motion only implements the onGround branch of vanilla ServerPlayer#checkMovementStatistics; adding the swimming and in-water branches requires the whole else-if chain plus movement-state plumbing (Player::is_swimming is itself still an inferred approximation carrying a TODO), so it is deliberately left out of this change. --- pumpkin/src/entity/player.rs | 13 +++++++++++++ pumpkin/src/net/bedrock/play.rs | 12 +++++++++++- pumpkin/src/net/java/play.rs | 9 ++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index f184c3664..95609f954 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -122,6 +122,14 @@ const MAX_PREVIOUS_MESSAGES: u8 = 20; // Vanilla: 20 pub const DATA_VERSION: i32 = 4903; // 26.2 +/// Food exhaustion applied for every block a player mines. +/// +/// Vanilla: `Block#playerDestroy` calls `player.causeFoodExhaustion(0.005F)`. +/// `ServerPlayerGameMode#destroyBlock` only reaches `playerDestroy` for +/// non-creative players holding a tool that can harvest the block, so callers +/// must apply the same gating. +pub const MINE_BLOCK_EXHAUSTION: f32 = 0.005; // Vanilla: 0.005F + struct HeapNode(i32, Vector2, Weak); impl Eq for HeapNode {} @@ -1164,6 +1172,11 @@ impl Player { }) .await; + // Vanilla `Player#attack` ends the successful-hit branch with + // `causeFoodExhaustion(0.1F)`. Only landed hits exhaust; the miss/no-damage + // case returned early above. + self.add_exhaustion(0.1).await; + if config.swing {} } diff --git a/pumpkin/src/net/bedrock/play.rs b/pumpkin/src/net/bedrock/play.rs index 36c0e43ea..acb9c73dd 100644 --- a/pumpkin/src/net/bedrock/play.rs +++ b/pumpkin/src/net/bedrock/play.rs @@ -49,7 +49,10 @@ use pumpkin_world::world::BlockFlags; use crate::{ block::{BlockHitResult, registry::BlockActionResult}, - entity::{EntityBase, player::Player}, + entity::{ + EntityBase, + player::{MINE_BLOCK_EXHAUSTION, Player}, + }, net::{DisconnectReason, bedrock::BedrockClient}, plugin::player::{ item_held::PlayerItemHeldEvent, @@ -1099,6 +1102,7 @@ impl BedrockClient { let speed = crate::block::calc_block_breaking(player, state, block).await; if speed >= 1.0 { let broken_state = world.get_block_state(&location); + let can_harvest = player.can_harvest(broken_state, block).await; let new_state = world .break_block( &location, @@ -1112,6 +1116,9 @@ impl BedrockClient { .broken(&world, block, player, &location, server, broken_state) .await; player.apply_tool_damage_for_block_break(broken_state).await; + if can_harvest { + player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await; + } } } else { player.mining.store(true, Ordering::Relaxed); @@ -1157,6 +1164,9 @@ impl BedrockClient { .broken(&world, block, player, &location, server, state) .await; player.apply_tool_damage_for_block_break(state).await; + if block_drop { + player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await; + } } } } diff --git a/pumpkin/src/net/java/play.rs b/pumpkin/src/net/java/play.rs index 41d057198..738ae5eba 100644 --- a/pumpkin/src/net/java/play.rs +++ b/pumpkin/src/net/java/play.rs @@ -16,7 +16,7 @@ use crate::block::{self}; use crate::entity::EntityBase; use crate::entity::equipment_break_status; use crate::entity::player::statistics::{CustomStatistic, StatisticCategory}; -use crate::entity::player::{ChatMode, ChatSession, Player}; +use crate::entity::player::{ChatMode, ChatSession, MINE_BLOCK_EXHAUSTION, Player}; use crate::error::PumpkinError; use crate::log_at_level; use crate::net::PlayerConfig; @@ -1993,6 +1993,7 @@ impl JavaClient { // Instant break if speed >= 1.0 { let broken_state = world.get_block_state(&position); + let can_harvest = player.can_harvest(broken_state, block).await; let new_state = world .break_block( &position, @@ -2006,6 +2007,9 @@ impl JavaClient { .broken(&world, block, player, &position, server, broken_state) .await; player.apply_tool_damage_for_block_break(broken_state).await; + if can_harvest { + player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await; + } let item_id = player.inventory().held_item().lock().await.item.id; player .increment_stat(StatisticCategory::Used, item_id as i32, 1) @@ -2090,6 +2094,9 @@ impl JavaClient { .await; player.apply_tool_damage_for_block_break(state).await; + if block_drop { + player.add_exhaustion(MINE_BLOCK_EXHAUSTION).await; + } let item_id = player.inventory().held_item().lock().await.item.id; player .increment_stat(StatisticCategory::Used, item_id as i32, 1)