Powder snow: sinking, freezing, leather boots, buckets and falling blocks
Powder snow was inert: the player's collision context never carried its
fall distance or boots, block-inside effects never ran for the player,
frozen ticks stayed 0 and the empty bucket only took fluids.
Player (PlayerMotion, LivingEntity.aiStep order):
- PowderSnowBlock.getCollisionShape gets the player's fall distance and
leather boots: it sinks through, walks on top in leather boots (sinks
when crouching), lands on the 0.9-high shape after a fall of more
than 2.5 blocks.
- Entity.applyEffectsFromBlocks over this tick's movement path (the
ported block sweep, StepBasedCollector steps): entityInside makes it
stuck (0.9, 1.5, 0.9) while its feet are in powder snow and sends the
server's snowflake from the level random; FREEZE sets isInPowderSnow
and adds a frozen tick per step; EXTINGUISH destroys the snow if the
player burns and puts the fire out. ServerPlayer.onInsideBlock fires
enter_block for every block entered (which also unlocks the boat
recipes in water, and the end gateway advancement).
- baseTick's wasInPowderSnow lets leather boots climb out (living_air).
- Server freezing: thawing 2 a tick, the powder_snow frost modifier
(-0.05 x percent frozen, not over air), freeze damage every 40 ticks
when fully frozen; leather armor (#freeze_immune_wearables) and
spectators don't freeze.
- HUD: the powder snow outline overlay and frozen hearts.
Items and blocks:
- BucketItem.use on powder snow: pickupBlock (2001), used statistic,
fill sound, FLUID_PICKUP, createFilledResult, filled_bucket trigger.
- SolidBucketItem: the empty_powder_snow place sound and an empty bucket
back in survival.
- Falling blocks land on powder snow, or end up inside it and break
after a fall of more than 2.5 blocks.
test_native_scene_motion gains powder snow fixtures ("snow_tick": the
oracle runs vanilla's real applyEffectsFromBlocks and freezing) and
prints frozen ticks, the snow flags and the stuck multiplier.
Fixes #62
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,7 @@ fn main() -> Result<()> {
|
||||
let mut scene = collisions::SceneCollisions {
|
||||
world: &world,
|
||||
binding: &binding,
|
||||
subject: None,
|
||||
};
|
||||
fs::write(
|
||||
dir.join("scene.json"),
|
||||
|
||||
@@ -165,6 +165,17 @@ fn snapshot(out: &mut impl Write, player: &PlayerMotion) -> Result<()> {
|
||||
.filter_map(|e| match e {
|
||||
MotionEvent::Fall(e) => Some(e.trace()),
|
||||
MotionEvent::Health(e) => Some(e.trace()),
|
||||
MotionEvent::Snowflake { position, velocity } => Some(format!(
|
||||
"snowflake {:016x},{:016x},{:016x} {:016x},{:016x},{:016x}",
|
||||
position.x.to_bits(),
|
||||
position.y.to_bits(),
|
||||
position.z.to_bits(),
|
||||
velocity.x.to_bits(),
|
||||
velocity.y.to_bits(),
|
||||
velocity.z.to_bits()
|
||||
)),
|
||||
// The oracle marks every getAdvancements call (ENTER_BLOCK.trigger) so.
|
||||
MotionEvent::EnterBlock { .. } => Some("hurt-criterion".into()),
|
||||
MotionEvent::Waypoint { receiver, enabled } => Some(format!(
|
||||
"waypoint {} {enabled}",
|
||||
if *receiver { "receive" } else { "transmit" }
|
||||
@@ -232,6 +243,8 @@ fn main() -> Result<()> {
|
||||
instances.next().expect("unexpected instance allocation")
|
||||
}),
|
||||
)?;
|
||||
// The oracle runs block-inside effects only in "snow_tick".
|
||||
player.block_effects = false;
|
||||
for op in f["ops"].as_array().unwrap() {
|
||||
let mut world = MotionWorld {
|
||||
player: &mut player,
|
||||
@@ -409,6 +422,26 @@ fn main() -> Result<()> {
|
||||
op["jumping"].as_bool().unwrap(),
|
||||
)?;
|
||||
}
|
||||
"snow_tick" => {
|
||||
// The clock, then the player tick: baseTick's powder snow
|
||||
// flags lead PlayerMotion::player_movement_tick.
|
||||
world.player.health.advance_clock();
|
||||
let v = vector(&op["value"]);
|
||||
world.player.block_effects = true;
|
||||
let ticked = world.player_movement_tick(
|
||||
[v.x as f32, v.z as f32],
|
||||
op["jumping"].as_bool().unwrap(),
|
||||
);
|
||||
world.player.block_effects = false;
|
||||
ticked?;
|
||||
}
|
||||
"boots" => {
|
||||
// Leather boots are also freeze-immune wearables.
|
||||
let boots = op["value"].as_bool().unwrap();
|
||||
world.player.leather_boots = boots;
|
||||
world.player.freeze_immune = boots;
|
||||
}
|
||||
"frozen" => world.player.ticks_frozen = op["value"].as_i64().unwrap() as i32,
|
||||
"trigger" => world.player.jump_trigger_time = op["value"].as_i64().unwrap() as i32,
|
||||
"delay" => world.player.no_jump_delay = op["value"].as_i64().unwrap() as i32,
|
||||
"move" => world.move_self(vector(&op["value"]))?,
|
||||
@@ -478,7 +511,18 @@ fn main() -> Result<()> {
|
||||
}
|
||||
let check_block = scene.block(25, 3, 25);
|
||||
snapshot(&mut out, &player)?;
|
||||
writeln!(out, "block {}", check_block.0)?;
|
||||
let stuck = player.stuck_multiplier;
|
||||
writeln!(
|
||||
out,
|
||||
"block {} frozen {} {} {} {:016x},{:016x},{:016x}",
|
||||
check_block.0,
|
||||
player.ticks_frozen,
|
||||
player.in_powder_snow,
|
||||
player.was_in_powder_snow,
|
||||
stuck.x.to_bits(),
|
||||
stuck.y.to_bits(),
|
||||
stuck.z.to_bits()
|
||||
)?;
|
||||
let h = &player.health;
|
||||
let canonical = |v: f32| {
|
||||
if v.is_nan() {
|
||||
|
||||
@@ -312,7 +312,7 @@ fn range_i(v: &Value, x: i64) -> bool {
|
||||
range_f(v, x as f64)
|
||||
}
|
||||
/// A registry id, `#tag`, or list matched against one id.
|
||||
fn id_matches(v: &Value, registry: &str, id: &str, w: &dyn World) -> bool {
|
||||
pub fn id_matches(v: &Value, registry: &str, id: &str, w: &dyn World) -> bool {
|
||||
match v {
|
||||
Value::Null => true,
|
||||
Value::String(s) => match s.strip_prefix('#') {
|
||||
|
||||
@@ -39,9 +39,24 @@ pub fn load<'r>(root: &Path, registry: &'r BlockRegistry) -> Result<BlockCollisi
|
||||
Ok(BlockCollisionRegistry::new(Arc::new(cached))?)
|
||||
}
|
||||
|
||||
/// What powder snow's contextual collision asks of the colliding entity
|
||||
/// (`PowderSnowBlock.getCollisionShape`).
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct PowderSnowSubject {
|
||||
pub fall_distance: f64,
|
||||
/// `FallingBlockEntity`.
|
||||
pub falling_block: bool,
|
||||
/// A living entity with leather boots on its feet.
|
||||
pub leather_boots: bool,
|
||||
/// `EntityTypeTags.POWDER_SNOW_WALKABLE_MOBS`.
|
||||
pub walkable_type: bool,
|
||||
}
|
||||
|
||||
pub struct SceneCollisions<'w, 'r> {
|
||||
pub world: &'w World,
|
||||
pub binding: &'w BlockCollisionRegistry<'r>,
|
||||
/// The entity colliding, for powder snow; None: no fall, no boots.
|
||||
pub subject: Option<PowderSnowSubject>,
|
||||
}
|
||||
impl SceneCollisions<'_, '_> {
|
||||
/// Position contexts permit geometric movement probes without inventing a
|
||||
@@ -169,18 +184,18 @@ impl LiquidCollisionWorld for SceneCollisions<'_, '_> {
|
||||
}
|
||||
impl PowderSnowWorld for SceneCollisions<'_, '_> {
|
||||
type Error = io::Error;
|
||||
// Entities here are never falling blocks and wear no leather boots.
|
||||
fn powder_snow_entity(&mut self, _: EntityIdentity) -> io::Result<PowderSnowEntity> {
|
||||
let subject = self.subject.unwrap_or_default();
|
||||
Ok(PowderSnowEntity {
|
||||
fall_distance: 0.0,
|
||||
falling_block: false,
|
||||
fall_distance: subject.fall_distance,
|
||||
falling_block: subject.falling_block,
|
||||
})
|
||||
}
|
||||
fn powder_snow_walkable_tag(&mut self, _: EntityIdentity) -> io::Result<bool> {
|
||||
Ok(false)
|
||||
Ok(self.subject.is_some_and(|s| s.walkable_type))
|
||||
}
|
||||
fn feet_are_leather_boots(&mut self, _: EntityIdentity) -> io::Result<bool> {
|
||||
Ok(false)
|
||||
Ok(self.subject.is_some_and(|s| s.leather_boots))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ impl ItemEntity {
|
||||
let mut scene = SceneCollisions {
|
||||
world,
|
||||
binding: collision,
|
||||
subject: None,
|
||||
};
|
||||
let wanted = self.velocity;
|
||||
let moved = scene
|
||||
@@ -268,6 +269,7 @@ impl ExperienceOrb {
|
||||
let mut scene = SceneCollisions {
|
||||
world,
|
||||
binding: collision,
|
||||
subject: None,
|
||||
};
|
||||
let wanted = self.velocity;
|
||||
let moved = scene
|
||||
|
||||
@@ -74,6 +74,7 @@ mod perspective;
|
||||
mod piglins;
|
||||
mod postprocess;
|
||||
mod potions;
|
||||
mod powder_snow;
|
||||
mod raids;
|
||||
mod recipe_book;
|
||||
mod remote;
|
||||
@@ -534,6 +535,8 @@ struct FallingBlock {
|
||||
no_gravity: bool,
|
||||
/// `FallingBlockEntity.dropItem`.
|
||||
drop_item: bool,
|
||||
/// `Entity.fallDistance` (powder snow holds a block that fell far at 0.9).
|
||||
fall_distance: f64,
|
||||
/// The UUID and compound it was loaded with, kept while it moves.
|
||||
saved: Option<Box<([i32; 4], horizon_world::nbt::Tag)>>,
|
||||
}
|
||||
@@ -1621,6 +1624,7 @@ impl Game<'_> {
|
||||
age: 0,
|
||||
no_gravity: false,
|
||||
drop_item: true,
|
||||
fall_distance: 0.0,
|
||||
saved: None,
|
||||
});
|
||||
}
|
||||
@@ -1832,7 +1836,17 @@ impl Game<'_> {
|
||||
next.y.floor() as i32,
|
||||
next.z.floor() as i32,
|
||||
);
|
||||
let solid = self.res.facts.get(below).full_collision && f.velocity.y < 0.0;
|
||||
let below_facts = self.res.facts.get(below);
|
||||
// PowderSnowBlock.getCollisionShape for a FallingBlockEntity: the
|
||||
// whole block, or 0.9 high after a fall of more than 2.5 blocks,
|
||||
// where the block it rests in is the snow itself.
|
||||
let powder_snow = &*below_facts.name == "minecraft:powder_snow";
|
||||
let sunk = powder_snow
|
||||
&& f.fall_distance > 2.5
|
||||
&& next.y - next.y.floor() < f64::from(0.9_f32);
|
||||
let solid =
|
||||
(below_facts.full_collision || powder_snow && f.fall_distance <= 2.5 || sunk)
|
||||
&& f.velocity.y < 0.0;
|
||||
// FallingBlockEntity.tick: off the ground, it gives up after 600
|
||||
// ticks (100 outside the world) and drops itself as an item.
|
||||
let y = next.y.floor() as i32;
|
||||
@@ -1849,12 +1863,15 @@ impl Game<'_> {
|
||||
f.state,
|
||||
[
|
||||
next.x.floor() as i32,
|
||||
next.y.floor() as i32 + 1,
|
||||
next.y.floor() as i32 + i32::from(!sunk),
|
||||
next.z.floor() as i32,
|
||||
],
|
||||
));
|
||||
return false;
|
||||
}
|
||||
if next.y < f.position.y {
|
||||
f.fall_distance += f.position.y - next.y;
|
||||
}
|
||||
f.position = next;
|
||||
f.velocity = Vec3::new(
|
||||
f.velocity.x * 0.98,
|
||||
@@ -2198,8 +2215,15 @@ impl Game<'_> {
|
||||
f64::from(target[1]) + 0.5,
|
||||
f64::from(target[2]) + 0.5,
|
||||
];
|
||||
// SolidBucketItem.getPlaceSound: the bucket's own empty sound.
|
||||
let solid_bucket = &*stack.item == "minecraft:powder_snow_bucket";
|
||||
let place_sound = if solid_bucket {
|
||||
"minecraft:item.bucket.empty_powder_snow"
|
||||
} else {
|
||||
&sounds.place
|
||||
};
|
||||
self.audio.play(
|
||||
&sounds.place,
|
||||
place_sound,
|
||||
Some(center),
|
||||
(sounds.volume + 1.0) / 2.0,
|
||||
sounds.pitch * 0.8,
|
||||
@@ -2211,6 +2235,10 @@ impl Game<'_> {
|
||||
self.check_wither_spawn(target)?;
|
||||
self.check_golem_spawn(target)?;
|
||||
self.consume_held();
|
||||
// SolidBucketItem.useOn: BucketItem.getEmptySuccessItem in hand.
|
||||
if solid_bucket && !self.creative {
|
||||
self.inventory[self.selected] = Some(ItemStack::new("minecraft:bucket", 1));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn consume_held(&mut self) {
|
||||
@@ -2699,6 +2727,20 @@ impl Game<'_> {
|
||||
})
|
||||
.and_then(|s| self.res.items.get(&s.item))
|
||||
.map(|i| i.use_speed);
|
||||
// PowderSnowBlock.canEntityWalkOnPowderSnow and LivingEntity.canFreeze.
|
||||
player.leather_boots = self.armor[3]
|
||||
.as_ref()
|
||||
.is_some_and(|s| &*s.item == "minecraft:leather_boots");
|
||||
let immune = self
|
||||
.res
|
||||
.items
|
||||
.item_tags
|
||||
.get("minecraft:freeze_immune_wearables");
|
||||
player.freeze_immune = self
|
||||
.armor
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|s| immune.is_some_and(|t| t.contains(&*s.item)));
|
||||
let grounded = player.ground.on_ground;
|
||||
let before = (
|
||||
grounded,
|
||||
@@ -2782,9 +2824,12 @@ impl Game<'_> {
|
||||
self.audio
|
||||
.play("minecraft:entity.player.hurt", Some(feet), 1.0, 1.0);
|
||||
}
|
||||
MotionEvent::Snowflake { position, velocity } => self.snowflake(position, velocity),
|
||||
MotionEvent::EnterBlock { state } => self.trigger_enter_block(state),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.powder_snow_fire()?;
|
||||
self.apply_totem();
|
||||
let Some(player) = self.player.as_mut() else {
|
||||
return Ok(());
|
||||
@@ -3144,6 +3189,8 @@ impl Game<'_> {
|
||||
hud_layout::HeartKind::Poisoned
|
||||
} else if effects.get("wither").is_some() {
|
||||
hud_layout::HeartKind::Withered
|
||||
} else if p.is_fully_frozen() {
|
||||
hud_layout::HeartKind::Frozen
|
||||
} else {
|
||||
hud_layout::HeartKind::Normal
|
||||
};
|
||||
@@ -6318,6 +6365,23 @@ impl Game<'_> {
|
||||
return Ok(true);
|
||||
}
|
||||
if item == "bucket" {
|
||||
// getPlayerPOVHitResult(SOURCE_ONLY): powder snow's outline before
|
||||
// a fluid source behind it.
|
||||
if let Some(hit) = self.pick()
|
||||
&& self.is_powder_snow(hit.pos)
|
||||
{
|
||||
let eye = self.eye_position().unwrap_or(hit.point);
|
||||
let d = |p: [f64; 3]| {
|
||||
(p[0] - eye[0]).powi(2) + (p[1] - eye[1]).powi(2) + (p[2] - eye[2]).powi(2)
|
||||
};
|
||||
if self
|
||||
.pick_fluid()
|
||||
.is_none_or(|fluid| d(fluid.point) >= d(hit.point))
|
||||
{
|
||||
self.pick_up_powder_snow(hit.pos)?;
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
let Some(hit) = self.pick_fluid() else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
@@ -581,6 +581,28 @@ impl Game<'_> {
|
||||
adv::conditions(&c["player"], &pctx, w) && adv::item(&c["item"], &it, w)
|
||||
});
|
||||
}
|
||||
/// `EnterBlockTrigger`: the block, its state properties and the player.
|
||||
pub(super) fn trigger_enter_block(&mut self, state: horizon_world::registry::BlockStateId) {
|
||||
let Some(definition) = self.res.assets.registry.state(state).cloned() else {
|
||||
return;
|
||||
};
|
||||
let me = self.player_entity();
|
||||
self.trigger("minecraft:enter_block", move |c, w| {
|
||||
let block_ok = adv::id_matches(&c["blocks"], "block", &definition.name, w);
|
||||
let state_ok = c["state"]
|
||||
.as_object()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.all(|(k, v)| definition.properties.get(k).map(String::as_str) == v.as_str());
|
||||
let pctx = adv::Entities {
|
||||
this: Some(&me),
|
||||
origin: me.position,
|
||||
tool: None,
|
||||
block_pos: None,
|
||||
};
|
||||
block_ok && state_ok && adv::conditions(&c["player"], &pctx, w)
|
||||
});
|
||||
}
|
||||
/// `ItemUsedOnLocationTrigger` (placed_block, item_used_on_block).
|
||||
pub(super) fn trigger_block(&mut self, name: &str, pos: [i32; 3], stack: &ItemStack) {
|
||||
let me = self.player_entity();
|
||||
|
||||
@@ -159,6 +159,7 @@ impl Game<'_> {
|
||||
let mut scene = horizon_client::collisions::SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
if !scene.unobstructed(bounds).unwrap_or(false)
|
||||
|| self
|
||||
@@ -362,6 +363,7 @@ impl Game<'_> {
|
||||
let mut scene = horizon_client::collisions::SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
for a in &mut self.armor_stands {
|
||||
a.tick(&mut scene);
|
||||
|
||||
@@ -142,6 +142,7 @@ impl Game<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
no_collision_empty(
|
||||
&mut scene,
|
||||
|
||||
@@ -318,6 +318,7 @@ impl Game<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
if b.old_status == Status::InAir && b.status != Status::InAir && b.status != Status::OnLand
|
||||
{
|
||||
@@ -583,6 +584,7 @@ impl Game<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
if !scene.unobstructed(inner).unwrap_or(false) {
|
||||
return false;
|
||||
|
||||
@@ -47,6 +47,7 @@ impl ProjectileWorld for ProjWorld<'_, '_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: self.w.world,
|
||||
binding: self.w.collision,
|
||||
subject: None,
|
||||
};
|
||||
let r = traverse_blocks(from, to, 256, |p: BlockPos| {
|
||||
let state = self.w.world.block(p.x, p.y, p.z);
|
||||
|
||||
@@ -722,6 +722,7 @@ impl Game<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let mut amount = -1.0;
|
||||
// Level.getCollisions(null, aabb): CollisionContext.empty().
|
||||
|
||||
@@ -128,6 +128,7 @@ impl Game<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let h = 0.125;
|
||||
let bounds = Aabb::new(
|
||||
|
||||
@@ -444,6 +444,7 @@ impl Game<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
if let Ok(moved) = scene.resolve(bounds(h.position), h.velocity, 0.0, h.on_ground) {
|
||||
let horizontal =
|
||||
|
||||
@@ -104,6 +104,7 @@ impl Game<'_> {
|
||||
let mut scene = horizon_client::collisions::SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
if !scene.unobstructed(bounds).unwrap_or(false) {
|
||||
return false;
|
||||
|
||||
@@ -304,6 +304,7 @@ impl Game<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
if let Ok(moved) = scene.resolve(c.bounds(), delta, 0.0, c.on_ground) {
|
||||
c.position = Vec3::new(
|
||||
|
||||
@@ -163,6 +163,7 @@ impl NaturalLevel for GameLevel<'_, '_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.game.world,
|
||||
binding: self.w.collision,
|
||||
subject: None,
|
||||
};
|
||||
no_collision_empty(&mut scene, bounds)
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ impl Game<'_> {
|
||||
let mut scene = horizon_client::collisions::SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
if !scene.unobstructed(bounds).unwrap_or(false) {
|
||||
return false;
|
||||
|
||||
@@ -204,6 +204,7 @@ impl Game<'_> {
|
||||
age,
|
||||
no_gravity,
|
||||
drop_item,
|
||||
fall_distance: 0.0,
|
||||
saved: uuid.map(|u| Box::new((u, tag.clone()))),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -259,6 +259,7 @@ impl Game<'_> {
|
||||
scene: SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
},
|
||||
facts: &self.res.facts,
|
||||
context,
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
//! Powder snow around the player: its snowflakes, fire put out by it, and the
|
||||
//! bucket that picks it up (`BucketItem.use` on a `BucketPickup` block). The
|
||||
//! sinking, slowing, climbing and freezing are `PlayerMotion`'s
|
||||
//! (`crate::motion`); placing it is the powder snow bucket's `BlockItem.place`.
|
||||
use super::Game;
|
||||
use horizon_client::{
|
||||
Result,
|
||||
items::ItemStack,
|
||||
mobs::Who,
|
||||
particles::{ParticleOptions, ParticleType},
|
||||
};
|
||||
use horizon_core::{geometry::Vec3, position::BlockPos, random::RandomSource};
|
||||
|
||||
impl Game<'_> {
|
||||
/// `ClientPacketListener.handleParticleEvent` for the one ALTERNATIVE
|
||||
/// snowflake `PowderSnowBlock.entityInside` sends: no spread, its speed.
|
||||
pub(super) fn snowflake(&mut self, position: Vec3, velocity: Vec3) {
|
||||
let r = self.particles.random();
|
||||
// nextDouble() * dist for each axis, the distances being 0.
|
||||
let offset = [
|
||||
r.next_double() * 0.0,
|
||||
r.next_double() * 0.0,
|
||||
r.next_double() * 0.0,
|
||||
];
|
||||
let options = ParticleOptions::simple(ParticleType::Snowflake);
|
||||
self.particles.add(
|
||||
&options,
|
||||
[
|
||||
position.x + offset[0],
|
||||
position.y + offset[1],
|
||||
position.z + offset[2],
|
||||
],
|
||||
[velocity.x, velocity.y, velocity.z],
|
||||
);
|
||||
}
|
||||
/// The EXTINGUISH effect of the powder snow the player went into this
|
||||
/// tick: burning, it destroys those blocks (a player always may), then
|
||||
/// its fire goes out (`Entity.clearFire`) with the extinguish sound.
|
||||
pub(super) fn powder_snow_fire(&mut self) -> Result<()> {
|
||||
let Some(player) = self.player.as_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
let entered = std::mem::take(&mut player.powder_snow_entered);
|
||||
let at = [player.position.x, player.position.y, player.position.z];
|
||||
if entered.is_empty() || self.creatures.player_fire <= 0 {
|
||||
return Ok(());
|
||||
}
|
||||
for pos in entered {
|
||||
self.with_level(|b, level, ticks| b.destroy_block(level, ticks, pos, false));
|
||||
self.world.modified.insert((pos.x >> 4, pos.z >> 4));
|
||||
}
|
||||
self.process_level_events()?;
|
||||
self.creatures.player_fire = 0;
|
||||
// Entity.playEntityOnFireExtinguishedSound
|
||||
let r = self.particles.random();
|
||||
let pitch = 1.6 + (r.next_float() - r.next_float()) * 0.4;
|
||||
self.audio.play(
|
||||
"minecraft:entity.generic.extinguish_fire",
|
||||
Some(at),
|
||||
0.7,
|
||||
pitch,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
/// Whether the block at `pos` is powder snow.
|
||||
pub(super) fn is_powder_snow(&self, pos: [i32; 3]) -> bool {
|
||||
&*self
|
||||
.res
|
||||
.facts
|
||||
.get(self.world.block(pos[0], pos[1], pos[2]))
|
||||
.name
|
||||
== "minecraft:powder_snow"
|
||||
}
|
||||
/// `BucketItem.use` with an empty bucket on powder snow:
|
||||
/// `PowderSnowBlock.pickupBlock` (air, level event 2001), the used
|
||||
/// statistic, the fill sound, FLUID_PICKUP, `ItemUtils.createFilledResult`
|
||||
/// and the filled_bucket trigger.
|
||||
pub(super) fn pick_up_powder_snow(&mut self, pos: [i32; 3]) -> Result<()> {
|
||||
let at = BlockPos::new(pos[0], pos[1], pos[2]);
|
||||
let picked = self.with_level(|b, level, ticks| b.bucket_pickup(level, ticks, at));
|
||||
self.process_level_events()?;
|
||||
let Some(filled) = picked else {
|
||||
return Ok(());
|
||||
};
|
||||
self.world.modified.insert((pos[0] >> 4, pos[2] >> 4));
|
||||
self.stats.add("used", "minecraft:bucket", 1);
|
||||
if let Some(eye) = self
|
||||
.player
|
||||
.as_ref()
|
||||
.map(|p| [p.position.x, p.position.y, p.position.z])
|
||||
{
|
||||
self.audio.play(
|
||||
"minecraft:item.bucket.fill_powder_snow",
|
||||
Some(eye),
|
||||
1.0,
|
||||
1.0,
|
||||
);
|
||||
}
|
||||
self.post_block_game_event("minecraft:fluid_pickup", pos, Some(Who::Player), None);
|
||||
let filled = ItemStack::new(filled, 1);
|
||||
self.filled_result(filled.clone());
|
||||
self.trigger_item("minecraft:filled_bucket", &filled);
|
||||
self.swing_arm();
|
||||
Ok(())
|
||||
}
|
||||
/// `ItemUtils.createFilledResult(held, player, filled)` for the held stack.
|
||||
fn filled_result(&mut self, filled: ItemStack) {
|
||||
if self.creative {
|
||||
// Kept, and the filled one added unless the inventory has it.
|
||||
let has = self
|
||||
.inventory
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|s| s.same_item_same_components(&filled));
|
||||
if !has {
|
||||
let _ = self.give(filled);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let held = &mut self.inventory[self.selected];
|
||||
let last = held.as_ref().is_none_or(|s| s.count <= 1);
|
||||
if last {
|
||||
*held = Some(filled);
|
||||
return;
|
||||
}
|
||||
if let Some(s) = held.as_mut() {
|
||||
s.count -= 1;
|
||||
}
|
||||
if let Some(rest) = self.give(filled) {
|
||||
self.player_drop(rest);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -161,8 +161,8 @@ impl Game<'_> {
|
||||
}
|
||||
_ => Vec::new(),
|
||||
},
|
||||
ticks_frozen: 0,
|
||||
percent_frozen: 0.0,
|
||||
ticks_frozen: p.ticks_frozen,
|
||||
percent_frozen: p.percent_frozen(),
|
||||
partial,
|
||||
portal: [fx.o_portal, fx.portal],
|
||||
nausea: 0.0,
|
||||
|
||||
@@ -88,6 +88,7 @@ impl Game<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let wanted = t.velocity;
|
||||
let moved = scene
|
||||
|
||||
@@ -274,6 +274,7 @@ impl Game<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: &self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
if let Ok(moved) = scene.resolve(bounds(t.position), t.velocity, 0.0, false) {
|
||||
let blocked = (moved.x - t.velocity.x).abs() > 1e-7
|
||||
|
||||
@@ -79,6 +79,8 @@ impl HealthResources {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum HealthEvent {
|
||||
StarvationDamage(f32),
|
||||
/// `LivingEntity.aiStep` hurting a fully frozen entity.
|
||||
FreezeDamage(f32),
|
||||
Updated {
|
||||
id: u8,
|
||||
value: f32,
|
||||
@@ -103,6 +105,7 @@ impl HealthEvent {
|
||||
pub fn trace(self) -> String {
|
||||
match self {
|
||||
Self::StarvationDamage(amount) => format!("damage starve {:08x}", amount.to_bits()),
|
||||
Self::FreezeDamage(amount) => format!("damage freeze {:08x}", amount.to_bits()),
|
||||
Self::Updated { id, value, dirty } => {
|
||||
format!("updated {id} {:08x} {dirty}", value.to_bits())
|
||||
}
|
||||
|
||||
@@ -295,6 +295,7 @@ impl BlockPushWorld for MotionWorld<'_, '_, '_> {
|
||||
let mut world = SceneCollisions {
|
||||
world: self.scene,
|
||||
binding: self.collision_binding,
|
||||
subject: None,
|
||||
};
|
||||
self.collision_binding
|
||||
.collision_shape(&mut world, state, [p.x, p.y, p.z])
|
||||
|
||||
@@ -360,6 +360,7 @@ fn can_spawn_on(b: &Blocks, pos: [i32; 3], state: BlockStateId, above: BlockStat
|
||||
let mut scene = SceneCollisions {
|
||||
world: b.w.world,
|
||||
binding: b.w.collision,
|
||||
subject: None,
|
||||
};
|
||||
let above_pos = [pos[0], pos[1] + 1, pos[2]];
|
||||
let above_empty =
|
||||
@@ -422,6 +423,7 @@ fn spawn_protector(
|
||||
let mut scene = SceneCollisions {
|
||||
world: w.world,
|
||||
binding: w.collision,
|
||||
subject: None,
|
||||
};
|
||||
if !scene.unobstructed(bounds).unwrap_or(false) {
|
||||
continue;
|
||||
|
||||
@@ -122,6 +122,7 @@ impl MobWorld<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let state = self.world.block(x, y, z);
|
||||
let at = horizon_core::position::BlockPos::new(x, y, z);
|
||||
@@ -168,6 +169,7 @@ impl MobWorld<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
scene.unobstructed(b).unwrap_or(false)
|
||||
}
|
||||
@@ -311,6 +313,7 @@ impl Mob {
|
||||
let mut scene = SceneCollisions {
|
||||
world: w.world,
|
||||
binding: w.collision,
|
||||
subject: None,
|
||||
};
|
||||
let moved = scene
|
||||
.resolve(self.bounds(), wanted, step, self.on_ground)
|
||||
|
||||
@@ -62,6 +62,7 @@ impl MobWorld<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let result = traverse_blocks(from, to, 512, |p: BlockPos| {
|
||||
let state = self.world.block(p.x, p.y, p.z);
|
||||
@@ -86,6 +87,7 @@ impl MobWorld<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let full = horizon_core::geometry::Aabb::new(Vec3::ZERO, Vec3::new(1.0, 1.0, 1.0));
|
||||
let result = traverse_blocks(from, to, 512, |p: BlockPos| {
|
||||
@@ -117,6 +119,7 @@ impl MobWorld<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let result = traverse_blocks(from, to, 512, |p: BlockPos| {
|
||||
let state = self.world.block(p.x, p.y, p.z);
|
||||
@@ -137,6 +140,7 @@ impl MobWorld<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let result = traverse_blocks(from, to, 512, |p: BlockPos| {
|
||||
let state = self.world.block(p.x, p.y, p.z);
|
||||
@@ -156,6 +160,7 @@ impl MobWorld<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
let state = self.world.block(p[0], p[1], p[2]);
|
||||
self.collision
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
//! Stored movement state for the resident scene. This binds the validated air,
|
||||
//! jump, collision, support, restitution and stored health bindings. Entity lifecycle,
|
||||
//! jump, collision, support, restitution and stored health bindings, and
|
||||
//! powder snow's block-inside effects and freezing. Entity lifecycle, other
|
||||
//! block-inside effects and audiovisual event consumers remain separate work.
|
||||
use crate::{Result, assets::Assets, collisions::SceneCollisions, world::World};
|
||||
use horizon_core::{
|
||||
geometry::{Aabb, Vec3, VoxelShape, traverse_blocks},
|
||||
geometry::{Aabb, Axis, Direction, Vec3, VoxelShape, traverse_blocks},
|
||||
position::BlockPos,
|
||||
random::RandomSource,
|
||||
};
|
||||
use horizon_world::{
|
||||
attribute_map::ValueKind,
|
||||
@@ -126,6 +128,16 @@ pub enum MotionEvent {
|
||||
state: BlockStateId,
|
||||
fall_distance: f64,
|
||||
},
|
||||
/// `PowderSnowBlock.entityInside`'s server particle: one snowflake at
|
||||
/// `position` with `velocity` (`RandomizationType.ALTERNATIVE`).
|
||||
Snowflake {
|
||||
position: Vec3,
|
||||
velocity: Vec3,
|
||||
},
|
||||
/// `ServerPlayer.onInsideBlock`: the enter_block criterion trigger.
|
||||
EnterBlock {
|
||||
state: BlockStateId,
|
||||
},
|
||||
}
|
||||
pub struct PlayerMotion {
|
||||
pub position: Vec3,
|
||||
@@ -186,6 +198,23 @@ pub struct PlayerMotion {
|
||||
pub health: crate::health::PlayerHealth,
|
||||
/// Status effects and armor protection seen by movement and damage.
|
||||
pub status: crate::effects::StatusFlags,
|
||||
/// `Entity.isInPowderSnow` / `wasInPowderSnow`.
|
||||
pub in_powder_snow: bool,
|
||||
pub was_in_powder_snow: bool,
|
||||
/// `Entity.DATA_TICKS_FROZEN`.
|
||||
pub ticks_frozen: i32,
|
||||
/// Leather boots on the feet (`PowderSnowBlock.canEntityWalkOnPowderSnow`).
|
||||
pub leather_boots: bool,
|
||||
/// An armor piece in `#freeze_immune_wearables` (`LivingEntity.canFreeze`).
|
||||
pub freeze_immune: bool,
|
||||
/// `Entity.oldPosition`: where this tick started.
|
||||
pub old_position: Vec3,
|
||||
/// `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>,
|
||||
}
|
||||
fn attribute_error(error: impl std::fmt::Debug) -> io::Error {
|
||||
io::Error::other(format!("movement attribute: {error:?}"))
|
||||
@@ -268,8 +297,68 @@ impl PlayerMotion {
|
||||
sync_position: false,
|
||||
events: Vec::new(),
|
||||
attributes,
|
||||
in_powder_snow: false,
|
||||
was_in_powder_snow: false,
|
||||
ticks_frozen: 0,
|
||||
leather_boots: false,
|
||||
freeze_immune: false,
|
||||
old_position: position,
|
||||
block_effects: true,
|
||||
powder_snow_entered: Vec::new(),
|
||||
})
|
||||
}
|
||||
/// `Entity.getTicksRequiredToFreeze`.
|
||||
pub const TICKS_REQUIRED_TO_FREEZE: i32 = 140;
|
||||
/// `Entity.getPercentFrozen`.
|
||||
pub fn percent_frozen(&self) -> f32 {
|
||||
self.ticks_frozen.min(Self::TICKS_REQUIRED_TO_FREEZE) as f32
|
||||
/ Self::TICKS_REQUIRED_TO_FREEZE as f32
|
||||
}
|
||||
/// `Entity.isFullyFrozen`.
|
||||
pub fn is_fully_frozen(&self) -> bool {
|
||||
self.ticks_frozen >= Self::TICKS_REQUIRED_TO_FREEZE
|
||||
}
|
||||
/// `LivingEntity.canFreeze`: not a spectator, no freeze-immune armor.
|
||||
pub fn can_freeze(&self) -> bool {
|
||||
!self.spectator && !self.freeze_immune
|
||||
}
|
||||
/// What powder snow's contextual collision asks of the player.
|
||||
pub fn powder_snow_subject(&self) -> crate::collisions::PowderSnowSubject {
|
||||
crate::collisions::PowderSnowSubject {
|
||||
fall_distance: self.fall_distance,
|
||||
falling_block: false,
|
||||
leather_boots: self.leather_boots,
|
||||
walkable_type: false,
|
||||
}
|
||||
}
|
||||
/// `LivingEntity.tryAddFrost`: the frozen slowdown, on anything but air.
|
||||
fn try_add_frost(&mut self, on_air: bool) -> Result<()> {
|
||||
if on_air || self.ticks_frozen <= 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let Some(key) = self.attributes.key("minecraft:movement_speed") else {
|
||||
return Ok(());
|
||||
};
|
||||
let Some(handle) = self
|
||||
.attributes
|
||||
.get_instance(Some(key))
|
||||
.map_err(attribute_error)?
|
||||
else {
|
||||
return Ok(());
|
||||
};
|
||||
let slow = -0.05f32 * self.percent_frozen();
|
||||
let modifier = Modifier::new(
|
||||
"minecraft:powder_snow",
|
||||
f64::from(slow),
|
||||
Operation::AddValue,
|
||||
)?;
|
||||
self.attributes
|
||||
.edit(&handle, |value, observer| {
|
||||
value.add_transient(modifier, observer)
|
||||
})
|
||||
.map_err(attribute_error)?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn bounds(&self) -> Aabb {
|
||||
self.dimensions.bounds(self.position)
|
||||
}
|
||||
@@ -416,6 +505,7 @@ impl MotionWorld<'_, '_, '_> {
|
||||
SceneCollisions {
|
||||
world: self.scene,
|
||||
binding: self.collision_binding,
|
||||
subject: Some(self.player.powder_snow_subject()),
|
||||
}
|
||||
}
|
||||
pub(crate) fn no_player_collision(&mut self, bounds: Aabb) -> Result<bool> {
|
||||
@@ -470,9 +560,10 @@ impl MotionWorld<'_, '_, '_> {
|
||||
pub fn travel_in_air(&mut self, input: Vec3) -> Result<()> {
|
||||
living_air::travel_in_air(self, input)
|
||||
}
|
||||
/// Dry, standing movement phases of LivingEntity.aiStep, including server
|
||||
/// frost-modifier cleanup with zero frozen ticks. Equipment, interpolation,
|
||||
/// effects and world/entity effects remain unbound. Inputs are the native
|
||||
/// Dry, standing movement phases of LivingEntity.aiStep, including powder
|
||||
/// snow's block-inside effects and server freezing (thawing, the frost
|
||||
/// modifier, freeze damage). Equipment, interpolation, effects and other
|
||||
/// world/entity effects remain unbound. Inputs are the native
|
||||
/// float xxa/yya/zza fields, before LivingEntity.applyInput damping.
|
||||
pub fn movement_tick(&mut self, input: [f32; 3]) -> Result<()> {
|
||||
self.dry_tick(TickInput::Living(input))
|
||||
@@ -481,6 +572,10 @@ impl MotionWorld<'_, '_, '_> {
|
||||
/// caller applies LocalPlayer input preparation once. Empty inventory,
|
||||
/// equipment and nearby-entity sets are the current scene domain.
|
||||
pub fn player_movement_tick(&mut self, direction: [f32; 2], jumping: bool) -> Result<()> {
|
||||
// Entity.baseTick
|
||||
self.player.was_in_powder_snow = self.player.in_powder_snow;
|
||||
self.player.in_powder_snow = false;
|
||||
self.player.old_position = self.player.position;
|
||||
if self.player.jump_trigger_time > 0 {
|
||||
self.player.jump_trigger_time -= 1;
|
||||
}
|
||||
@@ -639,7 +734,269 @@ impl MotionWorld<'_, '_, '_> {
|
||||
} else {
|
||||
self.travel_in_air(input)?;
|
||||
}
|
||||
self.player.remove_frost()
|
||||
if self.player.block_effects {
|
||||
self.apply_effects_from_blocks()?;
|
||||
}
|
||||
// LivingEntity.aiStep "freezing" (server).
|
||||
if !self.player.in_powder_snow || !self.player.can_freeze() {
|
||||
self.player.ticks_frozen = (self.player.ticks_frozen - 2).max(0);
|
||||
}
|
||||
self.player.remove_frost()?;
|
||||
let on = self.support(movement_support::LEGACY_SUPPORT_OFFSET)?;
|
||||
let on_air = self.assets.registry.state(self.block(on)).is_none_or(|s| {
|
||||
matches!(
|
||||
s.name.as_str(),
|
||||
"minecraft:air" | "minecraft:cave_air" | "minecraft:void_air"
|
||||
)
|
||||
});
|
||||
self.player.try_add_frost(on_air)?;
|
||||
if self.player.health.tick_count % 40 == 0
|
||||
&& self.player.is_fully_frozen()
|
||||
&& self.player.can_freeze()
|
||||
{
|
||||
self.freeze_damage()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// `hurtServer(damageSources().freeze(), 1)` for a fully frozen player.
|
||||
fn freeze_damage(&mut self) -> Result<()> {
|
||||
let id = self
|
||||
.resources
|
||||
.health
|
||||
.damage
|
||||
.id("minecraft:freeze")
|
||||
.ok_or("missing freeze damage type")?;
|
||||
self.player.events.push(MotionEvent::Health(
|
||||
crate::health::HealthEvent::FreezeDamage(1.0),
|
||||
));
|
||||
let rules = self.scene.health_rules;
|
||||
self.player.hurt(&self.resources.health, rules, id, 1.0)?;
|
||||
Ok(())
|
||||
}
|
||||
/// `Entity.applyEffectsFromBlocks` for the blocks Horizon's player reacts
|
||||
/// to inside its movement path: powder snow (`PowderSnowBlock.entityInside`
|
||||
/// and the FREEZE / EXTINGUISH effects). Pressure plates, tripwires and
|
||||
/// rails are the world tick's scan.
|
||||
pub fn apply_effects_from_blocks(&mut self) -> Result<()> {
|
||||
use horizon_world::entity_move::MovementRecord;
|
||||
let mut movements: Vec<MovementRecord> =
|
||||
self.player.history.records().iter().copied().collect();
|
||||
self.player.history.clear();
|
||||
let position = self.player.position;
|
||||
match movements.last() {
|
||||
None => movements.push(MovementRecord {
|
||||
from: self.player.old_position,
|
||||
to: position,
|
||||
original: None,
|
||||
}),
|
||||
Some(last) if last.to.distance_squared(position) > f64::from(9.999_999_4e-11_f32) => {
|
||||
movements.push(MovementRecord {
|
||||
from: last.to,
|
||||
to: position,
|
||||
original: None,
|
||||
})
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// isAffectedByBlocks: no physics for spectators.
|
||||
if self.player.spectator {
|
||||
return Ok(());
|
||||
}
|
||||
let mut visited = std::collections::HashSet::new();
|
||||
let mut entered = Vec::new();
|
||||
for movement in &movements {
|
||||
let mut from = movement.from;
|
||||
let delta = movement.to.subtract(movement.from);
|
||||
let mut budget = 16;
|
||||
match movement.original {
|
||||
Some(original) if delta.length_squared() > 0.0 => {
|
||||
for axis in Axis::step_order(original) {
|
||||
let step = delta.component(axis);
|
||||
if step != 0.0 {
|
||||
let to = from.relative(
|
||||
match axis {
|
||||
Axis::X => Direction::East,
|
||||
Axis::Y => Direction::Up,
|
||||
Axis::Z => Direction::South,
|
||||
},
|
||||
step,
|
||||
);
|
||||
budget -= self.check_inside_blocks(
|
||||
from,
|
||||
to,
|
||||
&mut visited,
|
||||
&mut entered,
|
||||
budget,
|
||||
)?;
|
||||
from = to;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
budget -= self.check_inside_blocks(
|
||||
movement.from,
|
||||
movement.to,
|
||||
&mut visited,
|
||||
&mut entered,
|
||||
16,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
if budget <= 0 {
|
||||
self.check_inside_blocks(movement.to, movement.to, &mut visited, &mut entered, 1)?;
|
||||
}
|
||||
}
|
||||
// InsideBlockEffectApplier.StepBasedCollector: the blocks entered
|
||||
// at one step (a sweep iteration differing from the last) form a
|
||||
// group whose EXTINGUISH callbacks (the game destroys those blocks if
|
||||
// the player burns), FREEZE and EXTINGUISH apply once, in order.
|
||||
let mut last_step = -1;
|
||||
let mut groups = 0;
|
||||
for (index, &(step, p)) in entered.iter().enumerate() {
|
||||
if index == 0 || step != last_step {
|
||||
groups += 1;
|
||||
last_step = step;
|
||||
}
|
||||
if groups == 1 {
|
||||
// A later group finds the fire already out.
|
||||
self.player.powder_snow_entered.push(p);
|
||||
}
|
||||
}
|
||||
for _ in 0..groups {
|
||||
if self.player.health.dead_or_dying() {
|
||||
break;
|
||||
}
|
||||
self.player.in_powder_snow = true;
|
||||
if self.player.can_freeze() {
|
||||
self.player.ticks_frozen =
|
||||
(self.player.ticks_frozen + 1).min(PlayerMotion::TICKS_REQUIRED_TO_FREEZE);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// `Entity.checkInsideBlocks(from, to, ...)`: the blocks the box at `to`
|
||||
/// sweeps through from `from`; returns the iterations used plus one.
|
||||
fn check_inside_blocks(
|
||||
&mut self,
|
||||
from: Vec3,
|
||||
to: Vec3,
|
||||
visited: &mut std::collections::HashSet<i64>,
|
||||
entered: &mut Vec<(i32, BlockPos)>,
|
||||
max_iterations: i32,
|
||||
) -> Result<i32> {
|
||||
let deflated = self.player.dimensions.bounds(to).deflate(Vec3::new(
|
||||
f64::from(1.0e-5_f32),
|
||||
f64::from(1.0e-5_f32),
|
||||
f64::from(1.0e-5_f32),
|
||||
));
|
||||
let mut hits = Vec::new();
|
||||
let mut iterations = 0;
|
||||
let alive = !self.player.health.dead_or_dying();
|
||||
horizon_core::geometry::sweep_intersected_blocks(
|
||||
from,
|
||||
to,
|
||||
deflated,
|
||||
1_000_000,
|
||||
|p, iteration| {
|
||||
if !alive || iteration >= max_iterations {
|
||||
return false;
|
||||
}
|
||||
iterations = iteration;
|
||||
hits.push((iteration, p));
|
||||
true
|
||||
},
|
||||
)
|
||||
.map_err(|e| format!("block sweep: {e:?}"))?;
|
||||
for (step, p) in hits {
|
||||
let state = self.block(p);
|
||||
let Some(definition) = self.assets.registry.state(state) else {
|
||||
continue;
|
||||
};
|
||||
let name = definition.name.as_str();
|
||||
if matches!(
|
||||
name,
|
||||
"minecraft:air" | "minecraft:cave_air" | "minecraft:void_air"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let powder_snow = name == "minecraft:powder_snow";
|
||||
// getEntityInsideCollisionShape is the whole block but for powder
|
||||
// snow (its collision shape, the whole block when it is empty),
|
||||
// filled cauldrons, end portals and tripwires; Horizon doesn't
|
||||
// follow the player into those last three.
|
||||
if matches!(
|
||||
name,
|
||||
"minecraft:water_cauldron"
|
||||
| "minecraft:lava_cauldron"
|
||||
| "minecraft:powder_snow_cauldron"
|
||||
| "minecraft:end_portal"
|
||||
| "minecraft:tripwire"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let inside = !powder_snow || {
|
||||
let context = self.context();
|
||||
let binding = self.collision_binding;
|
||||
let shape = binding.collision_shape_with_context(
|
||||
&mut self.collisions(),
|
||||
state,
|
||||
p,
|
||||
&context,
|
||||
)?;
|
||||
shape.is_empty() || shape.boxes() == VoxelShape::block().boxes() || {
|
||||
let boxes: Vec<_> = shape
|
||||
.moved(Vec3::new(f64::from(p.x), f64::from(p.y), f64::from(p.z)))
|
||||
.boxes()
|
||||
.to_vec();
|
||||
self.player
|
||||
.dimensions
|
||||
.bounds(from)
|
||||
.collided_along_vector(to.subtract(from), &boxes)
|
||||
}
|
||||
};
|
||||
if inside && visited.insert(p.pack()) {
|
||||
if powder_snow {
|
||||
self.powder_snow_inside(p)?;
|
||||
entered.push((step, p));
|
||||
}
|
||||
// ServerPlayer.onInsideBlock
|
||||
self.player.events.push(MotionEvent::EnterBlock { state });
|
||||
}
|
||||
}
|
||||
Ok(iterations + 1)
|
||||
}
|
||||
/// `PowderSnowBlock.entityInside` for the player: stuck while the block
|
||||
/// at its feet is powder snow, with a snowflake now and then (server).
|
||||
fn powder_snow_inside(&mut self, p: BlockPos) -> Result<()> {
|
||||
let feet = BlockPos::containing(self.player.position);
|
||||
let in_block = self
|
||||
.assets
|
||||
.registry
|
||||
.state(self.block(feet))
|
||||
.is_some_and(|s| s.name == "minecraft:powder_snow");
|
||||
if !in_block {
|
||||
return Ok(());
|
||||
}
|
||||
// Entity.makeStuckInBlock
|
||||
EntityMoveWorld::reset_fall_distance(self)?;
|
||||
self.player.stuck_multiplier = Vec3::new(f64::from(0.9_f32), 1.5, f64::from(0.9_f32));
|
||||
// ServerPlayer.getKnownMovement: this tick's movement.
|
||||
let known = self.player.position.subtract(self.player.old_position);
|
||||
let random = &mut self.scene.random;
|
||||
if (known.x != 0.0 || known.z != 0.0) && random.next_boolean() {
|
||||
// Mth.randomBetween(random, -1, 1) * (1 / 12)
|
||||
let xa = (random.next_float() * 2.0 - 1.0) * 0.083_333_336_f32;
|
||||
let za = (random.next_float() * 2.0 - 1.0) * 0.083_333_336_f32;
|
||||
self.player.events.push(MotionEvent::Snowflake {
|
||||
position: Vec3::new(
|
||||
self.player.position.x,
|
||||
f64::from(p.y + 1),
|
||||
self.player.position.z,
|
||||
),
|
||||
velocity: Vec3::new(f64::from(xa), f64::from(0.05_f32), f64::from(za)),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
pub fn jump_from_ground(&mut self) -> Result<()> {
|
||||
living_motion::jump_from_ground(self)?;
|
||||
@@ -932,6 +1289,7 @@ impl EntityMoveWorld for MotionWorld<'_, '_, '_> {
|
||||
let mut collisions = SceneCollisions {
|
||||
world: self.scene,
|
||||
binding: self.collision_binding,
|
||||
subject: Some(self.player.powder_snow_subject()),
|
||||
};
|
||||
self.player.ground.set_with_movement(
|
||||
below,
|
||||
@@ -1264,10 +1622,10 @@ impl LivingAirWorld for MotionWorld<'_, '_, '_> {
|
||||
self.player.jumping
|
||||
}
|
||||
fn was_in_powder_snow(&self) -> bool {
|
||||
false
|
||||
self.player.was_in_powder_snow
|
||||
}
|
||||
fn can_walk_powder_snow(&mut self) -> Result<bool> {
|
||||
Ok(false)
|
||||
Ok(self.player.leather_boots)
|
||||
}
|
||||
fn levitation_amplifier(&mut self) -> Result<Option<i32>> {
|
||||
Ok(self.player.status.levitation)
|
||||
|
||||
@@ -685,6 +685,7 @@ impl ParticleLevel<'_> {
|
||||
let mut scene = SceneCollisions {
|
||||
world: self.world,
|
||||
binding: self.collision,
|
||||
subject: None,
|
||||
};
|
||||
match scene.resolve(bb, Vec3::new(d[0], d[1], d[2]), 0.0, false) {
|
||||
Ok(v) => [v.x, v.y, v.z],
|
||||
|
||||
@@ -74,7 +74,8 @@ public final class VanillaSceneMotionOracle {
|
||||
return VanillaBlockLandingOracle.sound(s);
|
||||
}
|
||||
static final class Sources extends DamageSources {
|
||||
DamageSource fall,stalagmite,starve;Sources(){super(null);}
|
||||
DamageSource fall,stalagmite,starve,freeze;Sources(){super(null);}
|
||||
@Override public DamageSource freeze(){return freeze;}
|
||||
@Override public DamageSource starve(){return starve;}
|
||||
@Override public DamageSource fall(){return fall;}
|
||||
@Override public DamageSource stalagmite(){return stalagmite;}
|
||||
@@ -111,9 +112,14 @@ public final class VanillaSceneMotionOracle {
|
||||
@Override public WorldBorder getWorldBorder(){return border;}
|
||||
@Override public DamageSources damageSources(){return sources;}
|
||||
@Override public net.minecraft.util.RandomSource getRandom(){return rng;}
|
||||
@Override public boolean isRainingAt(BlockPos p){return false;}
|
||||
@Override public net.minecraft.server.MinecraftServer getServer(){return server;}
|
||||
@Override public void gameEvent(Holder<GameEvent> event,Vec3 pos,GameEvent.Context context){if(event==GameEvent.HIT_GROUND)actor.events.add("hit "+vectorBits(pos)+" "+Block.getId(context.affectedState()));}
|
||||
@Override public void gameEvent(Holder<GameEvent> event,BlockPos pos,GameEvent.Context context){actor.events.add("event "+(event==GameEvent.BLOCK_CHANGE?"change":event==GameEvent.BLOCK_DESTROY?"destroy":"unknown")+" "+blockPos(pos)+" "+Block.getId(context.affectedState())+" "+(context.sourceEntity()!=null));}
|
||||
@Override public <T extends net.minecraft.core.particles.ParticleOptions> int sendParticles(T particle,double x,double y,double z,int count,double dx,double dy,double dz,double xs,double ys,double zs,net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket.RandomizationType type){
|
||||
if(particle!=net.minecraft.core.particles.ParticleTypes.SNOWFLAKE||count!=1||dx!=0||dy!=0||dz!=0||type!=net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket.RandomizationType.ALTERNATIVE)throw new AssertionError();
|
||||
actor.events.add("snowflake "+vectorBits(new Vec3(x,y,z))+" "+vectorBits(new Vec3(xs,ys,zs)));return 0;
|
||||
}
|
||||
@Override public <T extends net.minecraft.core.particles.ParticleOptions> int sendParticles(T particle,double x,double y,double z,int count,double dx,double dy,double dz,double speed){
|
||||
var b=(net.minecraft.core.particles.BlockParticleOption)particle;if(dx!=0||dy!=0||dz!=0)throw new AssertionError();
|
||||
actor.events.add(String.format("particles %d %s %d %016x",Block.getId(b.getState()),vectorBits(new Vec3(x,y,z)),count,Double.doubleToLongBits(speed)));return 0;
|
||||
@@ -199,16 +205,23 @@ public final class VanillaSceneMotionOracle {
|
||||
String name=stat==net.minecraft.stats.Stats.DAMAGE_ABSORBED?"absorbed":stat==net.minecraft.stats.Stats.DAMAGE_TAKEN?"taken":stat==net.minecraft.stats.Stats.DAMAGE_RESISTED?"resisted":null;
|
||||
if(name==null){name=stat.getPath();if(!Set.of("swim_one_cm","walk_under_water_one_cm","walk_on_water_one_cm","climb_one_cm","sprint_one_cm","crouch_one_cm","walk_one_cm","aviate_one_cm","fly_one_cm").contains(name))throw new AssertionError(stat);}events.add("health-stat "+name+" "+value);
|
||||
}
|
||||
@Override public boolean hurtServer(net.minecraft.server.level.ServerLevel level,DamageSource source,float damage){var sources=((Level)level).sources;events.add(String.format("damage %s %08x",source==sources.fall?"fall":source==sources.stalagmite?"stalagmite":source==sources.starve?"starve":"unknown",Float.floatToIntBits(damage)));return super.hurtServer(level,source,damage);}
|
||||
@Override public boolean hurtServer(net.minecraft.server.level.ServerLevel level,DamageSource source,float damage){var sources=((Level)level).sources;events.add(String.format("damage %s %08x",source==sources.fall?"fall":source==sources.stalagmite?"stalagmite":source==sources.starve?"starve":source==sources.freeze?"freeze":"unknown",Float.floatToIntBits(damage)));return super.hurtServer(level,source,damage);}
|
||||
@Override public void teleportRelative(double x,double y,double z){setPos(getX()+x,getY()+y,getZ()+z);}
|
||||
@Override protected void playStepSound(BlockPos p,BlockState state){}
|
||||
@Override public void gameEvent(Holder<GameEvent> event,Entity source){}
|
||||
@Override public LivingEntity getControllingPassenger(){return null;}
|
||||
@Override public double getFluidHeight(net.minecraft.tags.TagKey<net.minecraft.world.level.material.Fluid> tag){return 0;}
|
||||
@Override public double getFluidJumpThreshold(){return .4;}
|
||||
@Override public int getTicksFrozen(){return 0;}
|
||||
@Override public void setTicksFrozen(int value){if(value!=0)throw new AssertionError(value);}
|
||||
@Override protected void applyEffectsFromBlocks(){}
|
||||
// Block-inside effects and frozen ticks run only in "snow_tick" operations.
|
||||
boolean effects;int frozen;Vec3 tickStart;
|
||||
@Override public int getTicksFrozen(){return frozen;}
|
||||
@Override public void setTicksFrozen(int value){if(!effects&&value!=0)throw new AssertionError(value);frozen=value;}
|
||||
@Override protected void applyEffectsFromBlocks(){if(effects)super.applyEffectsFromBlocks();}
|
||||
// LivingEntity.canFreeze with #freeze_immune_wearables, whose only
|
||||
// member a scene wears is leather boots (item tags aren't bound here).
|
||||
@Override public boolean canFreeze(){return !isSpectator()&&!getItemBySlot(net.minecraft.world.entity.EquipmentSlot.FEET).is(net.minecraft.world.item.Items.LEATHER_BOOTS);}
|
||||
// ServerPlayer.lastKnownClientMovement: this tick's movement.
|
||||
@Override public Vec3 getKnownMovement(){return tickStart==null?Vec3.ZERO:position().subtract(tickStart);}
|
||||
@Override protected void applyInput(){if(!preparedInput)super.applyInput();}
|
||||
void peaceful(){tickRegeneration();}
|
||||
void clock()throws Exception{sceneLevel.time++;tickCount++;if(cooldown.getInt(this)>0)cooldown.setInt(this,cooldown.getInt(this)-1);if(hurtTime>0)hurtTime--;}
|
||||
@@ -240,7 +253,7 @@ public final class VanillaSceneMotionOracle {
|
||||
out.printf("%s %s %s %s %s %s %s %016x %s %s %s %08x %d %08x %d %d %s | %s%n",p.horizontalCollision,p.verticalCollision,p.verticalCollisionBelow,p.minorHorizontalCollision,
|
||||
p.onGround(),noBlocks.getBoolean(p),pos(support.get(p)),Double.doubleToLongBits(p.fallDistance),pos(last.get(p)),p.needsSync,p.syncPosition,Float.floatToIntBits(storedSpeed.getFloat(p)),delay.getInt(p),Float.floatToIntBits(p.yHeadRot),trigger.getInt(p),grace.getInt(p),impact.get(p)==null?"none":vectorBits((Vec3)impact.get(p)),String.join(";",attributes));
|
||||
out.println("events "+String.join(" | ",p.events));p.events.clear();
|
||||
out.println("block "+Block.getId(p.level().getBlockState(new BlockPos(25,3,25))));
|
||||
out.println("block "+Block.getId(p.level().getBlockState(new BlockPos(25,3,25)))+" frozen "+p.frozen+" "+p.isInPowderSnow+" "+p.wasInPowderSnow+" "+vectorBits((Vec3)stuck.get(p)));
|
||||
out.printf("health %08x %08x %08x %d %d %08x %d %d %s %d %s %s %s %s %s %d%n",
|
||||
Float.floatToIntBits(p.getHealth()),Float.floatToIntBits(p.getAbsorptionAmount()),Float.floatToIntBits(foodExhaustion.getFloat(p.getFoodData())),
|
||||
noAction.getInt(p),cooldown.getInt(p),Float.floatToIntBits(lastHurt.getFloat(p)),p.hurtDuration,p.hurtTime,sourceName((DamageSource)lastDamage.get(p)),lastStamp.getLong(p),healthDead.getBoolean(p),p.syncVelocity,
|
||||
@@ -270,11 +283,11 @@ public final class VanillaSceneMotionOracle {
|
||||
var trace=new Trace();trace.out=new PrintWriter(OutputStream.nullOutputStream());trace.fail=-1;
|
||||
var level=(Level)allocate.invoke(unsafe,Level.class);level.chunks=VanillaSceneCollisionOracle.scene(description,trace);level.border=new Border();
|
||||
level.rng=net.minecraft.util.RandomSource.create(0);level.sources=(Sources)allocate.invoke(unsafe,Sources.class);
|
||||
level.sources.fall=damageSources.get("minecraft:fall");level.sources.stalagmite=damageSources.get("minecraft:stalagmite");level.sources.starve=damageSources.get("minecraft:starve");
|
||||
level.sources.fall=damageSources.get("minecraft:fall");level.sources.stalagmite=damageSources.get("minecraft:stalagmite");level.sources.starve=damageSources.get("minecraft:starve");level.sources.freeze=damageSources.get("minecraft:freeze");
|
||||
level.waypoints=(Waypoints)allocate.invoke(unsafe,Waypoints.class);level.waypoints.level=level;
|
||||
level.rules=new Rules();level.difficulty=net.minecraft.world.Difficulty.NORMAL;
|
||||
var server=(VanillaBlockLandingOracle.Server)allocate.invoke(unsafe,VanillaBlockLandingOracle.Server.class);
|
||||
server.d=new VanillaBlockLandingOracle.Data();server.d.out=new PrintWriter(OutputStream.nullOutputStream());server.d.f=JsonParser.parseString("{\"fail\":-1,\"protected\":false}").getAsJsonObject();level.server=server;
|
||||
server.d=new VanillaBlockLandingOracle.Data();server.d.out=new PrintWriter(OutputStream.nullOutputStream());server.d.f=JsonParser.parseString("{\"fail\":-1,\"protected\":false}").getAsJsonObject();level.server=server;write(field(net.minecraft.server.MinecraftServer.class,"debugSubscribers"),server,new net.minecraft.util.debug.ServerDebugSubscribers(server));
|
||||
try(var out=new PrintWriter(Files.newBufferedWriter(dir.resolve(args[0]+".txt")));var enriched=new PrintWriter(Files.newBufferedWriter(dir.resolve("inputs-"+args[0]+".txt")));var lines=Files.lines(dir.resolve("inputs.txt"))){
|
||||
for(var line:(Iterable<String>)lines::iterator){
|
||||
var row=JsonParser.parseString(line).getAsJsonObject();var p=(Actor)allocate.invoke(unsafe,Actor.class);p.configure(level);p.events=new ArrayList<>();level.actor=p;
|
||||
@@ -285,6 +298,8 @@ public final class VanillaSceneMotionOracle {
|
||||
var equipment=new EntityEquipment();write(field(LivingEntity.class,"equipment"),p,equipment);
|
||||
write(field(Player.class,"inventory"),p,new Inventory(p,equipment));
|
||||
p.attrs=new TrackedAttributes(VanillaEntityAttributeOracle.supplier(EntityTypes.PLAYER));write(field(LivingEntity.class,"attributes"),p,p.attrs);
|
||||
write(field(Entity.class,"finalMovementsThisTick"),p,new ArrayList<>());write(field(Entity.class,"visitedBlocks"),p,new it.unimi.dsi.fastutil.longs.LongOpenHashSet());
|
||||
write(field(Entity.class,"insideEffectCollector"),p,new net.minecraft.world.entity.InsideBlockEffectApplier.StepBasedCollector());
|
||||
write(history,p,new ArrayDeque<>());write(support,p,Optional.empty());write(last,p,Optional.empty());write(stuck,p,Vec3.ZERO);write(abilities,p,new Abilities());
|
||||
write(field(ServerPlayer.class,"shoulderEntityLeft"),p,new net.minecraft.nbt.CompoundTag());write(field(ServerPlayer.class,"shoulderEntityRight"),p,new net.minecraft.nbt.CompoundTag());
|
||||
p.connection=(Connection)allocate.invoke(unsafe,Connection.class);
|
||||
@@ -334,6 +349,27 @@ public final class VanillaSceneMotionOracle {
|
||||
case "travel"->p.travel(VanillaSceneCollisionOracle.vector(op.get("value")));
|
||||
case "tick"->p.movementTick(VanillaSceneCollisionOracle.vector(op.get("value")));
|
||||
case "player_tick"->p.playerMovementTick(VanillaSceneCollisionOracle.vector(op.get("value")),op.get("jumping").getAsBoolean());
|
||||
case "snow_tick"->{
|
||||
// The clock, Entity.baseTick's powder snow flags and old
|
||||
// position, then aiStep with its block-inside effects.
|
||||
p.clock();p.wasInPowderSnow=p.isInPowderSnow;p.isInPowderSnow=false;
|
||||
p.xOld=p.getX();p.yOld=p.getY();p.zOld=p.getZ();p.tickStart=p.position();
|
||||
p.effects=true;
|
||||
try{p.playerMovementTick(VanillaSceneCollisionOracle.vector(op.get("value")),op.get("jumping").getAsBoolean());}finally{p.effects=false;}
|
||||
}
|
||||
case "boots"->{
|
||||
// Leather boots without data components (item components aren't
|
||||
// bound here), straight into the equipment: no equip callbacks.
|
||||
var stack=net.minecraft.world.item.ItemStack.EMPTY;
|
||||
if(op.get("value").getAsBoolean()){
|
||||
stack=(net.minecraft.world.item.ItemStack)allocate.invoke(unsafe,net.minecraft.world.item.ItemStack.class);
|
||||
write(field(net.minecraft.world.item.ItemStack.class,"item"),stack,net.minecraft.world.item.Items.LEATHER_BOOTS.builtInRegistryHolder());
|
||||
field(net.minecraft.world.item.ItemStack.class,"count").setInt(stack,1);
|
||||
write(field(net.minecraft.world.item.ItemStack.class,"components"),stack,new net.minecraft.core.component.PatchedDataComponentMap(net.minecraft.core.component.DataComponentMap.EMPTY));
|
||||
}
|
||||
((EntityEquipment)field(LivingEntity.class,"equipment").get(p)).set(net.minecraft.world.entity.EquipmentSlot.FEET,stack);
|
||||
}
|
||||
case "frozen"->p.frozen=op.get("value").getAsInt();
|
||||
case "trigger"->trigger.setInt(p,op.get("value").getAsInt());
|
||||
case "delay"->delay.setInt(p,op.get("value").getAsInt());
|
||||
case "ack_attributes"->{p.attrs.getAttributesToUpdate().clear();p.attrs.getAttributesToSync().clear();}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user