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:
Ludovic Marie
2026-10-14 15:45:55 +02:00
parent 6f2823b652
commit 6ce83021b5
33 changed files with 781 additions and 29 deletions
@@ -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() {
+1 -1
View File
@@ -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('#') {
+20 -5
View File
@@ -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))
}
}
+2
View File
@@ -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
+67 -3
View File
@@ -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,
+2
View File
@@ -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 =
+1
View File
@@ -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,
+1
View File
@@ -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
+3
View File
@@ -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())
}
+1
View File
@@ -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;
+3
View File
@@ -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)
+5
View File
@@ -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
+366 -8
View File
@@ -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)
+1
View File
@@ -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