mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Match shulker teleport collision and attachment rules
Co-Authored-By: Codex <[email protected]>
This commit is contained in:
1 parent
2e5a2629a0
commit
5150ff66bf
4 files changed
+484
-37
No files matched your search
@@ -14,7 +14,7 @@ use super::{
|
||||
use crate::projectiles::{Projectile, ProjectileWorld, STEPS};
|
||||
use horizon_core::geometry::trig::Mth;
|
||||
use horizon_core::{
|
||||
geometry::{Aabb, Vec3},
|
||||
geometry::{Aabb, Direction, ShapeOp, Vec3, VoxelShape},
|
||||
random::{LegacyRandom, RandomSource},
|
||||
};
|
||||
use std::f32::consts::PI;
|
||||
@@ -110,19 +110,121 @@ fn progress_aabb(direction: u8, from: f64, to: f64, pos: Vec3) -> Aabb {
|
||||
))
|
||||
.move_by(pos)
|
||||
}
|
||||
/// `Level.noCollision(shulker, aabb)`: no block collision under the empty
|
||||
/// context, and none of the entities a shulker collides with overlaps
|
||||
/// (`canBeCollidedWith`: living shulkers and adult happy ghasts; players
|
||||
/// and other mobs never block it).
|
||||
fn no_collision(m: &Mob, w: &MobWorld, bounds: Aabb) -> bool {
|
||||
let mut scene = crate::collisions::SceneCollisions {
|
||||
world: w.world,
|
||||
binding: w.collision,
|
||||
subject: None,
|
||||
};
|
||||
if !crate::collisions::no_collision_empty(&mut scene, bounds) {
|
||||
return false;
|
||||
/// Block queries shared by attachment and teleport candidates. Entity overlap
|
||||
/// is checked separately, using the nearby-entity index from MobWorld.
|
||||
struct BlockSpace<'w, 'r> {
|
||||
world: &'w crate::world::World,
|
||||
registry: &'r horizon_world::registry::BlockRegistry,
|
||||
facts: &'w crate::blocks::BlockFacts,
|
||||
binding: &'w horizon_world::block_collision::BlockCollisionRegistry<'r>,
|
||||
powder_walkable: bool,
|
||||
}
|
||||
impl<'w, 'r> BlockSpace<'w, 'r> {
|
||||
fn context(m: &Mob) -> horizon_world::collision_context::CollisionContext<'_> {
|
||||
use horizon_world::{
|
||||
collision_context::{CollisionContext, CollisionEntity, CollisionItem},
|
||||
entity_obstruction::EntityIdentity,
|
||||
};
|
||||
CollisionContext::for_entity(
|
||||
CollisionEntity {
|
||||
identity: EntityIdentity(u64::from(m.id)),
|
||||
// A teleport candidate is queried before setPos changes the entity.
|
||||
y: m.position.y,
|
||||
descending: false,
|
||||
main_hand: Some(CollisionItem {
|
||||
item: m.extra.main_hand.as_deref().unwrap_or("minecraft:air"),
|
||||
count: m
|
||||
.extra
|
||||
.main_hand_stack()
|
||||
.map_or(i32::from(m.extra.main_hand.is_some()), |s| {
|
||||
s.count.min(i32::MAX as u32) as i32
|
||||
}),
|
||||
}),
|
||||
},
|
||||
false,
|
||||
)
|
||||
}
|
||||
fn scene(&self, m: &Mob) -> crate::collisions::SceneCollisions<'_, 'r> {
|
||||
crate::collisions::SceneCollisions {
|
||||
world: self.world,
|
||||
binding: self.binding,
|
||||
subject: Some(crate::collisions::PowderSnowSubject {
|
||||
fall_distance: m.fall_distance,
|
||||
leather_boots: m.extra.feet.as_deref() == Some("minecraft:leather_boots")
|
||||
&& m.extra.armor[3].as_ref().is_none_or(|s| s.count > 0),
|
||||
walkable_type: self.powder_walkable,
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
fn no_collision(&self, m: &Mob, bounds: Aabb) -> bool {
|
||||
use horizon_world::{
|
||||
entity_obstruction::EntityIdentity,
|
||||
world_movement::{CollisionCheckRequest, no_world_collision},
|
||||
};
|
||||
no_world_collision(
|
||||
&mut self.scene(m),
|
||||
CollisionCheckRequest {
|
||||
entity: EntityIdentity(u64::from(m.id)),
|
||||
context: Self::context(m),
|
||||
bounds,
|
||||
candidate_budget: 1_000_000,
|
||||
},
|
||||
)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
fn can_stay_at(&self, m: &Mob, p: [i32; 3], face: u8) -> bool {
|
||||
// isPositionBlocked: anything but air blocks, except a moving piston
|
||||
// being driven through the shulker's own cell.
|
||||
if !self.facts.is_air(self.world.block(p[0], p[1], p[2]))
|
||||
&& (p != block_pos(m)
|
||||
|| self
|
||||
.registry
|
||||
.state(self.world.block(p[0], p[1], p[2]))
|
||||
.map_or("", |s| s.name.as_str())
|
||||
!= "minecraft:moving_piston")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// loadedAndEntityCanStandOnFace uses the entity-context collision shape,
|
||||
// not the block's generic support shape (notably different on soul sand).
|
||||
let s = face_step(face);
|
||||
let support_pos =
|
||||
horizon_core::position::BlockPos::new(p[0] + s[0], p[1] + s[1], p[2] + s[2]);
|
||||
let support = self
|
||||
.world
|
||||
.block(support_pos.x, support_pos.y, support_pos.z);
|
||||
let open = opposite(face);
|
||||
let Ok(shape) = self.binding.collision_shape_with_context(
|
||||
&mut self.scene(m),
|
||||
support,
|
||||
support_pos,
|
||||
&Self::context(m),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let full = shape
|
||||
.face_shape(Direction::ALL[open as usize])
|
||||
.is_ok_and(|face| {
|
||||
VoxelShape::block()
|
||||
.join_is_not_empty(&face, ShapeOp::XOR)
|
||||
.is_ok_and(|differs| !differs)
|
||||
});
|
||||
full && self.no_collision(m, attachment_box(p, face))
|
||||
}
|
||||
}
|
||||
fn block_space<'w, 'r>(m: &Mob, w: &'w MobWorld<'r>) -> BlockSpace<'w, 'r> {
|
||||
BlockSpace {
|
||||
world: w.world,
|
||||
registry: w.registry,
|
||||
facts: w.facts,
|
||||
binding: w.collision,
|
||||
powder_walkable: w
|
||||
.data
|
||||
.entity_has("minecraft:powder_snow_walkable_mobs", &m.kind.id),
|
||||
}
|
||||
}
|
||||
fn entities_clear(m: &Mob, w: &MobWorld, bounds: Aabb) -> bool {
|
||||
let grown = bounds.inflate(Vec3::new(1.0e-7, 1.0e-7, 1.0e-7));
|
||||
!w.others_near(grown).any(|s| {
|
||||
s.id != m.id
|
||||
@@ -131,27 +233,12 @@ fn no_collision(m: &Mob, w: &MobWorld, bounds: Aabb) -> bool {
|
||||
&& s.bounds.intersects(grown)
|
||||
})
|
||||
}
|
||||
/// `Shulker.canStayAt`: an empty spot against a sturdy face, with room
|
||||
/// for the fully opened shell (`canStayAt`).
|
||||
fn can_stay_at(m: &Mob, w: &MobWorld, p: [i32; 3], face: u8) -> bool {
|
||||
// isPositionBlocked: anything but air blocks, except a moving piston
|
||||
// being driven through the shulker's own cell.
|
||||
if !w.facts.is_air(w.world.block(p[0], p[1], p[2]))
|
||||
&& (p != block_pos(m) || w.block_name(p[0], p[1], p[2]) != "minecraft:moving_piston")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// loadedAndEntityCanStandOnFace: the supporting block's face toward
|
||||
// the shell is sturdy.
|
||||
let s = face_step(face);
|
||||
let support = w.world.block(p[0] + s[0], p[1] + s[1], p[2] + s[2]);
|
||||
let open = opposite(face);
|
||||
if w.facts.get(support).sturdy_full & (1 << open) == 0 {
|
||||
return false;
|
||||
}
|
||||
// The fully opened shell's box, shrunk by the skin vanilla uses.
|
||||
let opened = progress_aabb(
|
||||
open,
|
||||
fn no_collision(m: &Mob, w: &MobWorld, bounds: Aabb) -> bool {
|
||||
block_space(m, w).no_collision(m, bounds) && entities_clear(m, w, bounds)
|
||||
}
|
||||
fn attachment_box(p: [i32; 3], face: u8) -> Aabb {
|
||||
progress_aabb(
|
||||
opposite(face),
|
||||
-1.0,
|
||||
1.0,
|
||||
Vec3::new(
|
||||
@@ -160,8 +247,11 @@ fn can_stay_at(m: &Mob, w: &MobWorld, p: [i32; 3], face: u8) -> bool {
|
||||
f64::from(p[2]) + 0.5,
|
||||
),
|
||||
)
|
||||
.deflate(Vec3::new(1.0e-6, 1.0e-6, 1.0e-6));
|
||||
no_collision(m, w, opened)
|
||||
.deflate(Vec3::new(1.0e-6, 1.0e-6, 1.0e-6))
|
||||
}
|
||||
/// Shulker.canStayAt, including its separately indexed collidable entities.
|
||||
fn can_stay_at(m: &Mob, w: &MobWorld, p: [i32; 3], face: u8) -> bool {
|
||||
block_space(m, w).can_stay_at(m, p, face) && entities_clear(m, w, attachment_box(p, face))
|
||||
}
|
||||
/// `findAttachableSurface`: the first face (down, up, north, south, west,
|
||||
/// east) to cling to.
|
||||
@@ -615,6 +705,10 @@ pub fn anim(m: &Mob, s: &State, p: &mut Poses) {
|
||||
p.yrot("head", (s.y_rot - 180.0).to_radians());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "shulker_space_tests.rs"]
|
||||
mod space_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -622,7 +716,7 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
/// A shulker fixed to a block face.
|
||||
fn shulker() -> Mob {
|
||||
pub(super) fn shulker() -> Mob {
|
||||
let mut kind = kind();
|
||||
let k = Arc::make_mut(&mut kind);
|
||||
k.id = "minecraft:shulker".into();
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
use super::*;
|
||||
use horizon_core::geometry::{Axis, VoxelShape};
|
||||
use horizon_world::{
|
||||
block_collision::BlockCollisionRegistry,
|
||||
collision::CachedCollisionRegistry,
|
||||
nbt::Tag,
|
||||
registry::{BlockRegistry, BlockStateId},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
fn text(value: &str) -> Tag {
|
||||
Tag::String(value.into())
|
||||
}
|
||||
fn shape_tag(shape: &VoxelShape) -> Tag {
|
||||
let sizes = shape.sizes();
|
||||
let mut bits = Vec::new();
|
||||
for z in 0..sizes[2] {
|
||||
for y in 0..sizes[1] {
|
||||
for x in 0..sizes[0] {
|
||||
bits.push(u8::from(shape.is_full(x as i32, y as i32, z as i32)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let (min, max) = shape.discrete_bounds();
|
||||
let mut fields = vec![
|
||||
("occupied", Tag::ByteArray(bits)),
|
||||
("cube", Tag::Byte(i8::from(shape.is_cube()))),
|
||||
(
|
||||
"cube_axes",
|
||||
Tag::Byte(
|
||||
shape
|
||||
.cube_coordinate_axes()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(0, |mask, (i, on)| mask | (i8::from(*on) << i)),
|
||||
),
|
||||
),
|
||||
("discrete_min", Tag::IntArray(min.to_vec())),
|
||||
("discrete_max", Tag::IntArray(max.to_vec())),
|
||||
];
|
||||
for (axis, name) in [(Axis::X, "x"), (Axis::Y, "y"), (Axis::Z, "z")] {
|
||||
fields.push((
|
||||
name,
|
||||
Tag::LongArray(
|
||||
shape
|
||||
.coordinates(axis)
|
||||
.iter()
|
||||
.map(|v| v.to_bits() as i64)
|
||||
.collect(),
|
||||
),
|
||||
));
|
||||
}
|
||||
Tag::compound(fields)
|
||||
}
|
||||
|
||||
fn fixture() -> (BlockRegistry, crate::blocks::BlockFacts, Tag) {
|
||||
let names = ["air", "soul_sand", "stone", "scaffolding", "oak_slab"];
|
||||
let states: Vec<_> = names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
let mut fields = vec![("id", text(&format!("minecraft:{name}")))];
|
||||
if *name == "scaffolding" {
|
||||
fields.push((
|
||||
"properties",
|
||||
Tag::compound([
|
||||
("distance", text("7")),
|
||||
("bottom", text("false")),
|
||||
("waterlogged", text("false")),
|
||||
]),
|
||||
));
|
||||
}
|
||||
Tag::compound(fields)
|
||||
})
|
||||
.collect();
|
||||
let registry_tag = Tag::compound([
|
||||
("DataVersion", Tag::Int(horizon_world::DATA_VERSION)),
|
||||
("states", Tag::List(states.clone())),
|
||||
(
|
||||
"defaults",
|
||||
Tag::Compound(
|
||||
names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, name)| {
|
||||
(
|
||||
format!("minecraft:{name}").as_str().into(),
|
||||
Tag::Int(i as i32),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
]);
|
||||
let registry = BlockRegistry::from_nbt(®istry_tag).unwrap();
|
||||
static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"horizon-shulker-space-{}-{}",
|
||||
std::process::id(),
|
||||
NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
));
|
||||
std::fs::create_dir(&root).unwrap();
|
||||
// Original minimal facts: air identity and generic sturdy flags, deliberately
|
||||
// distinct from the soul sand's lower collision shape.
|
||||
let facts = serde_json::json!({"states":(0..names.len()).map(|id|serde_json::json!({"id":id})).collect::<Vec<_>>()});
|
||||
std::fs::write(root.join("gameplay-facts.json"), facts.to_string()).unwrap();
|
||||
let behaviours = serde_json::json!({"states":[[0,0],[63,0],[63,0],[0,0],[1,0]],"blocks":names.iter().map(|name|serde_json::json!({"name":format!("minecraft:{name}"),"class":["Block"]})).collect::<Vec<_>>()});
|
||||
let facts =
|
||||
crate::blocks::BlockFacts::load_with_behaviour_json(&root, ®istry, &behaviours).unwrap();
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
let box_shape =
|
||||
|height| VoxelShape::from_box(Aabb::new(Vec3::ZERO, Vec3::new(1.0, height, 1.0))).unwrap();
|
||||
let shapes = [
|
||||
VoxelShape::empty(),
|
||||
box_shape(0.875),
|
||||
VoxelShape::block(),
|
||||
VoxelShape::block(),
|
||||
box_shape(0.5),
|
||||
];
|
||||
let table = Tag::compound([
|
||||
("DataVersion", Tag::Int(horizon_world::DATA_VERSION)),
|
||||
("states", Tag::List(states)),
|
||||
("shapes", Tag::List(shapes.iter().map(shape_tag).collect())),
|
||||
("cached_shapes", Tag::IntArray(vec![0, 1, 2, 3, 4])),
|
||||
]);
|
||||
(registry, facts, table)
|
||||
}
|
||||
|
||||
fn check(current: Vec3, cells: &[([i32; 3], u32)], target: [i32; 3], face: u8) -> bool {
|
||||
let (registry, facts, table) = fixture();
|
||||
let cached = CachedCollisionRegistry::from_nbt(&table, ®istry).unwrap();
|
||||
let binding = BlockCollisionRegistry::new(Arc::new(cached)).unwrap();
|
||||
let mut world = crate::world::World::new(BlockStateId(0), 0);
|
||||
for x in -1..=1 {
|
||||
for z in -1..=1 {
|
||||
world
|
||||
.chunks
|
||||
.insert((x, z), Arc::new(crate::chunk::Chunk::empty(x, z)));
|
||||
}
|
||||
}
|
||||
for &(p, state) in cells {
|
||||
world.set(p[0], p[1], p[2], BlockStateId(state)).unwrap();
|
||||
}
|
||||
let mut mob = super::tests::shulker();
|
||||
mob.position = current;
|
||||
BlockSpace {
|
||||
world: &world,
|
||||
registry: ®istry,
|
||||
facts: &facts,
|
||||
binding: &binding,
|
||||
powder_walkable: false,
|
||||
}
|
||||
.can_stay_at(&mob, target, face)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn soul_sand_only_accepts_attachment_to_its_bottom() {
|
||||
for (face, step) in STEPS.iter().enumerate() {
|
||||
let support = [step[0], 64 + step[1], step[2]];
|
||||
assert_eq!(
|
||||
check(
|
||||
Vec3::new(8.5, 64.0, 8.5),
|
||||
&[(support, 1)],
|
||||
[0, 64, 0],
|
||||
face as u8
|
||||
),
|
||||
face == 1,
|
||||
"face {face}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaffolding_uses_the_shulkers_current_height_before_and_after_teleport() {
|
||||
let cells = [([0, 63, 0], 3), ([0, 65, 0], 4)];
|
||||
assert!(check(Vec3::new(0.5, 62.0, 0.5), &cells, [0, 64, 0], 1));
|
||||
assert!(!check(Vec3::new(0.5, 64.0, 0.5), &cells, [0, 64, 0], 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "run by tools/test_shulker_space_parity.py"]
|
||||
fn shulker_space_parity_fixture() {
|
||||
use std::io::Write;
|
||||
let input =
|
||||
std::fs::read_to_string(std::env::var("HORIZON_SHULKER_SPACE_INPUT").unwrap()).unwrap();
|
||||
let mut output =
|
||||
std::fs::File::create(std::env::var("HORIZON_SHULKER_SPACE_OUTPUT").unwrap()).unwrap();
|
||||
for line in input.lines() {
|
||||
let row: serde_json::Value = serde_json::from_str(line).unwrap();
|
||||
let current = &row["current"];
|
||||
let current = Vec3::new(
|
||||
current[0].as_f64().unwrap(),
|
||||
current[1].as_f64().unwrap(),
|
||||
current[2].as_f64().unwrap(),
|
||||
);
|
||||
let target = std::array::from_fn(|i| row["target"][i].as_i64().unwrap() as i32);
|
||||
let cells: Vec<_> = row["cells"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|cell| {
|
||||
let pos = std::array::from_fn(|i| cell[i].as_i64().unwrap() as i32);
|
||||
let state = match cell[3].as_str().unwrap() {
|
||||
"minecraft:air" => 0,
|
||||
"minecraft:soul_sand" => 1,
|
||||
"minecraft:stone" => 2,
|
||||
"minecraft:scaffolding" => 3,
|
||||
"minecraft:oak_slab" => 4,
|
||||
_ => panic!("unknown fixture block"),
|
||||
};
|
||||
(pos, state)
|
||||
})
|
||||
.collect();
|
||||
writeln!(
|
||||
output,
|
||||
"{}",
|
||||
check(current, &cells, target, row["face"].as_u64().unwrap() as u8)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import com.google.gson.JsonParser;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.entity.EntityTypes;
|
||||
import net.minecraft.world.entity.monster.Shulker;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Original oracle: the native shulker attachment predicate on a recording level. */
|
||||
public final class VanillaShulkerSpaceOracle {
|
||||
public static void main(String[] args) throws Exception {
|
||||
ParityData.init();
|
||||
var allocator = Class.forName("sun.misc.Unsafe");
|
||||
var field = allocator.getDeclaredField("theUnsafe");
|
||||
field.setAccessible(true);
|
||||
VanillaBlockUpdateOracle.unsafe = field.get(null);
|
||||
VanillaBlockUpdateOracle.allocate = allocator.getMethod("allocateInstance", Class.class);
|
||||
VanillaBlockUpdateOracle.AIR = Blocks.AIR.defaultBlockState();
|
||||
VanillaBlockUpdateOracle.OVERWORLD_TYPE = VanillaBlockUpdateOracle.loadOverworld();
|
||||
var stay = Shulker.class.getDeclaredMethod("canStayAt", BlockPos.class, Direction.class);
|
||||
stay.setAccessible(true);
|
||||
try (var out = new PrintWriter(Files.newBufferedWriter(Path.of(args[1]), StandardCharsets.UTF_8))) {
|
||||
for (var line : Files.readAllLines(Path.of(args[0]))) {
|
||||
var input = JsonParser.parseString(line).getAsJsonObject();
|
||||
// Resolve names through actual vanilla states; the shared world takes numeric ids.
|
||||
var cells = input.getAsJsonArray("cells");
|
||||
for (var cell : cells) {
|
||||
var row = cell.getAsJsonArray();
|
||||
var name = row.get(3).getAsString();
|
||||
var state = net.minecraft.core.registries.BuiltInRegistries.BLOCK.getValue(
|
||||
net.minecraft.resources.Identifier.parse(name)).defaultBlockState();
|
||||
row.set(3, new com.google.gson.JsonPrimitive(Block.getId(state)));
|
||||
}
|
||||
var world = VanillaBlockUpdateOracle.build(input);
|
||||
world.registries = ParityData.access;
|
||||
var shulker = new Shulker(EntityTypes.SHULKER, world);
|
||||
var current = input.getAsJsonArray("current");
|
||||
shulker.setPos(current.get(0).getAsDouble(), current.get(1).getAsDouble(), current.get(2).getAsDouble());
|
||||
var target = VanillaBlockUpdateOracle.readPos(input.getAsJsonArray("target"));
|
||||
var direction = Direction.from3DDataValue(input.get("face").getAsInt());
|
||||
out.println(stay.invoke(shulker, target, direction));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare native shulker attachment queries with Horizon's production block queries."""
|
||||
import os
|
||||
from pathlib import Path
|
||||
import json
|
||||
import tempfile
|
||||
try:
|
||||
from . import lab, oracle_harness as harness, test_rust_parity as base, integration_env
|
||||
except ImportError:
|
||||
import lab, oracle_harness as harness, test_rust_parity as base, integration_env
|
||||
|
||||
|
||||
def cases():
|
||||
steps = [(0,-1,0),(0,1,0),(0,0,-1),(0,0,1),(-1,0,0),(1,0,0)]
|
||||
for x,z in [(0,0),(15,15),(-1,-1)]:
|
||||
target=[x,64,z]
|
||||
for name in ['air','soul_sand','stone','scaffolding','oak_slab']:
|
||||
for face,(dx,dy,dz) in enumerate(steps):
|
||||
yield json.dumps(dict(seed=42,cells=[[x+dx,64+dy,z+dz,'minecraft:'+name]],
|
||||
current=[8.5,64,8.5],target=target,face=face))
|
||||
for y in range(60,69):
|
||||
yield json.dumps(dict(seed=42,cells=[[x,63,z,'minecraft:scaffolding'],[x,65,z,'minecraft:oak_slab']],
|
||||
current=[x+.5,y,z+.5],target=target,face=1))
|
||||
for face,(dx,dy,dz) in enumerate(steps):
|
||||
yield json.dumps(dict(seed=42,cells=[[x+dx,64+dy,z+dz,'minecraft:stone'],[x,64,z,'minecraft:stone']],
|
||||
current=[8.5,64,8.5],target=target,face=face))
|
||||
|
||||
|
||||
def main():
|
||||
lock, directory = lab.config()
|
||||
manifest, client = lab.import_client(lock, directory)
|
||||
natives = {v['downloads']['artifact']['path'] for v in manifest['libraries']
|
||||
if v['name'].split(':')[-1].startswith('natives-')}
|
||||
libraries = [lab.import_library(a, directory / 'libraries/client')
|
||||
for a in lab.client_library_artifacts(manifest) if a['path'] not in natives]
|
||||
build = lab.LOCAL / 'rust-shulker-space-parity'
|
||||
build.mkdir(parents=True, exist_ok=True)
|
||||
report = build / 'report.json'
|
||||
report.unlink(missing_ok=True)
|
||||
sources = [Path(__file__), lab.ROOT / 'tools/lab.py', lab.ROOT / 'tools/oracle_harness.py',
|
||||
lab.ROOT / 'tools/test_rust_parity.py', lab.ROOT / 'tools/integration_env.py',
|
||||
lab.ROOT / 'Cargo.toml', lab.ROOT / 'Cargo.lock',
|
||||
lab.ROOT / 'crates/horizon-client/src/mobs/shulker.rs',
|
||||
lab.ROOT / 'crates/horizon-client/src/mobs/shulker_space_tests.rs',
|
||||
lab.ROOT / 'crates/horizon-client-render/src/collisions.rs',
|
||||
lab.ROOT / 'crates/horizon-world/src/block_collision.rs',
|
||||
lab.ROOT / 'crates/horizon-world/src/collision_context.rs',
|
||||
lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaShulkerSpaceOracle.java',
|
||||
lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaBlockUpdateOracle.java',
|
||||
lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/ParityData.java']
|
||||
hashes = base.source_hashes(sources)
|
||||
rows = list(cases())
|
||||
inputs = build / 'inputs.txt'
|
||||
inputs.write_text('\n'.join(rows) + '\n', encoding='utf-8')
|
||||
input_hash = lab.digest(inputs, 'sha256')
|
||||
classes, dependencies, _ = harness.compile_oracle(build, 'VanillaShulkerSpaceOracle',
|
||||
{'client': client}, libraries, lock['minecraft']['java_major'])
|
||||
harness.java_run(build, classes, dependencies, 'VanillaShulkerSpaceOracle',
|
||||
[inputs, build / 'client.txt'], 'client')
|
||||
with tempfile.TemporaryDirectory(prefix='horizon-shulker-space-parity-') as scratch:
|
||||
env = integration_env.game_environment(os.environ, scratch)
|
||||
env.update(HORIZON_SHULKER_SPACE_INPUT=str(inputs), HORIZON_SHULKER_SPACE_OUTPUT=str(build / 'rust.txt'))
|
||||
harness.run_logged(['cargo', 'test', '--locked', '-p', 'horizon-client-lib', '--lib',
|
||||
'mobs::shulker::space_tests::shulker_space_parity_fixture',
|
||||
'--', '--exact', '--ignored'], build, 'rust', env=env)
|
||||
matched = harness.compare(build / 'client.txt', build / 'rust.txt', len(rows))
|
||||
base.ensure_sources_unchanged(sources, hashes)
|
||||
if lab.digest(inputs, 'sha256') != input_hash:
|
||||
raise AssertionError('Input fixtures changed')
|
||||
lab.write_json(report, dict(schema=1, minecraft=lock['minecraft']['version'], sources_sha256=hashes,
|
||||
client_jar_sha256=lab.digest(client, 'sha256'), input_sha256=input_hash,
|
||||
matched_output_lines=matched, output_sha256=lab.digest(build / 'rust.txt', 'sha256'),
|
||||
scope='Actual native Shulker.canStayAt, loadedAndEntityCanStandOnFace and noCollision on a recording level, '
|
||||
'compared with the production Horizon block query used by attachment and teleport. '
|
||||
'Five support blocks,all six faces,blocked targets,nine current heights and three chunk-boundary positions. '
|
||||
'Rust uses original minimal facts/collision fixtures; normal scale and empty equipment. '
|
||||
'No nearby entities,world border,random teleport destination selection,AI ticks,networking or rendered interpolation comparison.'))
|
||||
print(f'{matched} shulker attachment cases match the pinned client: {report}', flush=True)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user