Merge pull request #10022 from DUzzL/fix/9964-burning-powder-snow

Burning players and mobs destroy powder snow they stand in
This commit is contained in:
Kacper authored and GitHub committed 2026-11-06 09:00:43 +01:00
commit d16b78e715
5 files changed
+100 -9

No files matched your search

@@ -4748,8 +4748,10 @@ impl Game<'_> {
let mut events = std::mem::take(&mut w.player.events);
// LayeredCauldronBlock.handleEntityOnFireInside along those moves.
for event in &events {
if let MotionEvent::CauldronExtinguish { pos } = event {
game.cauldron_extinguish(*pos);
match event {
MotionEvent::CauldronExtinguish { pos } => game.cauldron_extinguish(*pos),
MotionEvent::PowderSnowMelt { pos } => game.powder_snow_melt(*pos),
_ => {}
}
}
// Entity.baseTick: rain puts out a burning player.
@@ -34,6 +34,18 @@ impl Game<'_> {
}
self.extinguish_sound([at.x, at.y, at.z]);
}
/// `PowderSnowBlock.entityInside` (runBefore EXTINGUISH) for a burning
/// player: `Level.destroyBlock(pos, false)`. A client of a remote server
/// leaves it to the server.
pub(super) fn powder_snow_melt(&mut self, pos: BlockPos) {
let at = [pos.x, pos.y, pos.z];
if self.remote.is_some() || !self.is_powder_snow(at) {
return;
}
self.with_level(|b, level, ticks| b.destroy_block(level, ticks, pos, false));
let _ = self.process_level_events();
self.world.modified.insert((at[0] >> 4, at[2] >> 4));
}
/// Whether the block at `pos` is powder snow.
pub(super) fn is_powder_snow(&self, pos: [i32; 3]) -> bool {
&*self
+3
View File
@@ -6411,6 +6411,9 @@ impl Game<'_> {
// NetherPortalBlock.entityInside on the client's own level:
// the portal overlay and its trigger sound (the server moves
// the player).
horizon_client::motion::MotionEvent::PowderSnowMelt { pos } => {
self.powder_snow_melt(pos);
}
horizon_client::motion::MotionEvent::InsidePortal { .. } => {
self.client_inside_nether_portal();
}
+11
View File
@@ -840,6 +840,17 @@ fn effects_from_blocks(
},
));
visited.push(p);
// PowderSnowBlock.entityInside: a burning mob destroys the
// block (with `mob_griefing`), then EXTINGUISH puts it out.
if w.block_name(x, y, z) == "minecraft:powder_snow" {
if m.is_on_fire() && w.mob_griefing {
events.push(MobEvent::BreakBlock {
pos: p,
drop: false,
});
}
m.extinguish_fire();
}
match w.block_name(x, y, z) {
"minecraft:cactus" => {
m.damage(1.0, Damage::of(DamageKind::Cactus));
+70 -7
View File
@@ -229,6 +229,11 @@ pub enum MotionEvent {
CauldronExtinguish {
pos: BlockPos,
},
/// `PowderSnowBlock.entityInside` (runBefore EXTINGUISH): a burning
/// player in powder snow destroys the block, without drops.
PowderSnowMelt {
pos: BlockPos,
},
/// `WitherRoseBlock.entityInside`: the player's box swept through a
/// wither rose; the server adds its Wither unless peaceful.
InsideWitherRose,
@@ -435,9 +440,6 @@ pub struct PlayerMotion {
/// `Entity.applyEffectsFromBlocks` runs after travel (always in the game;
/// differential fixtures turn it off where their reference does).
pub block_effects: bool,
/// The powder snow blocks this tick's movement went into: the game
/// destroys them if the player is burning, then puts the fire out.
pub powder_snow_entered: Vec<BlockPos>,
/// The last hurts taken (`hurt_blocking`), newest last, for tests and
/// the control socket's `hurtlog`.
pub hurt_log: Vec<crate::health::HurtRecord>,
@@ -631,7 +633,6 @@ impl PlayerMotion {
entity_id: 0,
client_loaded: true,
block_effects: true,
powder_snow_entered: Vec::new(),
hurt_log: Vec::new(),
attack_log: Vec::new(),
border: None,
@@ -1677,11 +1678,17 @@ impl MotionWorld<'_, '_, '_> {
groups += 1;
last_step = step;
}
if groups == 1 && !damage_only {
// A later group finds the fire already out.
self.player.powder_snow_entered.push(p);
// A later group finds the fire already out.
if groups == 1 && self.player.fire_ticks > 0 && !self.player.spectator {
self.player
.events
.push(MotionEvent::PowderSnowMelt { pos: p });
}
}
if groups > 0 {
// InsideBlockEffectType.EXTINGUISH: Entity.clearFire.
self.player.fire_ticks = self.player.fire_ticks.min(0);
}
for _ in 0..groups {
if self.player.health.dead_or_dying() {
break;
@@ -3548,6 +3555,62 @@ mod tests {
assert_eq!(asks(&dry), 0);
}
/// `PowderSnowBlock.entityInside`: a burning player in powder snow asks
/// for the block to be destroyed, and EXTINGUISH puts the fire out.
#[test]
fn a_burning_player_melts_powder_snow() {
let root = std::env::var_os("HORIZON_TEST_RESOURCES")
.map(std::path::PathBuf::from)
.unwrap_or_else(|| {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../.local/horizon-client")
.join(horizon_core::VANILLA_VERSION)
});
if !root.join("movement-properties.nbt").exists() {
return;
}
let assets = Assets::load(&root).unwrap();
let render = assets.reload(&root).unwrap();
let binding = crate::collisions::load(&root, assets.registry).unwrap();
let resources = MotionResources::load(&root, &assets).unwrap();
let mut scene = World::test_scene(&assets, &render, 1).unwrap();
let snow = assets
.registry
.default_state("minecraft:powder_snow")
.unwrap();
scene.set(20, 4, 20, snow).unwrap();
let mut run = |fire: i32| {
let mut player = PlayerMotion::new(&resources, Vec3::new(20.5, 4.0, 20.5)).unwrap();
player.fire_ticks = fire;
MotionWorld {
entities: &[],
player: &mut player,
scene: &mut scene,
assets: &assets,
collision_binding: &binding,
resources: &resources,
health: &resources.health,
}
.controlled_simulation_tick(Default::default())
.unwrap();
player
};
let burning = run(100);
assert!(burning.fire_ticks <= 0, "powder snow must put the fire out");
assert!(
burning
.events
.iter()
.any(|e| matches!(e, MotionEvent::PowderSnowMelt { .. }))
);
let dry = run(-20);
assert!(
!dry.events
.iter()
.any(|e| matches!(e, MotionEvent::PowderSnowMelt { .. }))
);
}
#[test]
fn standing_in_a_wither_rose_asks_for_its_effect() {
let root = std::env::var_os("HORIZON_TEST_RESOURCES")