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.
This commit is contained in:
Eshan I.
2026-07-30 08:21:39 -04:00
committed by GitHub
parent 0be7e49e52
commit fe0c442a02
3 changed files with 32 additions and 2 deletions

View File

@@ -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<i32>, Weak<ChunkData>);
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 {}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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)