mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Pour a refused water bucket beside the clicked block
BucketItem.emptyContents retries at the block beside the clicked face when the clicked block refuses the fluid. Without it, a water bucket on a double slab did nothing, and a sneak-click on a waterloggable block failed instead of pouring next to it. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
10d2551439
commit
f37728687d
4 files changed
+103
-23
No files matched your search
@@ -13048,18 +13048,25 @@ impl Game<'_> {
|
||||
return Ok(UseResult::Fail);
|
||||
}
|
||||
let pos = BlockPos::new(target[0], target[1], target[2]);
|
||||
let beside = BlockPos::new(
|
||||
hit.pos[0] + hit.face[0],
|
||||
hit.pos[1] + hit.face[1],
|
||||
hit.pos[2] + hit.face[2],
|
||||
);
|
||||
// `Player.isShiftKeyDown`: only air takes a fluid under Shift.
|
||||
let sneaking = self.keys.contains(&KeyCode::ShiftLeft);
|
||||
let creative = self.creative;
|
||||
let emptied = self.with_level(|b, level, ticks| {
|
||||
b.bucket_empty(level, ticks, pos, content, sound, sneaking, creative)
|
||||
b.bucket_empty_beside(
|
||||
level, ticks, pos, beside, content, sound, sneaking, creative,
|
||||
)
|
||||
});
|
||||
self.process_level_events()?;
|
||||
if !emptied {
|
||||
let Some(emptied) = emptied else {
|
||||
return Ok(UseResult::Fail);
|
||||
}
|
||||
};
|
||||
if self.dim == Dim::Nether && content.is_water() {
|
||||
self.evaporation_smoke(target);
|
||||
self.evaporation_smoke([emptied.x, emptied.y, emptied.z]);
|
||||
}
|
||||
// `BucketItem.emptyContents` uses `playSound(player, …)`: the
|
||||
// actor's client already played the empty / evaporate sound from
|
||||
|
||||
@@ -7652,18 +7652,26 @@ impl Game<'_> {
|
||||
};
|
||||
let target = self.bucket_target(content, &hit);
|
||||
let pos = horizon_core::position::BlockPos::new(target[0], target[1], target[2]);
|
||||
let beside = horizon_core::position::BlockPos::new(
|
||||
hit.pos[0] + hit.face[0],
|
||||
hit.pos[1] + hit.face[1],
|
||||
hit.pos[2] + hit.face[2],
|
||||
);
|
||||
let sneaking = self.keys.contains(&KeyCode::ShiftLeft);
|
||||
let creative = self.creative;
|
||||
// A water bucket on a liquid container fills it server-side only.
|
||||
// A water bucket on a liquid container fills it server-side only;
|
||||
// a container that refuses passes it to the block beside, which
|
||||
// this client changes itself.
|
||||
let clicked = self.world.block(hit.pos[0], hit.pos[1], hit.pos[2]);
|
||||
let client_change =
|
||||
!(content.is_water() && self.res.behaviours().is_liquid_container(clicked));
|
||||
let container = content.is_water() && self.res.behaviours().is_liquid_container(clicked);
|
||||
self.with_level(|b, level, ticks| {
|
||||
let mut level = DryLevel::new(level);
|
||||
let emptied =
|
||||
b.bucket_empty(&mut level, ticks, pos, content, sound, sneaking, creative);
|
||||
let emptied = b.bucket_empty_beside(
|
||||
&mut level, ticks, pos, beside, content, sound, sneaking, creative,
|
||||
);
|
||||
let client_change = !(container && emptied == Some(pos));
|
||||
(
|
||||
emptied,
|
||||
emptied.is_some(),
|
||||
client_change.then(|| level.changed.into_iter().collect()),
|
||||
std::mem::take(&mut level.sounds),
|
||||
)
|
||||
|
||||
@@ -60,18 +60,22 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
};
|
||||
self.pickup_block(pos, state, creative).then_some(item)
|
||||
}
|
||||
/// `BucketItem.emptyContents(user, level, pos, hitResult)`. `sound` is the
|
||||
/// bucket's empty sound (mob buckets have their own); `sneaking` is
|
||||
/// `BucketItem.emptyContents(user, level, pos, hitResult)`: where the
|
||||
/// contents went, None when nothing was emptied. `beside` is
|
||||
/// `hitResult.getBlockPos().relative(hitResult.getDirection())` (None for
|
||||
/// a dispenser, which has no hit result). `sound` is the bucket's empty
|
||||
/// sound (mob buckets have their own); `sneaking` is
|
||||
/// `user.isShiftKeyDown()` (false for a dispenser, whose user is null);
|
||||
/// `creative` is whether that user is a creative player.
|
||||
pub(crate) fn bucket_empty(
|
||||
&mut self,
|
||||
pos: BlockPos,
|
||||
beside: Option<BlockPos>,
|
||||
content: BucketContent,
|
||||
sound: &str,
|
||||
sneaking: bool,
|
||||
creative: bool,
|
||||
) -> bool {
|
||||
) -> Option<BlockPos> {
|
||||
let b = self.b;
|
||||
let fluid = match content {
|
||||
BucketContent::Powder => {
|
||||
@@ -82,14 +86,14 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
self.game_event(pos, "minecraft:fluid_place");
|
||||
let sound: Arc<str> = sound.into();
|
||||
self.sound(pos, &sound, 1.0, 1.0);
|
||||
return true;
|
||||
return Some(pos);
|
||||
}
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
BucketContent::Nothing => {
|
||||
let sound: Arc<str> = sound.into();
|
||||
self.sound(pos, &sound, 1.0, 1.0);
|
||||
return true;
|
||||
return Some(pos);
|
||||
}
|
||||
BucketContent::MobWater => FluidKind::Water,
|
||||
BucketContent::Fluid(fluid) => fluid,
|
||||
@@ -108,7 +112,12 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
// when the player is not sneaking (a dispenser has no user and so
|
||||
// no hit result, which always allows it).
|
||||
if !(b.is_air(state) || place_liquid && !sneaking) {
|
||||
return false;
|
||||
// A refused click (a double slab, or Shift on a waterloggable
|
||||
// block) pours into the block beside the clicked face instead,
|
||||
// without a hit result, so Shift no longer matters there.
|
||||
return beside.and_then(|beside| {
|
||||
self.bucket_empty(beside, None, content, sound, false, creative)
|
||||
});
|
||||
}
|
||||
let water = matches!(fluid, FluidKind::Water | FluidKind::FlowingWater);
|
||||
if water && self.level.dimension() == Dimension::Nether {
|
||||
@@ -120,7 +129,7 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
volume: 0.5,
|
||||
pitch,
|
||||
});
|
||||
return true;
|
||||
return Some(pos);
|
||||
}
|
||||
if liquid_container && water {
|
||||
self.place_liquid(
|
||||
@@ -130,7 +139,7 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
creative,
|
||||
);
|
||||
self.empty_sound(pos, sound, mob);
|
||||
return true;
|
||||
return Some(pos);
|
||||
}
|
||||
if may_replace && !b.instance_of(state, Class::Liquid) {
|
||||
self.destroy_block(pos, true, super::flags::UPDATE_LIMIT);
|
||||
@@ -148,10 +157,10 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
if !self.set_block(pos, legacy, super::flags::UPDATE_ALL_IMMEDIATE)
|
||||
&& !self.fluid_at(pos).is_source()
|
||||
{
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
self.empty_sound(pos, sound, mob);
|
||||
true
|
||||
Some(pos)
|
||||
}
|
||||
/// `playEmptySound`: `MobBucketItem` overrides it without the game event.
|
||||
fn empty_sound(&mut self, pos: BlockPos, sound: &str, mob: bool) {
|
||||
@@ -175,7 +184,7 @@ impl Behaviours {
|
||||
) -> Option<&'static str> {
|
||||
self.sim(level, ticks).bucket_pickup(pos, creative)
|
||||
}
|
||||
/// `DispensibleContainerItem.emptyContents(user, level, pos, hitResult)`.
|
||||
/// `DispensibleContainerItem.emptyContents(user, level, pos, null)`.
|
||||
pub fn bucket_empty<L: LevelAccess + ?Sized>(
|
||||
&self,
|
||||
level: &mut L,
|
||||
@@ -187,6 +196,24 @@ impl Behaviours {
|
||||
creative: bool,
|
||||
) -> bool {
|
||||
self.sim(level, ticks)
|
||||
.bucket_empty(pos, content, sound, sneaking, creative)
|
||||
.bucket_empty(pos, None, content, sound, sneaking, creative)
|
||||
.is_some()
|
||||
}
|
||||
/// `BucketItem.emptyContents(user, level, pos, hitResult)` for a click:
|
||||
/// `beside` is the block beside the clicked face, tried when `pos`
|
||||
/// refuses. Returns where the contents went.
|
||||
pub fn bucket_empty_beside<L: LevelAccess + ?Sized>(
|
||||
&self,
|
||||
level: &mut L,
|
||||
ticks: &mut Ticks,
|
||||
pos: BlockPos,
|
||||
beside: BlockPos,
|
||||
content: BucketContent,
|
||||
sound: &str,
|
||||
sneaking: bool,
|
||||
creative: bool,
|
||||
) -> Option<BlockPos> {
|
||||
self.sim(level, ticks)
|
||||
.bucket_empty(pos, Some(beside), content, sound, sneaking, creative)
|
||||
}
|
||||
}
|
||||
@@ -2295,6 +2295,44 @@ fn dispenser_buckets_pick_up_and_empty() {
|
||||
.bucket_empty(&mut w.level, &mut w.ticks, at, content, sound, true, false)
|
||||
);
|
||||
assert_eq!(w.prop(at, "waterlogged"), "false");
|
||||
// A click on the slab pours beside it, where Shift no longer matters.
|
||||
let above = BlockPos::new(at.x, at.y + 1, at.z);
|
||||
assert_eq!(
|
||||
w.b().bucket_empty_beside(
|
||||
&mut w.level,
|
||||
&mut w.ticks,
|
||||
at,
|
||||
above,
|
||||
content,
|
||||
sound,
|
||||
true,
|
||||
false
|
||||
),
|
||||
Some(above)
|
||||
);
|
||||
assert_eq!(w.prop(at, "waterlogged"), "false");
|
||||
assert_eq!(w.name(above), "water[level=0]");
|
||||
// A double slab holds no water: the click fills the block beside it.
|
||||
let air = w.b().air();
|
||||
w.put(above, air);
|
||||
let double = w.id("oak_slab", &[("type", "double")]);
|
||||
w.put(at, double);
|
||||
assert_eq!(
|
||||
w.b().bucket_empty_beside(
|
||||
&mut w.level,
|
||||
&mut w.ticks,
|
||||
at,
|
||||
above,
|
||||
content,
|
||||
sound,
|
||||
false,
|
||||
false
|
||||
),
|
||||
Some(above)
|
||||
);
|
||||
assert_eq!(w.prop(at, "type"), "double");
|
||||
assert_eq!(w.name(above), "water[level=0]");
|
||||
w.put(above, air);
|
||||
// Powder snow only goes into empty space.
|
||||
let air = w.b().air();
|
||||
w.put(at, air);
|
||||
|
||||
Reference in new issue
Block a user