mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Let the off hand go when a bucket, bottle or boat has no target
On a server client (single player included) remote_use_item predicted SUCCESS for every bucket, glass bottle and boat, which ended Minecraft.startUseItem before the off hand's turn: a Riptide trident there never charged. BucketItem, BottleItem and BoatItem.use PASS or FAIL with nothing to fill, empty or place, and startUseItem stops only on SUCCESS. The client now checks the target first (a waterlogged block counts as a water source, as SimpleWaterloggedBlock is a BucketPickup); place_boat is split into boat_to_place (no side effects) so both share BoatItem's checks. A powder snow bucket (SolidBucketItem, only useOn) always passes its use. Fixes #3147 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
11494e17f9
commit
53beacfd13
2 files changed
+86
-17
No files matched your search
@@ -658,15 +658,30 @@ impl Game<'_> {
|
||||
}
|
||||
/// `BoatItem.use`: aim at a block or the water surface.
|
||||
pub(super) fn place_boat(&mut self, stack: &ItemStack) -> bool {
|
||||
let Some((wood, chest)) = any_boat_wood(&stack.item) else {
|
||||
let Some(boat) = self.boat_to_place(stack) else {
|
||||
return false;
|
||||
};
|
||||
if !self.boat_models.has(wood, chest) {
|
||||
return false;
|
||||
self.next_entity += 1;
|
||||
let key = (
|
||||
boat.position.x.floor() as i32 >> 4,
|
||||
boat.position.z.floor() as i32 >> 4,
|
||||
);
|
||||
self.world.modified.insert(key);
|
||||
self.boats.push(boat);
|
||||
if !self.creative {
|
||||
self.consume_held();
|
||||
}
|
||||
let Some((eye, dir)) = self.aim() else {
|
||||
return false;
|
||||
};
|
||||
self.swing_arm();
|
||||
true
|
||||
}
|
||||
/// The boat `BoatItem.use` would place for `stack` (its SUCCESS), or
|
||||
/// None when it aims at nothing (PASS) or the spot is taken (FAIL).
|
||||
pub(super) fn boat_to_place(&self, stack: &ItemStack) -> Option<Boat> {
|
||||
let (wood, chest) = any_boat_wood(&stack.item)?;
|
||||
if !self.boat_models.has(wood, chest) {
|
||||
return None;
|
||||
}
|
||||
let (eye, dir) = self.aim()?;
|
||||
let reach = self.reach();
|
||||
// ClipContext.Fluid.ANY: the first fluid surface (water or lava) or
|
||||
// block along the ray.
|
||||
@@ -706,7 +721,7 @@ impl Game<'_> {
|
||||
}
|
||||
(Some(b), None) => b.0,
|
||||
(None, Some(w)) => w.0,
|
||||
(None, None) => return false,
|
||||
(None, None) => return None,
|
||||
};
|
||||
let mut boat = Boat::new(
|
||||
self.next_entity + 1,
|
||||
@@ -730,17 +745,9 @@ impl Game<'_> {
|
||||
subject: None,
|
||||
};
|
||||
if !scene.unobstructed(inner).unwrap_or(false) || self.boat_entity_collision(bb) {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
self.next_entity += 1;
|
||||
let key = (hit[0].floor() as i32 >> 4, hit[2].floor() as i32 >> 4);
|
||||
self.world.modified.insert(key);
|
||||
self.boats.push(boat);
|
||||
if !self.creative {
|
||||
self.consume_held();
|
||||
}
|
||||
self.swing_arm();
|
||||
true
|
||||
Some(boat)
|
||||
}
|
||||
pub(super) fn boat_hit(&self) -> Option<(usize, f64)> {
|
||||
let (eye, dir) = self.aim()?;
|
||||
|
||||
@@ -11,6 +11,7 @@ use super::{Dim, Game, Mining, Screen};
|
||||
use horizon_client::first_person::{Hand, SwingAnimation};
|
||||
use horizon_client::{
|
||||
Result,
|
||||
blocks::Fluid,
|
||||
chunk::Chunk,
|
||||
containers::{BlockEntity, Furnace, FurnaceKind},
|
||||
entities::{Experience, ExperienceOrb, ItemEntity},
|
||||
@@ -5049,6 +5050,12 @@ impl Game<'_> {
|
||||
if info.food.is_some() && !eating {
|
||||
return false;
|
||||
}
|
||||
// A bucket, a glass bottle or a boat with nothing to fill, empty or
|
||||
// place PASSes or FAILs: startUseItem goes on to the off hand (a
|
||||
// Riptide trident there charges).
|
||||
if self.with_hand(hand, |g| g.use_finds_no_target(&held)) {
|
||||
return false;
|
||||
}
|
||||
// It runs the use here too: ItemStack.use starts the `use_cooldown`
|
||||
// of an item with no use time, and the server's
|
||||
// ClientboundCooldownPacket confirms it; one held in use starts it at
|
||||
@@ -5063,6 +5070,61 @@ impl Game<'_> {
|
||||
item_has_use(&info)
|
||||
}
|
||||
|
||||
/// Whether `held`'s `Item.use` needs a target this client sees none of
|
||||
/// (not SUCCESS): BucketItem.use (a fluid source or powder snow to pick
|
||||
/// up through `ClipContext.Fluid.SOURCE_ONLY`, a block to empty a full
|
||||
/// one against, `mayUseItemAt`), BottleItem.use (water to fill from) and
|
||||
/// BoatItem.use (a block or fluid surface with room for the boat).
|
||||
fn use_finds_no_target(&self, held: &ItemStack) -> bool {
|
||||
let id = &*held.item;
|
||||
let facts = |pos: [i32; 3]| {
|
||||
self.res
|
||||
.facts()
|
||||
.get(self.world.block(pos[0], pos[1], pos[2]))
|
||||
};
|
||||
let source = |want: Option<Fluid>| {
|
||||
let fluid_hit = self.pick_fluid();
|
||||
// pick_fluid steps through waterlogged blocks, but their water is
|
||||
// a source the SOURCE_ONLY ray stops at (and a BucketPickup).
|
||||
if let Some(hit) = self.pick()
|
||||
&& facts(hit.pos).waterlogged
|
||||
{
|
||||
let eye = self.aim().map_or(hit.point, |(eye, _)| eye);
|
||||
let d = |p: [f64; 3]| (0..3).map(|a| (p[a] - eye[a]).powi(2)).sum::<f64>();
|
||||
if fluid_hit
|
||||
.as_ref()
|
||||
.is_none_or(|fluid| d(fluid.point) >= d(hit.point))
|
||||
{
|
||||
return want.is_none_or(|want| want == Fluid::Water);
|
||||
}
|
||||
}
|
||||
fluid_hit.is_some_and(|hit| {
|
||||
let f = facts(hit.pos);
|
||||
let fluid = f.fluid != Fluid::None && f.fluid_level == 8 && !f.fluid_falling;
|
||||
match want {
|
||||
Some(want) => fluid && f.fluid == want,
|
||||
None => fluid || self.is_powder_snow(hit.pos),
|
||||
}
|
||||
})
|
||||
};
|
||||
// SolidBucketItem only has useOn: its use is Item.use's PASS.
|
||||
if id == "minecraft:powder_snow_bucket" {
|
||||
return true;
|
||||
}
|
||||
if id == "minecraft:bucket" {
|
||||
return !self.may_build() || !source(None);
|
||||
}
|
||||
if horizon_client::items::bucket_contents(id).is_some() {
|
||||
return !self.may_build() || self.pick().is_none();
|
||||
}
|
||||
if id == "minecraft:glass_bottle" {
|
||||
return !source(Some(Fluid::Water));
|
||||
}
|
||||
if super::boats::any_boat_wood(id).is_some() {
|
||||
return self.boat_to_place(held).is_none();
|
||||
}
|
||||
false
|
||||
}
|
||||
/// `Item.use` runs on the client too (MultiPlayerGameMode.useItem): an
|
||||
/// item held in use until the button lets go starts being used here, so
|
||||
/// that letting go sends RELEASE_USE_ITEM (Minecraft.handleKeybinds) and
|
||||
|
||||
Reference in new issue
Block a user