Implement cushion placement and synchronize placed cushions

Add cushion entities, shape-based placement and support checks, colored rendering, server tracking, sitting, destruction and save/load support. Compare placement rules against vanilla and exercise host/guest interactions.

Co-Authored-By: OpenAI GPT-6.1 Sol <noreply@openai.com>
This commit is contained in:
Eshayat Al-Wasiu
2026-10-26 13:29:16 +06:00
parent 7cb390fbff
commit f4b04218f1
20 changed files with 1069 additions and 5 deletions
@@ -0,0 +1,89 @@
use horizon_client::{
Result,
cushion::{self, World},
};
use horizon_core::geometry::{Aabb, ShapeOp, Vec3, VoxelShape};
use serde_json::Value;
use std::{
collections::HashMap,
fs::File,
io::{BufRead, BufReader, BufWriter, Write},
};
struct Cell {
outline: VoxelShape,
collision: VoxelShape,
suffocating: bool,
}
struct Fixture(HashMap<[i32; 3], Cell>);
impl World for Fixture {
fn outline_bounds(&self, p: [i32; 3]) -> Option<Aabb> {
self.0
.get(&p)?
.outline
.bounds()
.map(|b| b.move_by(Vec3::new(p[0] as f64, p[1] as f64, p[2] as f64)))
}
fn collision_shapes(&self, _: Aabb) -> Vec<VoxelShape> {
self.0
.iter()
.map(|(p, c)| {
c.collision
.moved(Vec3::new(p[0] as f64, p[1] as f64, p[2] as f64))
})
.collect()
}
fn suffocating(&self, p: [i32; 3]) -> bool {
self.0.get(&p).is_some_and(|c| c.suffocating)
}
}
fn shape(value: &Value) -> Result<VoxelShape> {
let mut shape = VoxelShape::empty();
for b in value.as_array().unwrap() {
let f = |i: usize| b[i].as_f64().unwrap();
shape = shape.join(
&VoxelShape::from_box(Aabb::new(
Vec3::new(f(0), f(1), f(2)),
Vec3::new(f(3), f(4), f(5)),
))?,
ShapeOp::OR,
)?;
}
Ok(shape)
}
pub fn main() -> Result<()> {
let args: Vec<_> = crate::args().collect();
let mut out = BufWriter::new(File::create(&args[2])?);
for line in BufReader::new(File::open(&args[1])?).lines() {
let row: Value = serde_json::from_str(&line?)?;
let mut world = Fixture(HashMap::new());
for c in row["cells"].as_array().unwrap() {
let p = std::array::from_fn(|i| c[i].as_i64().unwrap() as i32);
world.0.insert(
p,
Cell {
outline: shape(&c[3])?,
collision: shape(&c[4])?,
suffocating: c[5].as_bool().unwrap(),
},
);
}
let p = &row["pos"];
let (x, y, z) = (
p[0].as_f64().unwrap(),
p[1].as_f64().unwrap(),
p[2].as_f64().unwrap(),
);
let bounds = Aabb::new(
Vec3::new(x - 0.5, y, z - 0.5),
Vec3::new(x + 0.5, y + 0.25, z + 0.5),
);
writeln!(
out,
"{} {}",
cushion::can_place(&world, bounds),
cushion::survives(&world, bounds)
)?;
}
Ok(())
}
@@ -28,6 +28,7 @@ mod cloud_fixture;
mod command_parse_fixture;
mod container_screen_probe;
mod creative_search_fixture;
mod cushion_placement_probe;
mod emitter_particles_probe;
mod enchanting_fixture;
mod entity_anim_fixture;
@@ -107,6 +108,7 @@ probes! {
block_transform_fixture,
breaking_fixture,
camera_probe,
cushion_placement_probe,
cauldron_fixture,
chat_screen_fixture,
cloud_fixture,
+59
View File
@@ -0,0 +1,59 @@
//! Cushion placement and support checks (`Cushion.canBePlacedAt`/`wouldSurviveAt`).
use horizon_core::geometry::{Aabb, ShapeOp, Vec3, VoxelShape};
pub trait World {
fn outline_bounds(&self, pos: [i32; 3]) -> Option<Aabb>;
fn collision_shapes(&self, bounds: Aabb) -> Vec<VoxelShape>;
fn suffocating(&self, pos: [i32; 3]) -> bool;
}
pub fn blocks_in(a: Aabb) -> impl Iterator<Item = [i32; 3]> {
let (lo, hi) = (a.min_position(), a.max_position());
let (x0, y0, z0) = (
lo.x.floor() as i32,
lo.y.floor() as i32,
lo.z.floor() as i32,
);
let (x1, y1, z1) = (
hi.x.floor() as i32,
hi.y.floor() as i32,
hi.z.floor() as i32,
);
(y0..=y1).flat_map(move |y| (z0..=z1).flat_map(move |z| (x0..=x1).map(move |x| [x, y, z])))
}
pub fn survives(world: &impl World, bounds: Aabb) -> bool {
let (lo, hi) = (bounds.min_position(), bounds.max_position());
let anchor = Aabb::new(
Vec3::new(lo.x, lo.y - 0.015625, lo.z),
Vec3::new(hi.x.next_down(), lo.y, hi.z.next_down()),
);
// The extra eighth block expands the scan, not the intersection test.
let supported = blocks_in(anchor.expand_towards(Vec3::new(0.0, -0.125, 0.0))).any(|p| {
world
.outline_bounds(p)
.is_some_and(|shape| shape.intersects(anchor))
});
supported && !blocks_in(bounds.next_deflated()).all(|p| world.suffocating(p))
}
pub fn can_place(world: &impl World, bounds: Aabb) -> bool {
if !survives(world, bounds) {
return false;
}
let (lo, hi) = (bounds.min_position(), bounds.max_position());
let slab = Aabb::new(lo, Vec3::new(hi.x, lo.y + 0.015625, hi.z)).next_deflated();
let Ok(mut remaining) = VoxelShape::from_box(slab) else {
return false;
};
for shape in world.collision_shapes(slab) {
let Ok(next) = remaining.join(&shape, ShapeOp::ONLY_FIRST) else {
return false;
};
remaining = next;
if remaining.is_empty() {
return false;
}
}
true
}
+51
View File
@@ -589,6 +589,57 @@ impl Painting {
)
}
}
/// The `DyeColor` ordinal of a colour name (`DyeColor.byName`), white when
/// the name is unknown.
pub fn dye_index(name: &str) -> u8 {
crate::mobs::render::DYE_NAMES
.iter()
.position(|n| *n == name)
.unwrap_or(0) as u8
}
/// A cushion (`Cushion`, a `BlockAttachedEntity`): a 1 x 0.25 block pillow
/// that seats a player. It sits on the top face of a block, at the clicked
/// height, and is coloured by its `DyeColor`.
#[derive(Clone, Debug)]
pub struct Cushion {
pub id: u32,
/// `Entity.position`: the bottom centre of the cushion.
pub position: Vec3,
/// `BlockAttachedEntity.pos` (the saved `block_pos`).
pub pos: [i32; 3],
pub yaw: f32,
/// `DyeColor` ordinal: white 0, orange 1, ..., black 15.
pub color: u8,
/// BlockAttachedEntity.ticksSinceLastCheck.
pub check_interval: u32,
}
impl Cushion {
/// `Entity.makeBoundingBox`: x and z half a block out, y to y + 0.25.
pub fn bounds(&self) -> ([f64; 3], [f64; 3]) {
let p = self.position;
(
[p.x - 0.5, p.y, p.z - 0.5],
[p.x + 0.5, p.y + 0.25, p.z + 0.5],
)
}
/// The `DyeColor` name of the cushion.
pub fn color_name(&self) -> &'static str {
crate::mobs::render::DYE_NAMES[usize::from(self.color.min(15))]
}
/// `Cushion.getCushionItemStackWithData`: the matching coloured item.
pub fn item(&self) -> String {
format!("minecraft:{}_cushion", self.color_name())
}
/// `Cushion.interact`'s seat: the vehicle's PASSENGER attachment
/// `(0, 0.25, 0)` less the player's `(0, 0.6, 0)`.
pub fn seat(&self) -> Vec3 {
Vec3::new(self.position.x, self.position.y - 0.35, self.position.z)
}
/// `Entity.getDismountLocationForPassenger`: on top of the cushion.
pub fn dismount(&self) -> Vec3 {
Vec3::new(self.position.x, self.position.y + 0.25, self.position.z)
}
}
/// An item frame or glow item frame (`ItemFrame`).
#[derive(Clone, Debug)]
pub struct ItemFrame {
+74 -1
View File
@@ -6,7 +6,7 @@
//! survive a Horizon save.
use crate::{
arrows::Pickup,
entities::{ArmorStand, ExperienceOrb, ItemEntity, ItemFrame, Painting},
entities::{ArmorStand, Cushion, ExperienceOrb, ItemEntity, ItemFrame, Painting},
mobs::{Mob, MobType, Who},
projectiles::{Projectile, ProjectileKind},
save::{
@@ -1313,6 +1313,79 @@ pub fn painting_from_nbt(tag: &Tag) -> Option<([i32; 3], u8, String)> {
let facing = (number(tag.get("facing")).unwrap_or(0) & 3) as u8;
Some((pos, facing, text(tag.get("variant"))?))
}
/// Cushion.addAdditionalSaveData: the block it sits on and its colour.
pub fn cushion_to_nbt(c: &Cushion, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
let base = Base {
id: "minecraft:cushion",
uuid,
position: c.position,
motion: Vec3::ZERO,
rotation: [c.yaw, 0.0],
on_ground: false,
fall_distance: 0.0,
};
let mut tag = base.tag(raw);
put(&mut tag, "block_pos", Tag::IntArray(c.pos.to_vec()));
put(&mut tag, "color", string(c.color_name()));
tag
}
/// The position, block position, yaw and colour of a saved cushion.
pub fn cushion_from_nbt(tag: &Tag, id: u32) -> Option<Cushion> {
let position = position(tag)?;
let pos = match tag.get("block_pos") {
Some(Tag::IntArray(v)) if v.len() == 3 => [v[0], v[1], v[2]],
_ => [
position.x.floor() as i32,
position.y.floor() as i32,
position.z.floor() as i32,
],
};
let yaw = match tag.get("Rotation") {
Some(Tag::List(l)) => l
.first()
.and_then(|v| crate::save::float(Some(v)))
.unwrap_or(0.0) as f32,
_ => 0.0,
};
let color =
crate::entities::dye_index(&text(tag.get("color")).unwrap_or_else(|| "white".into()));
Some(Cushion {
id,
position,
pos,
yaw,
color,
check_interval: 0,
})
}
#[cfg(test)]
mod cushion_tests {
use super::*;
#[test]
fn cushion_save_preserves_color_anchor_rotation_and_unknown_data() {
let cushion = Cushion {
id: 7,
position: Vec3::new(-1.5, 64.5, 3.5),
pos: [-2, 64, 3],
yaw: 270.0,
color: 14,
check_interval: 55,
};
let raw = Tag::compound([("CustomName", string("Seat"))]);
let tag = cushion_to_nbt(&cushion, [1, 2, 3, 4], Some(&raw));
assert_eq!(text(tag.get("CustomName")).as_deref(), Some("Seat"));
let loaded = cushion_from_nbt(&tag, 8).unwrap();
assert_eq!(loaded.id, 8);
assert_eq!(loaded.position, cushion.position);
assert_eq!(loaded.pos, cushion.pos);
assert_eq!(loaded.yaw, cushion.yaw);
assert_eq!(loaded.color_name(), "red");
assert_eq!(loaded.check_interval, 0);
assert_eq!(loaded.bounds(), cushion.bounds());
}
}
/// ItemFrame.addAdditionalSaveData.
pub fn item_frame_to_nbt(f: &ItemFrame, uuid: [i32; 4], raw: Option<&Tag>) -> Tag {
let (lo, hi) = f.bounds();
+17 -1
View File
@@ -33,6 +33,7 @@ mod copper_golems;
mod creaking;
mod creatures;
mod crossbow;
pub(crate) mod cushions;
mod debug_keys;
mod decorated_pot;
mod dedicated;
@@ -1421,6 +1422,8 @@ struct Game<'a> {
map_frame_model: frames::FrameModel,
glow_map_frame_model: frames::FrameModel,
armor_stands: Vec<horizon_client::entities::ArmorStand>,
/// `Cushion` decoration entities (game/cushions.rs).
cushions: Vec<horizon_client::entities::Cushion>,
/// Entities of types Horizon does not simulate.
others: Vec<commands::OtherEntity>,
armor_stand_assets: armor_stands::ArmorStandAssets,
@@ -1436,6 +1439,8 @@ struct Game<'a> {
boat_models: boats::BoatModels,
/// EndCrystalModel's baked layer.
crystal_model: Option<horizon_client::mobs::Part>,
/// CushionModel's baked layer.
cushion_model: Option<horizon_client::mobs::Part>,
carts: Vec<minecarts::Minecart>,
/// Songs playing from jukeboxes (tracked sound handles).
jukebox_sounds: HashMap<(i32, i32, i32), u64>,
@@ -3460,6 +3465,7 @@ impl Game<'_> {
fn interact_entity(&mut self) -> Result<bool> {
Ok(self.interact_frame()
|| self.interact_armor_stand()
|| self.interact_cushion()
|| self.interact_boat()
|| self.interact_cart()
|| self.interact_mob()?
@@ -3646,6 +3652,7 @@ impl Game<'_> {
|| self.place_painting(hit, &stack)
|| self.place_item_frame(hit, &stack)
|| self.place_armor_stand(hit, &stack)
|| self.place_cushion(hit, &stack)
|| self.place_end_crystal(hit, &stack))
{
return Ok(true);
@@ -4526,6 +4533,7 @@ impl Game<'_> {
horizon_client::spans::time("tick_paintings", || self.tick_paintings());
horizon_client::spans::time("tick_item_frames", || self.tick_item_frames());
horizon_client::spans::time("tick_armor_stands", || self.tick_armor_stands());
horizon_client::spans::time("tick_cushions", || self.tick_cushions());
horizon_client::spans::time("tick_boats", || self.tick_boats());
horizon_client::spans::time("tick_carts", || self.tick_carts());
horizon_client::spans::time("tick_thunder", || self.tick_thunder());
@@ -7210,6 +7218,7 @@ impl Game<'_> {
self.frame_geometry(camera, &mut out);
self.mark_entity(out.len(), "minecraft:armor_stand");
self.armor_stand_geometry(camera, alpha, &mut out);
self.cushion_geometry(camera, &mut out);
self.mark_entity(out.len(), "minecraft:oak_boat");
self.boat_geometry(camera, alpha, &mut out);
self.mark_entity(out.len(), "minecraft:minecart");
@@ -7495,6 +7504,7 @@ impl Game<'_> {
.map_or(0.0, |i| i.attack_damage);
if self.attack_frame()
|| self.attack_armor_stand()
|| self.attack_cushion()
|| self.attack_boat(hand_damage)
|| self.attack_cart(hand_damage)
|| self.attack_dragon()
@@ -8240,6 +8250,7 @@ impl Game<'_> {
self.boats.clear();
self.carts.clear();
self.armor_stands.clear();
self.cushions.clear();
self.item_frames.clear();
self.paintings.clear();
self.others.clear();
@@ -12245,6 +12256,7 @@ impl<'a> Game<'a> {
map_frame_model: prepared.map_frame_model,
glow_map_frame_model: prepared.glow_map_frame_model,
armor_stands: Vec::new(),
cushions: Vec::new(),
others: Vec::new(),
armor_stand_assets: prepared.armor_stand_assets,
waypoint_styles: prepared.waypoint_styles,
@@ -12254,6 +12266,7 @@ impl<'a> Game<'a> {
boats: Vec::new(),
boat_models: prepared.boat_models,
crystal_model: prepared.crystal_model,
cushion_model: prepared.cushion_model,
carts: Vec::new(),
jukebox_sounds: HashMap::new(),
bolts: Vec::new(),
@@ -12391,6 +12404,7 @@ struct GameCpuResources {
boat_models: boats::BoatModels,
cart_models: minecarts::CartModels,
crystal_model: Option<horizon_client::mobs::Part>,
cushion_model: Option<horizon_client::mobs::Part>,
dragon_model: Option<horizon_client::mobs::Part>,
arrow_model: Option<horizon_client::mobs::Part>,
trident_model: Option<horizon_client::mobs::Part>,
@@ -12411,6 +12425,7 @@ impl GameCpuResources {
boat_models: boats::BoatModels::load(document.as_ref()),
cart_models: minecarts::CartModels::load(document.as_ref()),
crystal_model: model("minecraft:end_crystal#main"),
cushion_model: model("minecraft:cushion#main"),
dragon_model: model("minecraft:ender_dragon#main"),
arrow_model: model("minecraft:arrow#main"),
trident_model: model("minecraft:trident#main"),
@@ -13751,7 +13766,7 @@ mod startup_cpu_tests {
r#"{
"minecraft:end_crystal#main": {}, "minecraft:ender_dragon#main": {},
"minecraft:arrow#main": {}, "minecraft:trident#main": {},
"minecraft:armor_stand#main": {}
"minecraft:armor_stand#main": {}, "minecraft:cushion#main": {}
}"#,
)
.unwrap();
@@ -13771,6 +13786,7 @@ mod startup_cpu_tests {
.unwrap();
std::fs::remove_dir_all(&root).unwrap();
assert!(prepared.crystal_model.is_some());
assert!(prepared.cushion_model.is_some());
assert!(prepared.dragon_model.is_some());
assert!(prepared.arrow_model.is_some());
assert!(prepared.trident_model.is_some());
+3
View File
@@ -587,6 +587,9 @@ impl Game<'_> {
}
/// The player's tick while riding: steer, tick the boat, sit in it.
pub(super) fn tick_riding(&mut self, keys: Keys) -> horizon_client::Result<()> {
if self.tick_cushion_riding(keys) {
return Ok(());
}
let Some(i) = self.boats.iter().position(|b| Some(b.id) == self.riding) else {
if self.tick_cart_riding(keys) || self.tick_mount_riding(keys) {
return Ok(());
+482
View File
@@ -0,0 +1,482 @@
//! Cushions: `CushionItem.useOn` placement on the top face of a block,
//! `Cushion.canBePlacedAt`/`survives`, `Cushion.interact` (sit),
//! `BlockAttachedEntity.hurtServer` (break, dropping the matching colour),
//! `BlockAttachedEntity.tick` and `CushionRenderer`.
//!
//! A cushion is a `BlockAttachedEntity`, not a block: it has no block state,
//! only an entity whose `Pos` is the click point's height. Placement uses
//! outline bounds, collision shapes and suffocation predicates.
use super::{Game, Hit, view_vector};
use horizon_client::entities::{Cushion, dye_index};
use horizon_client::items::ItemStack;
use horizon_client::mobs::anim::Poses;
use horizon_core::geometry::{Aabb, Vec3};
use winit::keyboard::KeyCode;
/// `Direction.fromYRot(yaw).toYRot()`: the nearest 90 degree step.
pub(crate) fn snap_yaw(yaw: f32) -> f32 {
(((f64::from(yaw) / 90.0 + 0.5).floor() as i64) & 3) as f32 * 90.0
}
fn aabb(bounds: ([f64; 3], [f64; 3])) -> Aabb {
Aabb::new(
Vec3::new(bounds.0[0], bounds.0[1], bounds.0[2]),
Vec3::new(bounds.1[0], bounds.1[1], bounds.1[2]),
)
}
fn overlaps(a: Aabb, b: Aabb) -> bool {
a.intersects(b)
}
use horizon_client::cushion::blocks_in;
struct PlacementWorld<'a, 'r>(&'a Game<'r>);
impl horizon_client::cushion::World for PlacementWorld<'_, '_> {
fn outline_bounds(&self, p: [i32; 3]) -> Option<Aabb> {
let boxes = self.0.outline_boxes(p);
let mut shape = *boxes.first()?;
for b in &boxes[1..] {
for k in 0..3 {
shape[k] = shape[k].min(b[k]);
shape[k + 3] = shape[k + 3].max(b[k + 3]);
}
}
Some(Aabb::new(
Vec3::new(shape[0], shape[1], shape[2]),
Vec3::new(shape[3], shape[4], shape[5]),
))
}
fn collision_shapes(&self, bounds: Aabb) -> Vec<horizon_core::geometry::VoxelShape> {
let binding = self.0.res.collision();
let mut world = horizon_client::collisions::SceneCollisions {
world: &self.0.world,
binding,
subject: None,
};
// Include neighbour blocks whose collision shape can reach outside their cell.
blocks_in(bounds.inflate(Vec3::new(1.0, 1.0, 1.0)))
.filter_map(|p| {
let state = self.0.world.block(p[0], p[1], p[2]);
binding
.collision_shape(&mut world, state, p)
.ok()
.map(|s| s.moved(Vec3::new(f64::from(p[0]), f64::from(p[1]), f64::from(p[2]))))
})
.collect()
}
fn suffocating(&self, p: [i32; 3]) -> bool {
let state = self.0.world.block(p[0], p[1], p[2]);
let facts = self.0.res.facts().get(state);
let name = facts.name.as_ref();
// Blocks' explicit predicates, then the default causes_suffocation/full-shape rule.
match name {
"minecraft:dirt_path"
| "minecraft:farmland"
| "minecraft:mud"
| "minecraft:soul_sand" => true,
"minecraft:piston" | "minecraft:sticky_piston" => self
.0
.pack
.assets
.registry
.state(state)
.is_some_and(|s| s.properties.get("extended").is_some_and(|p| p == "false")),
"minecraft:mangrove_roots" | "minecraft:moving_piston" => false,
_ if name.ends_with("_leaves")
|| name.ends_with("glass")
|| name.ends_with("copper_grate") =>
{
false
}
_ if name.ends_with("shulker_box") => true,
_ => {
facts.full_collision
&& self
.0
.res
.items()
.block_in_tag("minecraft:causes_suffocation", name)
}
}
}
}
impl Game<'_> {
/// The cushion under the crosshair, its distance and the hit point.
pub(super) fn cushion_hit(&self) -> Option<(usize, f64, [f64; 3])> {
let eye = self.eye_position()?;
let dir = view_vector(self.yaw, self.pitch);
let reach: f64 = if self.creative { 5.0 } else { 3.0 };
let mut best: Option<(usize, f64, [f64; 3])> = None;
for (i, c) in self.cushions.iter().enumerate() {
let Some(d) = super::ray_box(eye, dir, reach, aabb(c.bounds())) else {
continue;
};
if best.is_none_or(|b| d < b.1) {
best = Some((i, d, std::array::from_fn(|k| eye[k] + dir[k] * d)));
}
}
best
}
pub(super) fn cushion_survives(&self, c: &Cushion) -> bool {
horizon_client::cushion::survives(&PlacementWorld(self), aabb(c.bounds()))
}
/// `Cushion.destroyIfInFire`: a fire-tagged block inside the deflated box.
fn cushion_in_fire(&self, c: &Cushion) -> bool {
let bounds = aabb(c.bounds()).next_deflated();
blocks_in(bounds).any(|p| {
let facts = self.res.facts().get(self.world.block(p[0], p[1], p[2]));
self.res.items().block_in_tag("minecraft:fire", &facts.name)
})
}
/// `CushionItem.useOn`: on the top face only, spawning at the click's
/// height above the clicked block (or the replaceable block itself).
pub(super) fn place_cushion(&mut self, hit: &Hit, stack: &ItemStack) -> bool {
let Some(info) = self.res.items().get(&stack.item).cloned() else {
return false;
};
let Some(default_color) = info.cushion_color.as_deref() else {
return false;
};
let color_override = stack
.data()
.and_then(|d| d.other("minecraft:cushion/color"))
.and_then(|tag| match tag {
horizon_world::nbt::Tag::String(s) => Some(s.to_string_lossy()),
_ => None,
});
let color = dye_index(color_override.as_deref().unwrap_or(default_color));
// CushionItem.recalculateContextForSpecialCollisionShapes: open containers
// (cauldrons, hoppers and composters) use the collision ray's actual height.
let mut adjusted = Hit {
pos: hit.pos,
face: hit.face,
point: hit.point,
};
let state = self.world.block(hit.pos[0], hit.pos[1], hit.pos[2]);
if self.res.items().block_in_tag(
"minecraft:cushion_uses_collision_shape",
&self.res.facts().get(state).name,
) && let Some(eye) = self.eye_position()
{
let start = Vec3::new(eye[0], eye[1], eye[2]);
let point = Vec3::new(hit.point[0], hit.point[1], hit.point[2]);
let end = point.add_vector(point.subtract(start).normalize().scale(0.001));
let binding = self.res.collision();
let mut world = horizon_client::collisions::SceneCollisions {
world: &self.world,
binding,
subject: None,
};
if let Ok(shape) = binding.collision_shape(&mut world, state, hit.pos)
&& let Some(result) = shape.clip(
start,
end,
horizon_core::position::BlockPos::new(hit.pos[0], hit.pos[1], hit.pos[2]),
)
{
adjusted.face = result.direction.normal();
adjusted.point = [result.location.x, result.location.y, result.location.z];
}
}
let hit = &adjusted;
if hit.face != [0, 1, 0] {
return false;
}
// BlockPlaceContext: a replaceable clicked block is replaced.
let clicked = self
.res
.facts()
.get(self.world.block(hit.pos[0], hit.pos[1], hit.pos[2]));
let pos = if clicked.replaceable {
hit.pos
} else {
std::array::from_fn(|i| hit.pos[i] + hit.face[i])
};
// `Vec3.atCenterOfWithY(clickedPos, clickLocation.y)`.
let y = hit.point[1];
let position = Vec3::new(f64::from(pos[0]) + 0.5, y, f64::from(pos[2]) + 0.5);
let bounds = aabb((
[position.x - 0.5, position.y, position.z - 0.5],
[position.x + 0.5, position.y + 0.25, position.z + 0.5],
));
if !horizon_client::cushion::can_place(&PlacementWorld(self), bounds) {
return false;
}
// `getEntitiesOfClass(Cushion.class, spawnAABB).isEmpty()`.
if self
.cushions
.iter()
.any(|c| overlaps(aabb(c.bounds()), bounds))
{
return false;
}
self.next_entity += 1;
let id = self.next_entity;
// `BlockAttachedEntity.setPos`: `BlockPos.containing(position)`.
let attached = [
position.x.floor() as i32,
position.y.floor() as i32,
position.z.floor() as i32,
];
self.cushions.push(Cushion {
id,
position,
pos: attached,
yaw: snap_yaw(self.yaw),
color,
check_interval: 0,
});
self.world
.modified
.insert((attached[0] >> 4, attached[2] >> 4));
self.audio.play(
"minecraft:entity.cushion.place",
Some([position.x, position.y, position.z]),
0.75,
0.8,
);
if !self.creative {
self.consume_held();
}
true
}
/// `Cushion.interact`: sit down unless sneaking or already occupied.
pub(super) fn interact_cushion(&mut self) -> bool {
let Some((i, d, _)) = self.cushion_hit() else {
return false;
};
if !self.before_block(d) {
return false;
}
// isSecondaryUseActive.
if self.keys.contains(&KeyCode::ShiftLeft) {
return false;
}
let id = self.cushions[i].id;
let occupied = self.riding == Some(id)
|| self
.lan
.as_ref()
.is_some_and(|h| h.hub.guests.iter().any(|g| g.vehicle == Some(id)));
// `Cushion.interact`: a vehicle it cannot seat returns CONSUME.
if occupied {
self.swing_arm();
return true;
}
// `Player.startRiding` gives up the vehicle it already has; the
// client never sends that click, so pass instead.
if self.riding.is_some() {
return false;
}
self.riding = Some(id);
let p = self.cushions[i].position;
self.audio.play(
"minecraft:entity.cushion.sit",
Some([p.x, p.y, p.z]),
1.0,
1.0,
);
self.swing_arm();
true
}
/// `Cushion.hurtServer` from a player: killed, then the item dropped.
pub(super) fn attack_cushion(&mut self) -> bool {
let Some((i, d, _)) = self.cushion_hit() else {
return false;
};
if !self.before_block(d) || self.mob_hit().is_some_and(|m| m.1 < d) {
return false;
}
// `skipAttackInteraction` when `mayInteract` is false.
if !self.may_build() {
return true;
}
self.break_cushion(i, true);
true
}
/// `Cushion.dropItem`: the break sound and particles, then the coloured
/// item unless drops are off or the breaker has infinite materials.
pub(super) fn break_cushion(&mut self, index: usize, by_player: bool) {
let c = self.cushions.remove(index);
let p = c.position;
self.world.modified.insert((c.pos[0] >> 4, c.pos[2] >> 4));
if self.riding == Some(c.id) {
self.riding = None;
}
self.audio.play(
"minecraft:entity.cushion.break",
Some([p.x, p.y, p.z]),
1.0,
1.0,
);
self.cushion_break_particles(&c);
if !self.ui.rules.bool("entity_drops") || (by_player && self.creative) {
return;
}
let stack = ItemStack {
item: std::sync::Arc::from(c.item()),
count: 1,
damage: 0,
extra: None,
};
self.spawn_item(stack, [p.x, p.y, p.z], [0.0; 3], 10);
}
/// `Cushion.showBreakingParticles`: ten of the matching wool block.
fn cushion_break_particles(&mut self, c: &Cushion) {
let name = format!("minecraft:{}_wool", c.color_name());
let Some(state) = self.pack.assets.registry.default_state(&name) else {
return;
};
let at = [
c.position.x.floor() as i32,
c.position.y.floor() as i32,
c.position.z.floor() as i32,
];
let Some((sprite, tint)) = self.particle_sprite(state, at) else {
return;
};
let options = horizon_client::particles::ParticleOptions::block(
horizon_client::particles::ParticleType::Block,
sprite,
tint,
);
let p = c.position;
self.particles.send_particles(
&options,
[p.x, p.y + 0.666_666_7, p.z],
[0.25, 0.0625, 0.25],
0.05,
10,
);
}
/// `BlockAttachedEntity.tick`: survive and burn checks after the post-increment reaches 100.
pub(super) fn tick_cushions(&mut self) {
let ticking = self.entity_ticking_area();
let mut i = 0;
while i < self.cushions.len() {
let p = self.cushions[i].position;
if !ticking(p.x.floor() as i32, p.z.floor() as i32) {
i += 1;
continue;
}
let previous = self.cushions[i].check_interval;
self.cushions[i].check_interval += 1;
if previous < 100 {
i += 1;
continue;
}
self.cushions[i].check_interval = 0;
let c = self.cushions[i].clone();
if self.cushion_in_fire(&c) || !self.cushion_survives(&c) {
self.break_cushion(i, false);
continue;
}
i += 1;
}
}
/// `Player.rideTick` on a cushion: hold the seat, and get up on sneak.
pub(super) fn tick_cushion_riding(&mut self, keys: super::Keys) -> bool {
let Some(c) = self
.cushions
.iter()
.find(|c| Some(c.id) == self.riding)
.cloned()
else {
return false;
};
if keys.shift {
let at = c.dismount();
self.riding = None;
if let Some(p) = self.player.as_mut() {
self.previous_position = p.position;
p.position = at;
p.velocity = Vec3::ZERO;
p.fall_distance = 0.0;
}
// `Cushion.removePassenger`: the get-up sound for a rider that
// left rather than one whose cushion was killed.
self.audio.play(
"minecraft:entity.cushion.get_up",
Some([c.position.x, c.position.y, c.position.z]),
1.0,
1.0,
);
return true;
}
let seat = c.seat();
if let Some(p) = self.player.as_mut() {
self.previous_position = p.position;
p.position = seat;
p.velocity = Vec3::ZERO;
p.fall_distance = 0.0;
}
true
}
/// `CushionRenderer`: the coloured model, rotated to the nearest
/// direction, with its bottom on the cushion's position.
pub(super) fn cushion_geometry(
&self,
camera: glam::DVec3,
out: &mut Vec<horizon_client::mesher::Vertex>,
) {
let Some(model) = self.cushion_model.as_ref() else {
return;
};
let sprites = &self.gui.atlas.sprites;
for c in &self.cushions {
let entity_start = out.len();
let a = aabb(c.bounds());
let (lo, hi) = (a.min_position(), a.max_position());
let (lo, hi) = (
glam::DVec3::new(lo.x, lo.y, lo.z),
glam::DVec3::new(hi.x, hi.y, hi.z),
);
let range =
super::entity_culling::default_range(hi - lo, self.settings.entity_distance);
if !self.entity_in_view(camera, (lo + hi) / 2.0, range, lo, hi) {
continue;
}
let Some(uv) = sprites
.get(&format!("entity/cushion/{}_cushion", c.color_name()))
.map(|r| [r.u0, r.v0, r.u1, r.v1])
else {
continue;
};
let p = glam::DVec3::new(c.position.x, c.position.y, c.position.z) - camera;
let base = glam::Mat4::from_translation(p.as_vec3())
* glam::Mat4::from_rotation_y((180.0 - snap_yaw(c.yaw)).to_radians())
* glam::Mat4::from_rotation_x(std::f32::consts::PI)
* glam::Mat4::from_translation(glam::Vec3::new(0.0, -0.25, 0.0));
// `EntityRenderer.getPackedLightCoords` probes at the eye height.
let l = self.world.light(
c.position.x.floor() as i32,
(c.position.y + 0.2125).floor() as i32,
c.position.z.floor() as i32,
);
let packed = (u32::from(l >> 4) * 16) | ((u32::from(l & 15) * 16) << 8);
horizon_client::mobs::render::draw(
model,
base,
&Poses::new(model),
uv,
0xFFFF_FFFF,
packed,
0,
out,
);
self.cull_entity_vertices(out, entity_start);
}
}
}
+1
View File
@@ -151,6 +151,7 @@ impl Game<'_> {
before(self.painting_hit().map(|h| h.1))
|| before(self.frame_hit().map(|h| h.1))
|| before(self.armor_stand_hit().map(|h| h.1))
|| before(self.cushion_hit().map(|h| h.1))
|| before(self.boat_hit().map(|h| h.1))
|| before(self.cart_hit().map(|h| h.1))
|| before(self.dragon_hit().map(|h| h.1))
@@ -245,6 +245,16 @@ impl Game<'_> {
}
out.push(e);
}
for c in &self.cushions {
let p = c.position;
let mut e = state(c.id, "minecraft:cushion", [p.x, p.y, p.z], c.yaw, false);
// Cushion.DATA_COLOR, set unless it is the default white.
if c.color != 0 {
e.meta
.push((8, serializer::DYE_COLOR, Value::Id(i32::from(c.color))));
}
out.push(e);
}
self.lan_projectile_states(host_entity, hub, out);
for o in &self.others {
let p = o.position;
+26 -1
View File
@@ -56,6 +56,13 @@ impl Game<'_> {
let (lo, hi) = corners(a.bounds());
return Some((lo, hi, at(a.position)));
}
if let Some(c) = self.cushions.iter().find(|c| c.id == id) {
let (lo, hi) = corners(Aabb::new(
Vec3::new(c.position.x - 0.5, c.position.y, c.position.z - 0.5),
Vec3::new(c.position.x + 0.5, c.position.y + 0.25, c.position.z + 0.5),
));
return Some((lo, hi, at(c.position)));
}
if let Some(f) = self.item_frames.iter().find(|f| f.id == id) {
let (lo, hi) = f.bounds();
return Some((lo, hi, centre(f.pos)));
@@ -98,9 +105,10 @@ impl Game<'_> {
let boat = self.boats.iter().position(|b| b.id == vehicle);
let cart = self.carts.iter().any(|c| c.id == vehicle);
let mount = self.mobs.iter().any(|m| m.id == vehicle);
let cushion = self.cushions.iter().any(|c| c.id == vehicle);
// The vehicle is gone (broken, in another level) or the rider died:
// its client drops it with the entity or the death screen.
if dead || (boat.is_none() && !cart && !mount) {
if dead || (boat.is_none() && !cart && !mount && !cushion) {
let g = &mut host.hub.guests[gi];
(g.vehicle, g.vehicle_good) = (None, None);
return;
@@ -115,6 +123,17 @@ impl Game<'_> {
// Getting out goes through leave_guest, which tells the client and
// teleports it (ServerPlayer.dismountTo).
let seat = self.as_guest(host, conn, |game| {
// A cushion does not move: the rider sits on it until it sneaks
// (`Cushion.removePassenger` plays the get-up sound).
if game.cushions.iter().any(|c| Some(c.id) == game.riding) {
if keys.shift {
game.tick_cushion_riding(keys);
return None;
}
let c = game.cushions.iter().find(|c| Some(c.id) == game.riding)?;
let p = c.seat();
return Some([p.x, p.y, p.z]);
}
if let Some(i) = game.boats.iter().position(|b| Some(b.id) == game.riding) {
if keys.shift {
game.dismount(i);
@@ -168,6 +187,12 @@ impl Game<'_> {
.iter()
.find(|c| c.id == vehicle)
.map(|c| (c.position, c.position.add_vector(Vec3::new(0.0, SEAT, 0.0))))
})
.or_else(|| {
self.cushions
.iter()
.find(|c| c.id == vehicle)
.map(|c| (c.position, c.seat()))
});
if let Some((p, seat)) = at {
let g = &mut host.hub.guests[gi];
+3
View File
@@ -50,6 +50,7 @@ pub(super) struct Parked {
paintings: Vec<horizon_client::entities::Painting>,
item_frames: Vec<horizon_client::entities::ItemFrame>,
armor_stands: Vec<horizon_client::entities::ArmorStand>,
cushions: Vec<horizon_client::entities::Cushion>,
others: Vec<commands::OtherEntity>,
boats: Vec<boats::Boat>,
carts: Vec<minecarts::Minecart>,
@@ -135,6 +136,7 @@ impl Game<'_> {
paintings: Vec::new(),
item_frames: Vec::new(),
armor_stands: Vec::new(),
cushions: Vec::new(),
others: Vec::new(),
boats: Vec::new(),
carts: Vec::new(),
@@ -185,6 +187,7 @@ impl Game<'_> {
swap(&mut self.paintings, &mut p.paintings);
swap(&mut self.item_frames, &mut p.item_frames);
swap(&mut self.armor_stands, &mut p.armor_stands);
swap(&mut self.cushions, &mut p.cushions);
swap(&mut self.others, &mut p.others);
swap(&mut self.boats, &mut p.boats);
swap(&mut self.carts, &mut p.carts);
+27
View File
@@ -314,6 +314,13 @@ impl Game<'_> {
self.item_frames.push(f);
}
}
"minecraft:cushion" => {
self.next_entity += 1;
if let Some(c) = entity_nbt::cushion_from_nbt(&tag, self.next_entity) {
self.remember(c.id, uuid, tag);
self.cushions.push(c);
}
}
"minecraft:painting" => {
if let Some((pos, facing, variant)) = entity_nbt::painting_from_nbt(&tag)
&& let Some(v) = self.painting_variants.get(&variant).cloned()
@@ -776,6 +783,18 @@ impl Game<'_> {
out.entry(chunk_of(at)).or_default().push(tag);
}
}
for c in &self.cushions {
if keys.contains(&chunk_of(c.position)) {
let uuid = self
.persist
.uuids
.get(&c.id)
.copied()
.unwrap_or_else(random_uuid);
let tag = entity_nbt::cushion_to_nbt(c, uuid, self.persist.raw.get(&c.id));
out.entry(chunk_of(c.position)).or_default().push(tag);
}
}
for p in &self.paintings {
let at = painting_center(p);
if keys.contains(&chunk_of(at)) {
@@ -868,6 +887,13 @@ impl Game<'_> {
}
keep
});
self.cushions.retain(|c| {
let keep = !gone(c.position);
if !keep {
removed.push(c.id);
}
keep
});
// Riders leave with their root vehicle, a boat's with the boat.
let leaving: HashSet<u32> = (0..self.mobs.len())
.filter(|&i| {
@@ -934,6 +960,7 @@ impl Game<'_> {
.chain(self.boats.iter().map(|e| e.id))
.chain(self.carts.iter().map(|e| e.id))
.chain(self.armor_stands.iter().map(|e| e.id))
.chain(self.cushions.iter().map(|e| e.id))
.chain(self.item_frames.iter().map(|e| e.id))
.chain(self.paintings.iter().map(|e| e.id))
.chain(self.others.iter().map(|e| e.id))
@@ -3,7 +3,7 @@
//! `handleSetEquipment`): the server simulates them, the client places them
//! where it says and interpolates their moves like a mob's.
use super::Game;
use horizon_client::entities::{ArmorStand, ItemFrame, Painting};
use horizon_client::entities::{ArmorStand, Cushion, ItemFrame, Painting};
use horizon_core::geometry::Vec3;
use horizon_protocol::entity_data::Value;
use std::sync::Arc;
@@ -57,6 +57,16 @@ impl Game<'_> {
invisible: false,
check_interval: 0,
});
} else if kind == "minecraft:cushion" {
// BlockAttachedEntity.pos: `BlockPos.containing` of the spawn.
self.cushions.push(Cushion {
id,
position: at,
pos: block,
yaw: super::cushions::snap_yaw(yaw),
color: 0,
check_interval: 0,
});
} else if kind == "minecraft:lightning_bolt" {
self.spawn_remote_bolt(id, position);
} else if kind == "minecraft:painting" {
@@ -152,6 +162,11 @@ impl Game<'_> {
}
_ => {}
}
} else if let Some(c) = self.cushions.iter_mut().find(|c| c.id == id) {
// Cushion.DATA_COLOR (DyeColor, a byte-backed id).
if let (8, Value::Id(color)) = (index, value) {
c.color = (*color).clamp(0, 15) as u8;
}
} else if let Some(b) = self.boats.iter_mut().find(|b| b.id == id) {
// VehicleEntity's hurt, direction and damage; the paddles.
match (index, value) {
@@ -267,6 +282,12 @@ impl Game<'_> {
out.push(json!({"id": a.id, "kind": "minecraft:armor_stand", "position": at(a.position), "yaw": a.yaw,
"small": a.small, "arms": a.show_arms, "no_base_plate": a.no_base_plate, "equipment": items}));
}
for c in &self.cushions {
out.push(
json!({"id": c.id, "kind": "minecraft:cushion", "position": at(c.position),
"yaw": c.yaw, "color": c.color_name()}),
);
}
for f in &self.item_frames {
out.push(
json!({"id": f.id, "kind": f.kind(), "position": f.pos, "facing": f.facing,
@@ -286,6 +307,7 @@ impl Game<'_> {
self.carts.clear();
self.boats.clear();
self.armor_stands.clear();
self.cushions.clear();
self.item_frames.clear();
self.paintings.clear();
}
@@ -295,6 +317,7 @@ impl Game<'_> {
self.carts.retain(|c| c.id != id);
self.boats.retain(|b| b.id != id);
self.armor_stands.retain(|a| a.id != id);
self.cushions.retain(|c| c.id != id);
self.item_frames.retain(|f| f.id != id);
self.paintings.retain(|p| p.id != id);
self.bolts.retain(|b| b.id != id);
@@ -56,6 +56,11 @@ impl Game<'_> {
let p = a.position;
hits.push((a.id as i32, d, [p.x, p.y, p.z]));
}
if let Some((i, d, _)) = self.cushion_hit() {
let c = &self.cushions[i];
let p = c.position;
hits.push((c.id as i32, d, [p.x, p.y, p.z]));
}
if let Some((i, d)) = self.boat_hit() {
let p = self.boats[i].position;
hits.push((self.boats[i].id as i32, d, [p.x, p.y, p.z]));
@@ -138,7 +143,10 @@ impl Game<'_> {
self.position_rider();
return true;
}
let seat = if let Some(i) = self.boats.iter().position(|b| b.id == id) {
let seat = if let Some(c) = self.cushions.iter().find(|c| c.id == id) {
// The seat does not move; the rider sits on the cushion.
Some(c.seat())
} else if let Some(i) = self.boats.iter().position(|b| b.id == id) {
// AbstractBoat.tick on the controlling client: steer and move.
let mut b = self.boats[i].clone();
b.input = [keys.left, keys.right, keys.forward, keys.backward];
+3
View File
@@ -120,6 +120,8 @@ pub struct ItemInfo {
pub crafting_remainder: Option<String>,
/// `dye`: the dye colour name (dyes, and anything else with the component).
pub dye: Option<String>,
/// `cushion/color`: the DyeColor name of a cushion item.
pub cushion_color: Option<String>,
/// `block_transformer`: the registry entry an axe, hoe or shovel uses on blocks.
pub block_transformer: Option<String>,
/// Has a default `bundle_contents` (the bundles, `BundleItem`).
@@ -599,6 +601,7 @@ impl Items {
.map(str::to_owned),
jukebox_song: c["minecraft:jukebox_playable"].as_str().map(str::to_owned),
dye: c["minecraft:dye"].as_str().map(str::to_owned),
cushion_color: c["minecraft:cushion/color"].as_str().map(str::to_owned),
block_transformer: c["minecraft:block_transformer"].as_str().map(str::to_owned),
bundle: c.get("minecraft:bundle_contents").is_some(),
crafting_remainder: CRAFT_REMAINDERS
+1
View File
@@ -50,6 +50,7 @@ pub mod cooldowns;
pub mod crafting;
pub mod crash;
pub mod creative;
pub mod cushion;
pub mod data_cmd;
pub mod datapack;
pub mod debug_chart;
@@ -0,0 +1,91 @@
package dev.horizonlab.parity;
import com.google.gson.*;
import java.io.PrintWriter;
import java.nio.file.*;
import java.util.*;
import java.util.function.*;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.*;
import net.minecraft.util.Continuation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.decoration.Cushion;
import net.minecraft.world.level.BlockGetter;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.*;
import net.minecraft.world.level.blockscan.*;
import net.minecraft.world.phys.*;
import net.minecraft.world.phys.shapes.*;
public final class VanillaCushionPlacementOracle {
record Cell(VoxelShape outline, VoxelShape collision, boolean suffocating) {}
static final Cell AIR = new Cell(Shapes.empty(), Shapes.empty(), false);
static final class FixtureBlock extends Block {
FixtureBlock() { super(BlockBehaviour.Properties.of().dynamicShape()
.setId(ResourceKey.create(Registries.BLOCK, Identifier.parse("horizon:cushion_fixture")))
.isSuffocating((s, w, p) -> ((World)w).cell(p).suffocating())); }
@Override protected VoxelShape getShape(BlockState s, BlockGetter w, BlockPos p, CollisionContext c) {
return w instanceof World f ? f.cell(p).outline() : Shapes.empty();
}
}
static BlockState state;
static final class World extends ClientLevel {
Map<BlockPos, Cell> cells;
World() { super(null, null, null, null, 0, 0, null, false, 0, 0); }
Cell cell(BlockPos p) { return cells.getOrDefault(p, AIR); }
@Override public BlockState getBlockState(BlockPos p) { return state; }
@Override public Iterable<VoxelShape> getBlockCollisions(Entity e, AABB b) {
var result = new ArrayList<VoxelShape>();
cells.forEach((p, c) -> result.add(c.collision().move(p)));
return result;
}
@Override public BlockMatcher findBlocksIn(AABB bounds) {
return new BlockMatcher(this) {
@Override public boolean atLeastMatched(int n) { throw new AssertionError(); }
@Override protected boolean anyMatched(Predicate<BlockState> p) { throw new AssertionError(); }
@Override public void forEach(BiConsumer<BlockPos, BlockState> c) { throw new AssertionError(); }
@Override public boolean forEachUntil(BlockStateConsumer consumer) {
for (var p : BlockPos.betweenClosed(bounds)) {
if (consumer.apply(p, state) == Continuation.ABORT) return true;
}
return false;
}
};
}
}
static VoxelShape shape(JsonElement v) {
var result = Shapes.empty();
for (var e : v.getAsJsonArray()) {
var b = e.getAsJsonArray();
result = Shapes.or(result, Shapes.box(b.get(0).getAsDouble(), b.get(1).getAsDouble(), b.get(2).getAsDouble(), b.get(3).getAsDouble(), b.get(4).getAsDouble(), b.get(5).getAsDouble()));
}
return result;
}
public static void main(String[] args) throws Exception {
net.minecraft.SharedConstants.tryDetectVersion(); net.minecraft.server.Bootstrap.bootStrap();
// Supply one unregistered fixture block without changing the checks under test.
var registry = net.minecraft.core.registries.BuiltInRegistries.BLOCK;
var holders = net.minecraft.core.MappedRegistry.class.getDeclaredField("unregisteredIntrusiveHolders");
holders.setAccessible(true); holders.set(registry, new IdentityHashMap<>());
var frozen = net.minecraft.core.MappedRegistry.class.getDeclaredField("frozen");
frozen.setAccessible(true); frozen.setBoolean(registry, false);
state = new FixtureBlock().defaultBlockState();
var u = Class.forName("sun.misc.Unsafe"); var f = u.getDeclaredField("theUnsafe"); f.setAccessible(true);
var unsafe = f.get(null); var allocate = u.getMethod("allocateInstance", Class.class);
try (var out = new PrintWriter(Files.newBufferedWriter(Path.of(args[1]))); var lines = Files.lines(Path.of(args[0]))) {
for (var line : (Iterable<String>)lines::iterator) {
var row = JsonParser.parseString(line).getAsJsonObject();
var world = (World)allocate.invoke(unsafe, World.class); world.cells = new HashMap<>();
for (var e : row.getAsJsonArray("cells")) {
var c = e.getAsJsonArray();
world.cells.put(new BlockPos(c.get(0).getAsInt(), c.get(1).getAsInt(), c.get(2).getAsInt()), new Cell(shape(c.get(3)), shape(c.get(4)), c.get(5).getAsBoolean()));
}
var p = row.getAsJsonArray("pos"); double x=p.get(0).getAsDouble(), y=p.get(1).getAsDouble(), z=p.get(2).getAsDouble();
var bounds = new AABB(x-.5, y, z-.5, x+.5, y+.25, z+.5);
out.println(Cushion.canBePlacedAt(world, bounds) + " " + Cushion.wouldSurviveAt(world, bounds));
}
}
}
}
+46
View File
@@ -6591,5 +6591,51 @@ CHECKS += ['replant']
CHECK_FNS['replant'] = check_replant
def check_cushions(host, guest, out):
"""#1935: place a colored cushion, consume it, track it, sit, dismount and break it."""
seen = {}
for label, player, color in (('host', host, 'red'), ('guest', guest, 'blue')):
x, y, z = (int(v // 1) for v in pos_of(player))
at = (x + 0.5, y, z + 2.5)
host('command', command=f'fill {x - 1} {y} {z + 1} {x + 1} {y + 3} {z + 3} minecraft:air')
host('command', command=f'setblock {x} {y - 1} {z + 2} minecraft:stone')
player('command', command='gamemode survival')
player('command', command=f'item replace entity @s weapon.mainhand with minecraft:{color}_cushion')
player.wait(10)
player.raw('look-at', *at)
player.raw('mouse', 'right', 'click')
player.wait(15)
def cushions(game):
return [o for o in game('status')['objects'] if o['kind'] == 'minecraft:cushion'
and abs(o['position'][0] - at[0]) < .1 and abs(o['position'][2] - at[2]) < .1]
server, client = cushions(host), cushions(player)
seen[label] = {'server': server, 'client': client,
'remaining': player('inventory')['counts'].get(f'minecraft:{color}_cushion', 0)}
assert len(server) == len(client) == 1, seen
assert server[0]['color'] == client[0]['color'] == color, seen
assert seen[label]['remaining'] == 0, seen
player.shot(out, 'cushion-placed')
player.raw('look-at', at[0], at[1] + .125, at[2])
player.raw('mouse', 'right', 'click')
player.wait(10)
seen[label]['riding'] = player('status').get('riding')
assert seen[label]['riding'] == server[0]['id'], seen
player.raw('key', 'shift', 'down')
player.wait(5)
player.raw('key', 'shift', 'up')
player.wait(5)
assert player('status').get('riding') is None, seen
player('command', command='gamemode creative')
player.raw('look-at', at[0], at[1] + .125, at[2])
player.raw('mouse', 'left', 'click')
player.wait(10)
assert not cushions(host) and not cushions(player), seen
return seen
CHECKS += ['cushions']
CHECK_FNS['cushions'] = check_cushions
if __name__ == '__main__':
main()
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""Compare cushion support, burial and coverage against vanilla's real checks."""
import json
import random
try:
from . import lab, oracle_harness as harness
except ImportError:
import lab, oracle_harness as harness
def make_input(build):
full = [[0, 0, 0, 1, 1, 1]]
shapes = [[], full, [[0, 0, 0, 1, .5, 1]], [[0, .5, 0, 1, 1, 1]],
[[.375, 0, .375, .625, 1.5, .625]], [[0, 0, 0, .5, 1, 1]],
[[0, 0, 0, .5, 1, 1], [.5, 0, 0, 1, 1, 1]]]
rows = []
for x, z in ((0, 0), (-2, -3), (17, 31)):
for y in (0, .125, .5, .875, 1, 1.00000001, 1.015625, 1.125):
for outline in shapes:
for collision in shapes:
for suffocating in (False, True):
rows.append(dict(pos=[x + .5, y, z + .5],
cells=[[x, 0, z, outline, collision, suffocating]]))
rng = random.Random(1935)
for _ in range(500):
cells = []
for y in (-1, 0, 1):
for x in (-1, 0, 1):
for z in (-1, 0, 1):
if rng.random() < .3:
cells.append([x, y, z, rng.choice(shapes), rng.choice(shapes), bool(rng.randrange(2))])
rows.append(dict(pos=[rng.choice((0, .5, 1)), rng.choice((0, .5, 1, 1.125)), rng.choice((0, .5, 1))], cells=cells))
path = build / 'inputs.jsonl'
path.write_text(''.join(json.dumps(row) + '\n' for row in rows))
return path, len(rows)
def main():
harness.differential(
'rust-cushion-placement-parity', 'VanillaCushionPlacementOracle',
'cushion_placement_probe', make_input,
'Actual Cushion.canBePlacedAt and wouldSurviveAt, with controlled outline, collision '
'and suffocation queries. Includes partial supports, burial, complete coverage, '
'overhanging outlines, negative coordinates and boundary heights. Item use, '
'entity tracking, interaction and persistence are checked separately.',
sources=[lab.ROOT / 'crates/horizon-client/src/cushion.rs'])
if __name__ == '__main__':
main()