mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Merge pull request #11051 from Delete228/native-dropped-item-contact
Native mods: expose dropped-item block contact
This commit is contained in:
8 files changed
+397
-98
No files matched your search
@@ -369,7 +369,8 @@ impl crate::entity_rules::EntityRules for ArmorStand {
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct ItemStep {
|
||||
old_velocity: Vec3,
|
||||
moved: bool,
|
||||
movement: Option<Vec3>,
|
||||
clipped_vertical: bool,
|
||||
}
|
||||
#[cfg(test)]
|
||||
impl ItemEntity {
|
||||
@@ -585,10 +586,35 @@ impl ItemEntity {
|
||||
facts: &BlockFacts,
|
||||
collision: &BlockCollisionRegistry<'_>,
|
||||
no_physics: bool,
|
||||
(water_height, lava_height): (f64, f64),
|
||||
heights: (f64, f64),
|
||||
entity_boxes: &[Aabb],
|
||||
border: Option<&BorderGeometry>,
|
||||
blocks: Option<&horizon_world::movement_properties::BoundMovementProperties>,
|
||||
) -> ItemStep {
|
||||
let step = self.tick_move_before_block_effects(
|
||||
world,
|
||||
facts,
|
||||
collision,
|
||||
no_physics,
|
||||
heights,
|
||||
entity_boxes,
|
||||
border,
|
||||
);
|
||||
self.finish_move_block_effects(world, facts, no_physics, blocks, step);
|
||||
step
|
||||
}
|
||||
|
||||
/// Resolve the item's move before the owner handles block contact and
|
||||
/// resumes the normal block effects.
|
||||
pub fn tick_move_before_block_effects(
|
||||
&mut self,
|
||||
world: &World,
|
||||
facts: &BlockFacts,
|
||||
collision: &BlockCollisionRegistry<'_>,
|
||||
no_physics: bool,
|
||||
(water_height, lava_height): (f64, f64),
|
||||
entity_boxes: &[Aabb],
|
||||
border: Option<&BorderGeometry>,
|
||||
) -> ItemStep {
|
||||
self.ticks += 1;
|
||||
self.previous = self.position;
|
||||
@@ -686,22 +712,10 @@ impl ItemEntity {
|
||||
slime,
|
||||
);
|
||||
}
|
||||
// Entity.move's last step: soul sand and honey slow the item's
|
||||
// horizontal speed. A move through blocks (noPhysics) returns
|
||||
// before it. `blocks` is None only for the isolated unit tests.
|
||||
if !no_physics && let Some(blocks) = blocks {
|
||||
let factor =
|
||||
crate::motion::block_speed_factor_at(world, blocks, self.position, None);
|
||||
self.velocity =
|
||||
horizon_world::entity_move::scale_by_block_speed(self.velocity, factor);
|
||||
}
|
||||
// Entity.move -> applyEffectsFromBlocks: honey then bubble columns
|
||||
// (before air drag), matching ItemEntity.tick.
|
||||
self.apply_honey_slide(world, facts);
|
||||
self.stuck = self.stuck_multiplier(world, facts).unwrap_or(Vec3::ZERO);
|
||||
return ItemStep {
|
||||
old_velocity,
|
||||
moved: true,
|
||||
movement: Some(moved),
|
||||
clipped_vertical: !no_physics && moved.y != wanted.y,
|
||||
};
|
||||
}
|
||||
// applyEffectsFromBlocksForLastMovements: still lift/drag while idle.
|
||||
@@ -710,7 +724,67 @@ impl ItemEntity {
|
||||
}
|
||||
ItemStep {
|
||||
old_velocity,
|
||||
moved: false,
|
||||
movement: None,
|
||||
clipped_vertical: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The block at `Entity.getOnPosLegacy` for this tick's clipped move.
|
||||
/// Idle ticks retain the entity's collision flags but have no contact.
|
||||
pub fn vertical_contact(
|
||||
&self,
|
||||
world: &World,
|
||||
collision: &BlockCollisionRegistry<'_>,
|
||||
blocks: &horizon_world::movement_properties::BoundMovementProperties,
|
||||
step: ItemStep,
|
||||
) -> Option<BlockPos> {
|
||||
if !step.clipped_vertical {
|
||||
return None;
|
||||
}
|
||||
let mut scene = SceneCollisions {
|
||||
world,
|
||||
binding: collision,
|
||||
subject: None,
|
||||
};
|
||||
let mut ground = horizon_world::ground_support::GroundState::default();
|
||||
ground
|
||||
.check_support(self.on_ground, self.bounds(), step.movement, |bounds| {
|
||||
horizon_world::ground_support::find_supporting_block(
|
||||
&mut scene,
|
||||
horizon_world::collision_context::CollisionContext::empty(false),
|
||||
self.position,
|
||||
bounds,
|
||||
1_000_000,
|
||||
)
|
||||
})
|
||||
.ok()?;
|
||||
Some(crate::motion::legacy_block_support_at(
|
||||
world,
|
||||
blocks,
|
||||
self.position,
|
||||
ground.supporting_block,
|
||||
))
|
||||
}
|
||||
|
||||
/// Resume the normal block effects after the collision response.
|
||||
pub fn finish_move_block_effects(
|
||||
&mut self,
|
||||
world: &World,
|
||||
facts: &BlockFacts,
|
||||
no_physics: bool,
|
||||
blocks: Option<&horizon_world::movement_properties::BoundMovementProperties>,
|
||||
step: ItemStep,
|
||||
) {
|
||||
if step.movement.is_some() {
|
||||
// Entity.move applies the block's speed factor before block effects.
|
||||
if !no_physics && let Some(blocks) = blocks {
|
||||
let factor =
|
||||
crate::motion::block_speed_factor_at(world, blocks, self.position, None);
|
||||
self.velocity =
|
||||
horizon_world::entity_move::scale_by_block_speed(self.velocity, factor);
|
||||
}
|
||||
self.apply_honey_slide(world, facts);
|
||||
self.stuck = self.stuck_multiplier(world, facts).unwrap_or(Vec3::ZERO);
|
||||
}
|
||||
}
|
||||
/// The rest of `ItemEntity.tick`, after the bubble column's push: slime,
|
||||
@@ -724,7 +798,7 @@ impl ItemEntity {
|
||||
no_physics: bool,
|
||||
step: ItemStep,
|
||||
) -> bool {
|
||||
if step.moved {
|
||||
if step.movement.is_some() {
|
||||
// Entity.applyEffectsFromBlocks -> SlimeBlock.stepOn, with the
|
||||
// vertical speed the bounce just left (before the air drag).
|
||||
if !no_physics {
|
||||
|
||||
@@ -159,6 +159,8 @@ mod integrated;
|
||||
mod interaction_shapes;
|
||||
mod interactions;
|
||||
mod item_areas;
|
||||
#[cfg(test)]
|
||||
mod item_contact_tests;
|
||||
mod item_draw;
|
||||
#[cfg(test)]
|
||||
mod item_interact_mob_tests;
|
||||
@@ -4496,17 +4498,11 @@ impl Game<'_> {
|
||||
);
|
||||
obstacles.near(item.bounds().inflate(reach), &mut nearby);
|
||||
let mut entered = Vec::new();
|
||||
let alive = entity::tick_item(
|
||||
let (alive, discarded) = self.tick_item_with_block_contacts(
|
||||
item,
|
||||
&self.world,
|
||||
self.pack.assets.registry,
|
||||
self.res.facts(),
|
||||
self.collision,
|
||||
&self.res.motion().fluids,
|
||||
no_physics,
|
||||
&nearby,
|
||||
Some(self.border.geometry()),
|
||||
Some(&self.res.motion().blocks),
|
||||
false,
|
||||
&mut |splash| entered.push(splash),
|
||||
);
|
||||
// Entity.doWaterSplashEffect: the server plays the sound (broadcast
|
||||
@@ -4514,6 +4510,11 @@ impl Game<'_> {
|
||||
for splash in entered {
|
||||
self.apply_item_splash(splash);
|
||||
}
|
||||
if discarded {
|
||||
removed[i] = true;
|
||||
cells.remove(i, item.previous);
|
||||
continue;
|
||||
}
|
||||
if merging && !no_physics {
|
||||
self.item_landing_event(item.id, item.previous, item.position, item.on_ground);
|
||||
self.entity_powder_snow_particles(
|
||||
@@ -4608,12 +4609,12 @@ impl Game<'_> {
|
||||
let mut removed = removed.into_iter();
|
||||
items.retain(|_| !removed.next().unwrap_or(false));
|
||||
let Some(p) = self.player.as_ref() else {
|
||||
self.dropped = items;
|
||||
self.restore_item_batch(items);
|
||||
self.spawn_destroyed_contents(destroyed);
|
||||
return;
|
||||
};
|
||||
if p.health.dead_or_dying() || self.spectator() {
|
||||
self.dropped = items;
|
||||
self.restore_item_batch(items);
|
||||
self.spawn_destroyed_contents(destroyed);
|
||||
return;
|
||||
}
|
||||
@@ -4695,7 +4696,7 @@ impl Game<'_> {
|
||||
}
|
||||
kept.push(item);
|
||||
}
|
||||
self.dropped = kept.into();
|
||||
self.restore_item_batch(kept.into());
|
||||
self.spawn_destroyed_contents(destroyed);
|
||||
}
|
||||
/// `ItemUtils.onContainerDestroyed`: each stack a destroyed item held
|
||||
|
||||
@@ -567,42 +567,77 @@ macro_rules! stored_motion {
|
||||
};
|
||||
}
|
||||
|
||||
/// One `ItemEntity.tick`, with the fluid push and the bubble column decided by
|
||||
/// the `Entity` defaults every kind shares: fluids, move, column, settle,
|
||||
/// fluids again. Returns false when the item should be removed.
|
||||
pub(super) fn tick_item(
|
||||
item: &mut ItemEntity,
|
||||
world: &horizon_client::world::World,
|
||||
registry: &horizon_world::registry::BlockRegistry,
|
||||
facts: &horizon_client::blocks::BlockFacts,
|
||||
collision: &horizon_world::block_collision::BlockCollisionRegistry<'_>,
|
||||
fluids: &horizon_client::motion::FluidResources,
|
||||
no_physics: bool,
|
||||
entity_boxes: &[Aabb],
|
||||
border: Option<&horizon_core::geometry::border::BorderGeometry>,
|
||||
blocks: Option<&horizon_world::movement_properties::BoundMovementProperties>,
|
||||
splash: &mut impl FnMut(horizon_client::entities::ItemSplash),
|
||||
) -> bool {
|
||||
let (water, _lava) = item.push_by_fluid(world, fluids);
|
||||
// `Entity.baseTick` splashes before the item moves. The first tick does not.
|
||||
item.water_entry(water > 0.0, item.ticks == 0, splash);
|
||||
let step = item.tick_move(
|
||||
world,
|
||||
facts,
|
||||
collision,
|
||||
no_physics,
|
||||
(water, _lava),
|
||||
entity_boxes,
|
||||
border,
|
||||
blocks,
|
||||
);
|
||||
item.push_by_bubble_column(world, registry, facts, collision);
|
||||
let alive = item.tick_settle(world, facts, no_physics, step);
|
||||
// `ItemEntity.tick` refreshes immersion after moving and splashes if that
|
||||
// move is what entered the water.
|
||||
let (water, _) = item.push_by_fluid(world, fluids);
|
||||
item.water_entry(water > 0.0, false, splash);
|
||||
alive
|
||||
impl Game<'_> {
|
||||
/// Tick an item owned by the caller. The two results are ordinary survival
|
||||
/// and explicit consumption by a block-contact handler.
|
||||
pub(super) fn tick_item_with_block_contacts(
|
||||
&mut self,
|
||||
item: &mut ItemEntity,
|
||||
no_physics: bool,
|
||||
entity_boxes: &[Aabb],
|
||||
client: bool,
|
||||
splash: &mut impl FnMut(horizon_client::entities::ItemSplash),
|
||||
) -> (bool, bool) {
|
||||
let heights = item.push_by_fluid(&self.world, &self.res.motion().fluids);
|
||||
// Entity.baseTick splashes before moving, except on the first tick.
|
||||
item.water_entry(heights.0 > 0.0, item.ticks == 0, splash);
|
||||
let step = item.tick_move_before_block_effects(
|
||||
&self.world,
|
||||
self.res.facts(),
|
||||
self.collision,
|
||||
no_physics,
|
||||
heights,
|
||||
entity_boxes,
|
||||
Some(self.border.geometry()),
|
||||
);
|
||||
if let Some(pos) =
|
||||
item.vertical_contact(&self.world, self.collision, &self.res.motion().blocks, step)
|
||||
{
|
||||
let state = self.world.block(pos.x, pos.y, pos.z);
|
||||
let position = item.position;
|
||||
let mut discarded = false;
|
||||
self.native_item_after_vertical_collision(
|
||||
self.dim.id(),
|
||||
client,
|
||||
item.id,
|
||||
state,
|
||||
[pos.x, pos.y, pos.z],
|
||||
[position.x, position.y, position.z],
|
||||
&mut item.stack,
|
||||
&mut discarded,
|
||||
);
|
||||
if discarded || item.stack.count == 0 {
|
||||
return (false, true);
|
||||
}
|
||||
}
|
||||
item.finish_move_block_effects(
|
||||
&self.world,
|
||||
self.res.facts(),
|
||||
no_physics,
|
||||
Some(&self.res.motion().blocks),
|
||||
step,
|
||||
);
|
||||
item.push_by_bubble_column(
|
||||
&self.world,
|
||||
self.pack.assets.registry,
|
||||
self.res.facts(),
|
||||
self.collision,
|
||||
);
|
||||
let alive = item.tick_settle(&self.world, self.res.facts(), no_physics, step);
|
||||
// ItemEntity.tick refreshes immersion and splashes on water entry.
|
||||
let (water, _) = item.push_by_fluid(&self.world, &self.res.motion().fluids);
|
||||
item.water_entry(water > 0.0, false, splash);
|
||||
(alive, false)
|
||||
}
|
||||
|
||||
/// Keep new joins after the active batch, so they tick next time.
|
||||
pub(super) fn restore_item_batch(
|
||||
&mut self,
|
||||
mut items: horizon_client::join_seq::JoinList<ItemEntity>,
|
||||
) {
|
||||
items.extend(std::mem::take(&mut self.dropped));
|
||||
self.dropped = items;
|
||||
}
|
||||
}
|
||||
|
||||
/// The game's lists of entities, in the order systems walk them. Hands them to
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
use super::*;
|
||||
use crate::mod_api::{self, Flow, Hooks, Host, ItemAfterVerticalCollisionEvent};
|
||||
use horizon_client::{chunk::Chunk, join_seq::Joined as _};
|
||||
use std::sync::Mutex;
|
||||
|
||||
type Contact = (bool, u32, [i32; 3], [f64; 3], u32);
|
||||
|
||||
#[derive(Default)]
|
||||
struct Probe {
|
||||
contacts: Mutex<Vec<Contact>>,
|
||||
}
|
||||
|
||||
impl Hooks for Arc<Probe> {
|
||||
fn item_after_vertical_collision(
|
||||
&self,
|
||||
_: &mut dyn Host,
|
||||
event: &mut ItemAfterVerticalCollisionEvent<'_>,
|
||||
) -> Flow {
|
||||
self.contacts.lock().unwrap().push((
|
||||
event.client,
|
||||
event.entity,
|
||||
event.block_pos,
|
||||
event.item_pos,
|
||||
event.stack.count,
|
||||
));
|
||||
if event.stack.count == 1 {
|
||||
return Flow::Cancel;
|
||||
}
|
||||
event.stack.count = event.stack.count.saturating_sub(2);
|
||||
Flow::Continue
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "needs prepared resources in HORIZON_TEST_RESOURCES and an isolated mod registry"]
|
||||
fn the_active_stack_survives_a_partial_contact_and_consumption_removes_the_entity() {
|
||||
let root = PathBuf::from(std::env::var_os("HORIZON_TEST_RESOURCES").unwrap());
|
||||
let settings = Settings::default();
|
||||
let loaded = load_resources(
|
||||
root.clone(),
|
||||
None,
|
||||
&root,
|
||||
&settings,
|
||||
Instant::now(),
|
||||
false,
|
||||
true,
|
||||
&Progress::new(),
|
||||
&mut |_, _| unreachable!(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut options = Options::parse(std::iter::empty()).unwrap().unwrap();
|
||||
options.resources = root.clone();
|
||||
options.execution_role = execution_role::ExecutionRole::IntegratedServer;
|
||||
let mut game = Game::new(
|
||||
&loaded.resources,
|
||||
loaded.pack,
|
||||
loaded.gui.unwrap(),
|
||||
options,
|
||||
settings,
|
||||
Audio::silent(&root),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
game.options.dedicated = Some(Vec::new());
|
||||
game.world = World::new(game.res.facts().air, 1);
|
||||
game.world
|
||||
.chunks
|
||||
.insert((0, 0), Arc::new(Chunk::empty(0, 0)));
|
||||
let stone = game
|
||||
.pack
|
||||
.assets
|
||||
.registry
|
||||
.default_state("minecraft:stone")
|
||||
.unwrap();
|
||||
game.world.set(4, 63, 4, stone).unwrap();
|
||||
|
||||
// The centre of the item is outside the block, while its box overlaps it.
|
||||
let landing = [3.99, 64.0, 4.5];
|
||||
let mut input = ItemStack::new("minecraft:iron_ingot", 13);
|
||||
input
|
||||
.data_mut()
|
||||
.set_other("fixture:opaque", Some(horizon_world::nbt::Tag::Long(99)));
|
||||
let spawn = |game: &mut Game<'_>, count| {
|
||||
game.dropped.clear();
|
||||
let mut stack = input.clone();
|
||||
stack.count = count;
|
||||
game.spawn_item(stack, [landing[0], 64.1, landing[2]], [0.0, -0.3, 0.0], 20);
|
||||
game.ticked_entities_through = game.next_entity;
|
||||
game.dropped[0].clone()
|
||||
};
|
||||
let before = spawn(&mut game, 13);
|
||||
game.tick_items();
|
||||
let vanilla = game.dropped[0].clone();
|
||||
assert_eq!(vanilla.id, before.id);
|
||||
assert_eq!(
|
||||
vanilla.position,
|
||||
Vec3::new(landing[0], landing[1], landing[2])
|
||||
);
|
||||
assert_eq!(vanilla.velocity, Vec3::ZERO);
|
||||
assert_eq!(vanilla.stack, input);
|
||||
assert_eq!(vanilla.age, 1);
|
||||
|
||||
let probe = Arc::new(Probe::default());
|
||||
assert!(
|
||||
mod_api::MODS
|
||||
.set(Arc::from(vec![Box::new(probe.clone()) as Box<dyn Hooks>]))
|
||||
.is_ok()
|
||||
);
|
||||
let old = spawn(&mut game, 13);
|
||||
game.tick_items();
|
||||
let item = &game.dropped[0];
|
||||
assert_eq!(item.id, old.id);
|
||||
assert_eq!(item.join_seq(), old.join_seq());
|
||||
assert_eq!(item.position, vanilla.position);
|
||||
assert_eq!(item.velocity, vanilla.velocity);
|
||||
let mut expected = input.clone();
|
||||
expected.count = 11;
|
||||
assert_eq!(item.stack, expected);
|
||||
assert_eq!(
|
||||
probe.contacts.lock().unwrap().as_slice(),
|
||||
&[(false, old.id, [4, 63, 4], landing, 13)]
|
||||
);
|
||||
|
||||
for count in [1, 2] {
|
||||
spawn(&mut game, count);
|
||||
game.tick_items();
|
||||
assert!(game.dropped.is_empty(), "consumed stack of {count}");
|
||||
}
|
||||
}
|
||||
@@ -2190,6 +2190,37 @@ fn chars_to_type(text: &str, canceled: impl FnMut(char) -> bool) -> Option<Strin
|
||||
|
||||
/// The hook points. Each does nothing in Horizon.
|
||||
impl Game<'_> {
|
||||
/// A dropped item's vertical move clipped. Collision response has run and
|
||||
/// block effects and pickup are still ahead. The owner lends the active
|
||||
/// stack and supplies the block from `Entity.getOnPosLegacy`.
|
||||
/// `client` identifies prediction. `discard` consumes the current entity
|
||||
/// without loot or pickup. A stack reduced to zero also consumes it.
|
||||
/// @API:item_after_vertical_collision@
|
||||
pub(super) fn native_item_after_vertical_collision(
|
||||
&mut self,
|
||||
dimension: &str,
|
||||
client: bool,
|
||||
entity: u32,
|
||||
state: BlockStateId,
|
||||
block_pos: [i32; 3],
|
||||
item_pos: [f64; 3],
|
||||
stack: &mut ItemStack,
|
||||
discard: &mut bool,
|
||||
) {
|
||||
mod_api::dispatch_item_after_vertical_collision(
|
||||
self,
|
||||
mod_api::mods(),
|
||||
dimension,
|
||||
client,
|
||||
entity,
|
||||
state,
|
||||
block_pos,
|
||||
item_pos,
|
||||
stack,
|
||||
discard,
|
||||
);
|
||||
}
|
||||
|
||||
/// Client initialization, before any outgoing connection; a window need not exist yet.
|
||||
pub(super) fn native_client_start(&mut self) {
|
||||
mod_api::dispatch_client_start(self, mod_api::mods());
|
||||
|
||||
@@ -7220,32 +7220,22 @@ impl Game<'_> {
|
||||
}
|
||||
// ItemEntity.tick runs on a client too: gravity, collisions and
|
||||
// friction move the item between the server's updates (every 20
|
||||
// ticks), and only the server removes it.
|
||||
let (world, registry, facts, collision, border) = (
|
||||
&self.world,
|
||||
self.pack.assets.registry,
|
||||
self.res.facts(),
|
||||
self.collision,
|
||||
Some(self.border.geometry()),
|
||||
);
|
||||
let mut entered = Vec::new();
|
||||
for d in self.dropped.iter_mut().filter(|_| running) {
|
||||
super::entity::tick_item(
|
||||
d,
|
||||
world,
|
||||
registry,
|
||||
facts,
|
||||
collision,
|
||||
&self.res.motion().fluids,
|
||||
false,
|
||||
// The server decides what an item rests on; this copy only moves between updates.
|
||||
&[],
|
||||
border,
|
||||
Some(&self.res.motion().blocks),
|
||||
&mut |splash| entered.push(splash),
|
||||
);
|
||||
}
|
||||
// ticks). Ordinary removal remains server-authoritative. Mods can opt
|
||||
// into predicting consumption at block contact.
|
||||
head_lerps.retain(|_, (_, steps)| *steps > 0);
|
||||
let mut entered = Vec::new();
|
||||
if running {
|
||||
let mut items = std::mem::take(&mut self.dropped);
|
||||
items.retain_mut(|item| {
|
||||
// The server decides entity collisions. This copy predicts block movement.
|
||||
let (_, discarded) =
|
||||
self.tick_item_with_block_contacts(item, false, &[], true, &mut |splash| {
|
||||
entered.push(splash);
|
||||
});
|
||||
!discarded
|
||||
});
|
||||
self.restore_item_batch(items);
|
||||
}
|
||||
// Entity.doWaterSplashEffect on this window: Level.addParticle draws
|
||||
// the bubbles and splashes. ClientLevel.playSeededSound drops
|
||||
// Entity.playSound, so the sound stays the server's.
|
||||
|
||||
@@ -3021,19 +3021,23 @@ fn fly_into_wall_damage(before: f64, after: f64) -> f32 {
|
||||
/// `Entity.getBlockSpeedFactor` for a kind that keeps no motion world of its
|
||||
/// own (mobs, boats): the factor of the block at `position`, else of the one
|
||||
/// below it, through the same port the player's move uses.
|
||||
struct MovementBlocks<'a>(&'a World);
|
||||
impl MovementBlockProvider for MovementBlocks<'_> {
|
||||
type Error = std::convert::Infallible;
|
||||
fn movement_block_state(
|
||||
&mut self,
|
||||
p: BlockPos,
|
||||
) -> std::result::Result<BlockStateId, Self::Error> {
|
||||
Ok(self.0.block(p.x, p.y, p.z))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_speed_factor_at(
|
||||
world: &World,
|
||||
properties: &BoundMovementProperties,
|
||||
position: Vec3,
|
||||
supporting_block: Option<BlockPos>,
|
||||
) -> f32 {
|
||||
struct Blocks<'a>(&'a World);
|
||||
impl MovementBlockProvider for Blocks<'_> {
|
||||
type Error = ();
|
||||
fn movement_block_state(&mut self, p: BlockPos) -> std::result::Result<BlockStateId, ()> {
|
||||
Ok(self.0.block(p.x, p.y, p.z))
|
||||
}
|
||||
}
|
||||
let location = movement_support::MovementLocation {
|
||||
position,
|
||||
block_position: BlockPos::new(
|
||||
@@ -3043,7 +3047,7 @@ pub fn block_speed_factor_at(
|
||||
),
|
||||
supporting_block,
|
||||
};
|
||||
let mut view = Blocks(world);
|
||||
let mut view = MovementBlocks(world);
|
||||
movement_support::block_speed_factor(
|
||||
&mut RegistryMovementWorld {
|
||||
properties,
|
||||
@@ -3054,6 +3058,25 @@ pub fn block_speed_factor_at(
|
||||
.unwrap_or(1.0)
|
||||
}
|
||||
|
||||
/// `Entity.getOnPosLegacy` with the world's bound fence and wall tags.
|
||||
pub(crate) fn legacy_block_support_at(
|
||||
world: &World,
|
||||
properties: &BoundMovementProperties,
|
||||
position: Vec3,
|
||||
supporting_block: Option<BlockPos>,
|
||||
) -> BlockPos {
|
||||
movement_support::support_position(
|
||||
&mut RegistryMovementWorld {
|
||||
properties,
|
||||
world: &mut MovementBlocks(world),
|
||||
},
|
||||
position,
|
||||
supporting_block,
|
||||
movement_support::LEGACY_SUPPORT_OFFSET,
|
||||
)
|
||||
.expect("loaded world block reads are infallible")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
/// The glide's collision damage: a third of a block a tick lost is 0.0 and
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "item_after_vertical_collision",
|
||||
"target": "horizon_client::game::native_hooks::Game<'_>::native_item_after_vertical_collision",
|
||||
"side": "both",
|
||||
"cancellable": true,
|
||||
"mod_api": {
|
||||
"contract": "cancel",
|
||||
"out": "discard"
|
||||
}
|
||||
}
|
||||
],
|
||||
"rust_summary": "dropped-item block contact after a clipped vertical move.",
|
||||
"doc_summary": "`native_item_after_vertical_collision` lends a dropped item's active stack after vertical collision response, before block effects, merging and pickup. The block is the one selected by the legacy on-position query. `client` identifies prediction. Set `discard` (trait `Flow::Cancel`) or reduce the stack to zero to consume the item without loot or pickup. A movement skipped by the normal item tick sends no notification. Bundles need \"hooks\": @API:item_after_vertical_collision@."
|
||||
}
|
||||
Reference in new issue
Block a user