Merge pull request #10200 from Hiuoy/feat/break-speed

Native hooks: PlayerEvent.BreakSpeed, how fast a player digs a block
This commit is contained in:
Kacper authored and GitHub committed 2026-11-06 09:17:32 +01:00
commit f7a5a3809f
6 files changed
+65 -42

No files matched your search

+2 -33
View File
@@ -7609,10 +7609,8 @@ impl Game<'_> {
ticks: 0,
});
}
let block_break_speed = self.block_break_speed();
let state = self.world.block(hit.pos[0], hit.pos[1], hit.pos[2]);
let facts = self.res.facts().get(state);
if facts.hardness < 0.0 {
if self.res.facts().get(state).hardness < 0.0 {
// The server still counts its destroy ticks on an unbreakable block.
let m = self.mining.as_mut().ok_or("mining")?;
m.ticks += 1;
@@ -7620,36 +7618,7 @@ impl Game<'_> {
self.destroy_progress_event(pos, hit.face, ticks);
return Ok(());
}
// Player.getDestroySpeed / BlockBehaviour.getDestroyProgress.
let held = self.held();
let mut speed = self.items().mining_speed(held, &facts.name);
let hardness = facts.hardness;
let harvest = !facts.requires_tool || self.items().correct_for_drops(held, &facts.name);
speed = self.modify_destroy_speed(speed);
speed *= block_break_speed;
let player = self.player.as_ref().ok_or("no player")?;
if !player.ground.on_ground {
speed /= 5.0;
}
let eye = self.eye_position().unwrap_or_default();
if self
.res
.facts()
.get(self.world.block(
eye[0].floor() as i32,
eye[1].floor() as i32,
eye[2].floor() as i32,
))
.fluid
== Fluid::Water
{
speed *= self.submerged_mining_speed();
}
let progress = if hardness == 0.0 {
1.0
} else {
speed / hardness / if harvest { 30.0 } else { 100.0 }
};
let progress = self.destroy_progress(hit.pos);
self.swing_arm();
let m = self.mining.as_mut().ok_or("mining")?;
m.progress += progress;
+5 -1
View File
@@ -383,7 +383,9 @@ impl Game<'_> {
}
Ok(())
}
/// `BlockBehaviour.getDestroyProgress` with `Player.getDestroySpeed`.
/// `BlockBehaviour.getDestroyProgress` with `Player.getDestroySpeed`, whose
/// end asks the mods' `native_break_speed`. Every dig's progress comes from
/// here: single player's, a host's for each guest, a remote client's own.
pub(super) fn destroy_progress(&mut self, pos: [i32; 3]) -> f32 {
let block_break_speed = self.block_break_speed();
let state = self.world.block(pos[0], pos[1], pos[2]);
@@ -414,6 +416,8 @@ impl Game<'_> {
if self.player.as_ref().is_some_and(|p| !p.ground.on_ground) {
speed /= 5.0;
}
// NeoForge's PlayerEvent.BreakSpeed, where Player.getDestroySpeed ends.
self.native_break_speed(pos, state, speed, &mut speed);
speed / hardness / if harvest { 30.0 } else { 100.0 }
}
/// A key for the main-hand stack (`destroyingItem`): its item and components.
@@ -2587,6 +2587,30 @@ impl Game<'_> {
) {
mod_api::dispatch_left_click_block(self, mod_api::mods(), pos, face, state, canceled);
}
/// NeoForge's `PlayerEvent.BreakSpeed`: how fast the player digs the block
/// `state` at `pos`, where `Player.getDestroySpeed` ends. `speed` starts as
/// vanilla's, `original` (the tool, mining efficiency, Haste and Mining
/// Fatigue, the block break speed attribute, the slowdowns under water and
/// off the ground), and a mod may change it; every mod is called in turn,
/// and `original` stays vanilla's. `BlockBehaviour.getDestroyProgress` then
/// divides the speed by the block's hardness and by 30, or 100 without the
/// right tool, for each tick of the dig: a speed of 0 makes no progress, so
/// the block isn't broken, not even one that breaks at once. Not asked for
/// an unbreakable block (hardness -1) nor in creative mode. Fires on the
/// `Game` that works out the dig: single player's, a LAN or dedicated
/// host's for each guest (the helpers read that player), and a remote
/// client's own prediction, so a mod changes the speed alike on both sides
/// to keep the crack in step with the server. It is asked every tick of a
/// dig, on each side, so it should only compute. API @API:break_speed@.
pub(super) fn native_break_speed(
&self,
pos: [i32; 3],
state: BlockStateId,
original: f32,
speed: &mut f32,
) {
mod_api::dispatch_break_speed(self, mod_api::mods(), pos, state, original, speed);
}
/// A living player's server tick ends (`ServerPlayer.tick`): once a tick
/// for every player in the world, after their digging, item use,
/// cooldowns, statistics and advancements. The helpers read that player.
+5 -8
View File
@@ -7492,15 +7492,12 @@ impl Game<'_> {
}
return;
}
let facts = self.res.facts().get(state).clone();
// The server's own getDestroyProgress: under water, off the ground,
// with haste, fatigue and efficiency, so the client stops digging
// when the server is done too (#1254).
let progress = if facts.hardness == 0.0 {
1.0
} else {
self.destroy_progress(hit.pos)
};
// with haste, fatigue, efficiency and the mods' native_break_speed, so
// the client stops digging when the server is done too (#1254). A
// block of hardness 0 gives infinity, breaking at once, unless a mod
// made the speed 0.
let progress = self.destroy_progress(hit.pos);
// sameDestroyTarget: the same block and still the item the break began with.
let held = self.main_hand_key();
let same = self.remote.as_ref().is_some_and(|r| {
+13
View File
@@ -455,6 +455,19 @@ a version. Helpers (`native_text`, `native_player`…) have no hook entry; the c
the generated `Host` offers, and they are kept stable as the hook points are. A test (`catalog_matches_hook_points`) keeps the
catalog and the hook points' sources (`native_hooks.rs`, `gfx/native_render.rs`, `horizon-world`'s `loot.rs`) in step.
### How fast a player digs
`native_break_speed` is NeoForge's `PlayerEvent.BreakSpeed`, at the end of vanilla's `Player.getDestroySpeed`. It is
told the block's state and position, and the speed vanilla gives the player for it: the tool, mining efficiency, Haste
and Mining Fatigue, the block break speed attribute, and the slowdowns under water and off the ground. A mod may change
`speed`. Every mod is called in turn, and `original` stays vanilla's. `BlockBehaviour.getDestroyProgress` then divides
the speed by the block's hardness, and by 30 (or 100 without the right tool), for each tick of the dig. A speed of 0
makes no progress, so the block isn't broken, not even one that breaks at once. It isn't asked for an unbreakable
block (hardness -1) or in creative mode. It fires on the `Game` that works out the dig: in single player, on a LAN or
dedicated host for each guest (the helpers read that player), and in a remote client's own prediction. A mod that
changes the speed alike on both sides keeps the crack in step with the server. It is asked every tick of a dig, on
each side, so it takes `&self` and should only compute. API @API:break_speed@.
### Host-aware weapon damage (API next)
`living_incoming_damage_v2` is an additive hook, like `menu_button_v2`; v1 keeps its signature and firing point.
@@ -0,0 +1,16 @@
{
"hooks": [
{
"name": "break_speed",
"target": "horizon_client::game::native_hooks::Game<'_>::native_break_speed",
"neoforge": "PlayerEvent.BreakSpeed",
"side": "both",
"cancellable": false,
"mod_api": {
"contract": "listen"
}
}
],
"rust_summary": "how fast a player digs a block (`native_break_speed`).",
"doc_summary": "how fast a player digs a block: `native_break_speed` may change the speed that `Player.getDestroySpeed` gives, as NeoForge's `PlayerEvent.BreakSpeed` does, on both sides ([How fast a player digs](#how-fast-a-player-digs)). Bundles need \"hooks\": @API:break_speed@."
}