mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Merge worktree-agent-abc3c79c07e209fba
This commit is contained in:
commit
40802dd9af
9 files changed
+1022
-25
No files matched your search
@@ -509,7 +509,16 @@ impl Items {
|
||||
return vec![];
|
||||
};
|
||||
let name = block.name.trim_start_matches("minecraft:");
|
||||
let Some(table) = self.loot.get(&format!("minecraft:blocks/{name}")) else {
|
||||
// `Blocks.wallVariant`: wall signs, banners, heads, fans and torches
|
||||
// reuse the standing block's loot table.
|
||||
let standing = name.replacen("_wall_", "_", 1);
|
||||
let standing = standing.strip_prefix("wall_").unwrap_or(&standing);
|
||||
let standing = standing.replacen("wall_", "", 1);
|
||||
let table = self
|
||||
.loot
|
||||
.get(&format!("minecraft:blocks/{name}"))
|
||||
.or_else(|| self.loot.get(&format!("minecraft:blocks/{standing}")));
|
||||
let Some(table) = table else {
|
||||
return vec![];
|
||||
};
|
||||
let ctx = LootContext {
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
//! Rust side of the block update, random tick and redstone parity harnesses
|
||||
//! (`tools/test_block_update_parity.py`, `test_random_tick_parity.py`,
|
||||
//! `test_redstone_parity.py`): replays `VanillaBlockUpdateOracle` cases against
|
||||
//! `Behaviours` over a map-backed level and prints the same result text.
|
||||
use horizon_core::{
|
||||
geometry::{Aabb, Direction},
|
||||
position::{BlockPos, ChunkPos},
|
||||
random::{LegacyRandom, RandomSource},
|
||||
};
|
||||
use horizon_world::{
|
||||
level::{
|
||||
Behaviours, Dimension, EntityQuery, GameRules, LevelAccess, LevelEvent, Ticks,
|
||||
data::load_registry,
|
||||
},
|
||||
registry::BlockStateId,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
fmt::Write as _,
|
||||
io::{BufRead, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
struct Cells {
|
||||
cells: HashMap<BlockPos, BlockStateId>,
|
||||
air: BlockStateId,
|
||||
random: LegacyRandom,
|
||||
time: i64,
|
||||
sky: i32,
|
||||
block: i32,
|
||||
raining: bool,
|
||||
thundering: bool,
|
||||
events: Vec<LevelEvent>,
|
||||
}
|
||||
impl LevelAccess for Cells {
|
||||
fn block_state(&self, pos: BlockPos) -> BlockStateId {
|
||||
self.cells.get(&pos).copied().unwrap_or(self.air)
|
||||
}
|
||||
fn set_raw(&mut self, pos: BlockPos, state: BlockStateId) -> Option<BlockStateId> {
|
||||
let old = self.block_state(pos);
|
||||
if state == self.air {
|
||||
self.cells.remove(&pos);
|
||||
} else {
|
||||
self.cells.insert(pos, state);
|
||||
}
|
||||
Some(old)
|
||||
}
|
||||
fn is_loaded(&self, _pos: BlockPos) -> bool {
|
||||
true
|
||||
}
|
||||
fn random(&mut self) -> &mut LegacyRandom {
|
||||
&mut self.random
|
||||
}
|
||||
fn game_time(&self) -> i64 {
|
||||
self.time
|
||||
}
|
||||
fn block_light(&self, _pos: BlockPos) -> i32 {
|
||||
self.block
|
||||
}
|
||||
fn sky_light(&self, _pos: BlockPos) -> i32 {
|
||||
self.sky
|
||||
}
|
||||
fn is_raining(&self) -> bool {
|
||||
self.raining
|
||||
}
|
||||
fn is_thundering(&self) -> bool {
|
||||
self.thundering
|
||||
}
|
||||
fn is_raining_at(&self, _pos: BlockPos) -> bool {
|
||||
self.raining
|
||||
}
|
||||
fn dimension(&self) -> Dimension {
|
||||
Dimension::Overworld
|
||||
}
|
||||
fn game_rules(&self) -> GameRules {
|
||||
GameRules::default()
|
||||
}
|
||||
fn count_entities(&self, _area: Aabb, _query: EntityQuery) -> usize {
|
||||
0
|
||||
}
|
||||
fn emit(&mut self, event: LevelEvent) {
|
||||
self.events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
fn pos_of(value: &Value) -> BlockPos {
|
||||
let c: Vec<i32> = value
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|v| v.as_i64().unwrap() as i32)
|
||||
.collect();
|
||||
BlockPos::new(c[0], c[1], c[2])
|
||||
}
|
||||
|
||||
fn p(pos: BlockPos) -> String {
|
||||
format!("{},{},{}", pos.x, pos.y, pos.z)
|
||||
}
|
||||
|
||||
fn summary(level: &Cells, before: &HashMap<BlockPos, BlockStateId>, out: &mut String) {
|
||||
let mut all: BTreeMap<(i32, i32, i32), ()> = BTreeMap::new();
|
||||
for pos in before.keys().chain(level.cells.keys()) {
|
||||
all.insert((pos.x, pos.y, pos.z), ());
|
||||
}
|
||||
for (x, y, z) in all.keys() {
|
||||
let pos = BlockPos::new(*x, *y, *z);
|
||||
let a = before.get(&pos).copied().unwrap_or(level.air);
|
||||
let b = level.block_state(pos);
|
||||
if a != b {
|
||||
let _ = writeln!(out, "set {} {}", p(pos), b.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let resources = args.next().expect("resource directory");
|
||||
let cases = args.next().expect("case file");
|
||||
let output = args.next().expect("output file");
|
||||
let resources = Path::new(&resources);
|
||||
let registry = load_registry(resources)?;
|
||||
let behaviours = Behaviours::load(®istry, resources)?;
|
||||
let air = registry.default_state("minecraft:air").unwrap();
|
||||
let mut out = std::io::BufWriter::new(std::fs::File::create(output)?);
|
||||
for (index, line) in std::io::BufReader::new(std::fs::File::open(cases)?)
|
||||
.lines()
|
||||
.enumerate()
|
||||
{
|
||||
let case: Value = serde_json::from_str(&line?)?;
|
||||
let mut cells = HashMap::new();
|
||||
for cell in case["cells"].as_array().unwrap() {
|
||||
let c = cell.as_array().unwrap();
|
||||
let state = BlockStateId(c[3].as_i64().unwrap() as u32);
|
||||
if state != air {
|
||||
cells.insert(
|
||||
BlockPos::new(
|
||||
c[0].as_i64().unwrap() as i32,
|
||||
c[1].as_i64().unwrap() as i32,
|
||||
c[2].as_i64().unwrap() as i32,
|
||||
),
|
||||
state,
|
||||
);
|
||||
}
|
||||
}
|
||||
let before = cells.clone();
|
||||
let light = case.get("light").and_then(|v| v.as_array());
|
||||
let mut level = Cells {
|
||||
cells,
|
||||
air,
|
||||
random: LegacyRandom::new(case["seed"].as_i64().unwrap()),
|
||||
time: case.get("time").and_then(|v| v.as_i64()).unwrap_or(0),
|
||||
sky: light.map_or(15, |l| l[0].as_i64().unwrap() as i32),
|
||||
block: light.map_or(0, |l| l[1].as_i64().unwrap() as i32),
|
||||
raining: case
|
||||
.get("raining")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false),
|
||||
thundering: case
|
||||
.get("thundering")
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(false),
|
||||
events: Vec::new(),
|
||||
};
|
||||
let mut ticks = Ticks::new(0);
|
||||
for x in -3..=3 {
|
||||
for z in -3..=3 {
|
||||
ticks.add_empty_chunk(ChunkPos::new(x, z))?;
|
||||
}
|
||||
}
|
||||
let mut result = format!("case {index}\n");
|
||||
let origin = pos_of(&case["pos"]);
|
||||
match case["mode"].as_str().unwrap() {
|
||||
"shape" => {
|
||||
let dir = Direction::ALL[case["dir"].as_u64().unwrap() as usize];
|
||||
let neighbour = BlockStateId(case["neighbor"].as_i64().unwrap() as u32);
|
||||
let state = level.block_state(origin);
|
||||
let s = behaviours.update_shape(
|
||||
&mut level,
|
||||
&mut ticks,
|
||||
state,
|
||||
origin,
|
||||
dir,
|
||||
{
|
||||
let [x, y, z] = dir.normal();
|
||||
origin.offset(x, y, z)
|
||||
},
|
||||
neighbour,
|
||||
);
|
||||
let _ = writeln!(result, "shape {}", s.0);
|
||||
}
|
||||
"random" => behaviours.random_tick(&mut level, &mut ticks, origin),
|
||||
"tick" => behaviours.scheduled_block_tick(&mut level, &mut ticks, origin),
|
||||
"ftick" => behaviours.scheduled_fluid_tick(&mut level, &mut ticks, origin),
|
||||
"neighbor" => {
|
||||
let from = pos_of(&case["from"]);
|
||||
let changed = behaviours.block(level.block_state(from));
|
||||
behaviours.handle_neighbor_changed(&mut level, &mut ticks, origin, changed);
|
||||
}
|
||||
"run" => {
|
||||
for op in case["ops"].as_array().unwrap() {
|
||||
match op["op"].as_str().unwrap() {
|
||||
"set" => {
|
||||
behaviours.set_block(
|
||||
&mut level,
|
||||
&mut ticks,
|
||||
pos_of(&op["pos"]),
|
||||
BlockStateId(op["state"].as_i64().unwrap() as u32),
|
||||
op["flags"].as_i64().unwrap() as i32,
|
||||
);
|
||||
}
|
||||
"ticks" => {
|
||||
let snap = op.get("snap").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||
for _ in 0..op["n"].as_i64().unwrap() {
|
||||
level.time += 1;
|
||||
behaviours.tick_scheduled(&mut level, &mut ticks);
|
||||
if snap {
|
||||
let _ = writeln!(result, "tick {}", level.time);
|
||||
summary(&level, &before, &mut result);
|
||||
}
|
||||
}
|
||||
}
|
||||
other => panic!("unknown op {other}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
other => panic!("unknown mode {other}"),
|
||||
}
|
||||
summary(&level, &before, &mut result);
|
||||
// Pending scheduled ticks, sorted as the oracle sorts its rows.
|
||||
let mut rows: Vec<String> = Vec::new();
|
||||
for x in -3..=3 {
|
||||
for z in -3..=3 {
|
||||
let chunk = ChunkPos::new(x, z);
|
||||
if let Some(c) = ticks.blocks.chunk(chunk) {
|
||||
for t in c.queued() {
|
||||
let block = horizon_world::level::BlockId(t.kind.0 as u16);
|
||||
rows.push(format!(
|
||||
"bt {} {} {} {}",
|
||||
p(t.pos),
|
||||
behaviours.table.info(block).name,
|
||||
t.trigger_tick - level.time,
|
||||
t.priority as i32
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(c) = ticks.fluids.chunk(chunk) {
|
||||
for t in c.queued() {
|
||||
let name = ["empty", "flowing_water", "water", "flowing_lava", "lava"]
|
||||
[t.kind.0 as usize];
|
||||
rows.push(format!(
|
||||
"ft {} minecraft:{} {} {}",
|
||||
p(t.pos),
|
||||
name,
|
||||
t.trigger_tick - level.time,
|
||||
t.priority as i32
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rows.sort();
|
||||
for r in rows {
|
||||
result.push_str(&r);
|
||||
result.push('\n');
|
||||
}
|
||||
for event in &level.events {
|
||||
match event {
|
||||
LevelEvent::LevelEvent { id, pos, data } => {
|
||||
let _ = writeln!(result, "L {} {} {}", id, p(*pos), data);
|
||||
}
|
||||
LevelEvent::Sound { pos, sound, .. } => {
|
||||
let _ = writeln!(result, "S {} {}", p(*pos), sound);
|
||||
}
|
||||
LevelEvent::DropResources { state, .. } => {
|
||||
let name = behaviours.table.name(*state);
|
||||
let short = name.strip_prefix("minecraft:").unwrap_or(name);
|
||||
// `Blocks.wallVariant` reuses the standing block's loot table.
|
||||
let short = short.replacen("_wall_", "_", 1);
|
||||
let short = short.strip_prefix("wall_").unwrap_or(&short);
|
||||
let short = short.replacen("wall_", "", 1);
|
||||
let _ = writeln!(result, "D minecraft:blocks/{short}");
|
||||
}
|
||||
LevelEvent::FallingBlock { .. } => {
|
||||
let _ = writeln!(result, "E minecraft:falling_block");
|
||||
}
|
||||
LevelEvent::PrimeTnt { .. } => {
|
||||
let _ = writeln!(result, "E minecraft:tnt");
|
||||
}
|
||||
LevelEvent::GameEvent { pos, event, .. } => {
|
||||
let _ = writeln!(result, "G {} {}", p(*pos), event);
|
||||
}
|
||||
LevelEvent::BlockEvent { kind, .. } => {
|
||||
let _ = writeln!(result, "X {kind}");
|
||||
}
|
||||
LevelEvent::GrowTree { .. } => {
|
||||
let _ = writeln!(result, "X grow_tree");
|
||||
}
|
||||
LevelEvent::PlaceFeature { feature, .. } => {
|
||||
let _ = writeln!(result, "X feature {feature}");
|
||||
}
|
||||
LevelEvent::Player { .. } => {
|
||||
let _ = writeln!(result, "X player");
|
||||
}
|
||||
}
|
||||
}
|
||||
for e in &ticks.block_events {
|
||||
let _ = writeln!(
|
||||
result,
|
||||
"B {} {} {} {}",
|
||||
p(e.pos),
|
||||
behaviours.table.info(e.block).name,
|
||||
e.param_a,
|
||||
e.param_b
|
||||
);
|
||||
}
|
||||
let _ = writeln!(result, "rng {}", level.random.next_long());
|
||||
out.write_all(result.as_bytes())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -568,6 +568,19 @@ impl Behaviours {
|
||||
) {
|
||||
self.sim(level, ticks).update_neighbors_at(pos, block);
|
||||
}
|
||||
/// `BlockState.handleNeighborChanged(level, pos, changedBlock, orientation, movedByPiston)`
|
||||
/// for the state currently at `pos`.
|
||||
pub fn handle_neighbor_changed<L: LevelAccess + ?Sized>(
|
||||
&self,
|
||||
level: &mut L,
|
||||
ticks: &mut Ticks,
|
||||
pos: BlockPos,
|
||||
changed: BlockId,
|
||||
) {
|
||||
let mut sim = self.sim(level, ticks);
|
||||
let state = sim.state(pos);
|
||||
sim.neighbor_changed(state, pos, changed, false);
|
||||
}
|
||||
/// Entity collision with a block (`entityInside`) for the entity-sensing
|
||||
/// blocks: pressure plates, tripwires and buttons hit by arrows are
|
||||
/// re-evaluated through [`LevelAccess::count_entities`].
|
||||
|
||||
@@ -111,6 +111,9 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
let data = self.b.data(state);
|
||||
if data.is(super::classes::Class::PistonBase) {
|
||||
self.piston_trigger_event(pos, state, a, b)
|
||||
} else if data.is(super::classes::Class::Note) {
|
||||
self.note_trigger_event(pos, state);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -1067,35 +1067,45 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
self.set_block(pos, b.table.set_bool(next, p.powered, signal), 3);
|
||||
}
|
||||
}
|
||||
/// `NoteBlock.playNote` (block event; instrument pitch from `note`).
|
||||
/// `NoteBlock.playNote`: queues the block event and emits the game event.
|
||||
pub(crate) fn note_play(&mut self, pos: BlockPos, state: BlockStateId) {
|
||||
let b = self.b;
|
||||
let instrument = b.table.text(state, b.props.instrument);
|
||||
let mob_head = super::data::INSTRUMENTS
|
||||
.iter()
|
||||
.position(|n| *n == instrument)
|
||||
.is_some_and(|i| super::data::instrument_works_above(i as u8));
|
||||
if mob_head || b.is_air(self.above_state(pos)) {
|
||||
self.block_event(pos, b.block(state), 0, 0);
|
||||
self.game_event(pos, "minecraft:note_block_play");
|
||||
}
|
||||
}
|
||||
/// `NoteBlock.triggerEvent`: the note's sound.
|
||||
pub(crate) fn note_trigger_event(&mut self, pos: BlockPos, state: BlockStateId) {
|
||||
let b = self.b;
|
||||
let instrument = b.table.text(state, b.props.instrument).to_owned();
|
||||
let mob_head = super::data::INSTRUMENTS
|
||||
.iter()
|
||||
.position(|n| *n == instrument)
|
||||
.is_some_and(|i| super::data::instrument_works_above(i as u8));
|
||||
if mob_head || b.is_air(self.above_state(pos)) {
|
||||
let note = b.table.int(state, b.props.note);
|
||||
let pitch = if mob_head {
|
||||
1.0
|
||||
} else {
|
||||
2f64.powf(f64::from(note - 12) / 12.0) as f32
|
||||
};
|
||||
let sound = match instrument.as_str() {
|
||||
"custom_head" => "minecraft:ui.button.click".to_owned(),
|
||||
"dragon" => "minecraft:block.note_block.imitate.ender_dragon".to_owned(),
|
||||
name if mob_head => format!("minecraft:block.note_block.imitate.{name}"),
|
||||
name => format!("minecraft:block.note_block.{name}"),
|
||||
};
|
||||
self.emit(LevelEvent::Sound {
|
||||
pos,
|
||||
sound: sound.into(),
|
||||
volume: 3.0,
|
||||
pitch,
|
||||
});
|
||||
self.game_event(pos, "minecraft:note_block_play");
|
||||
}
|
||||
let note = b.table.int(state, b.props.note);
|
||||
let pitch = if mob_head {
|
||||
1.0
|
||||
} else {
|
||||
2f64.powf(f64::from(note - 12) / 12.0) as f32
|
||||
};
|
||||
let sound = match instrument.as_str() {
|
||||
"custom_head" => "minecraft:ui.button.click".to_owned(),
|
||||
"dragon" => "minecraft:block.note_block.imitate.ender_dragon".to_owned(),
|
||||
name if mob_head => format!("minecraft:block.note_block.imitate.{name}"),
|
||||
name => format!("minecraft:block.note_block.{name}"),
|
||||
};
|
||||
self.emit(LevelEvent::Sound {
|
||||
pos,
|
||||
sound: sound.into(),
|
||||
volume: 3.0,
|
||||
pitch,
|
||||
});
|
||||
}
|
||||
pub(crate) fn note_neighbor_changed(&mut self, pos: BlockPos, state: BlockStateId) {
|
||||
let b = self.b;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! `Level.setBlock`, `LevelChunk.setBlockState` and the
|
||||
//! `CollectingNeighborUpdater` that orders neighbour and shape updates.
|
||||
use super::{LevelAccess, LevelEvent, Sim, flags, states::BlockId};
|
||||
use super::{LevelAccess, LevelEvent, Sim, classes::Class, flags, states::BlockId};
|
||||
use crate::{fluid::FluidState, registry::BlockStateId};
|
||||
use horizon_core::{
|
||||
geometry::Direction,
|
||||
@@ -338,7 +338,13 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
if self.b.is_air(state) {
|
||||
return false;
|
||||
}
|
||||
self.level_event(2001, pos, state.0 as i32);
|
||||
let is_fire = {
|
||||
let data = self.b.data(state);
|
||||
data.is(Class::BaseFire) || data.is(Class::Fire) || data.is(Class::SoulFire)
|
||||
};
|
||||
if !is_fire {
|
||||
self.level_event(2001, pos, state.0 as i32);
|
||||
}
|
||||
if drop {
|
||||
self.drop_resources(state, pos);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
package dev.horizonlab.parity;
|
||||
|
||||
import com.google.gson.*;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import net.minecraft.core.*;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.sounds.SoundEvent;
|
||||
import net.minecraft.sounds.SoundSource;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.flag.FeatureFlags;
|
||||
import net.minecraft.world.level.gamerules.GameRules;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.LevelChunk;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
import net.minecraft.world.level.material.Fluid;
|
||||
import net.minecraft.world.level.material.FluidState;
|
||||
import net.minecraft.world.phys.AABB;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
import net.minecraft.world.ticks.LevelTicks;
|
||||
|
||||
/**
|
||||
* Drives vanilla block behaviour (updateShape, randomTick, tick, neighbour
|
||||
* updates, setBlock cascades and multi-tick runs) over a map-backed headless
|
||||
* ServerLevel. Shared by tools/test_block_update_parity.py,
|
||||
* test_random_tick_parity.py and test_redstone_parity.py. Block entities do not
|
||||
* exist here; entities never exist; every side effect is appended to an event
|
||||
* log compared against the Rust port.
|
||||
*/
|
||||
public final class VanillaBlockUpdateOracle {
|
||||
static Object unsafe;
|
||||
static Method allocate;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> T make(Class<T> type) {
|
||||
try {
|
||||
return (T) allocate.invoke(unsafe, type);
|
||||
} catch (Exception e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
static void set(Class<?> owner, Object target, String name, Object value) throws Exception {
|
||||
Field f = owner.getDeclaredField(name);
|
||||
f.setAccessible(true);
|
||||
f.set(target, value);
|
||||
}
|
||||
|
||||
static BlockState AIR;
|
||||
|
||||
static String pos(BlockPos p) {
|
||||
return p.getX() + "," + p.getY() + "," + p.getZ();
|
||||
}
|
||||
|
||||
static final class Chunk extends LevelChunk {
|
||||
World world;
|
||||
Chunk() {
|
||||
super(null, null);
|
||||
}
|
||||
@Override
|
||||
public BlockState setBlockState(BlockPos pos, BlockState state, int flags) {
|
||||
BlockState old = world.cells.getOrDefault(pos.immutable(), AIR);
|
||||
if (old == state) {
|
||||
return null;
|
||||
}
|
||||
if (state.isAir()) {
|
||||
world.cells.remove(pos);
|
||||
} else {
|
||||
world.cells.put(pos.immutable(), state);
|
||||
}
|
||||
Block newBlock = state.getBlock();
|
||||
boolean blockChanged = !old.is(newBlock);
|
||||
boolean movedByPiston = (flags & 64) != 0;
|
||||
if ((blockChanged || newBlock instanceof net.minecraft.world.level.block.BaseRailBlock) && ((flags & 1) != 0 || movedByPiston)) {
|
||||
old.affectNeighborsAfterRemoval(world, pos, movedByPiston);
|
||||
}
|
||||
if (!world.getBlockState(pos).is(newBlock)) {
|
||||
return null;
|
||||
}
|
||||
if ((flags & 512) == 0) {
|
||||
state.onPlace(world, pos, old, movedByPiston);
|
||||
}
|
||||
return old;
|
||||
}
|
||||
@Override
|
||||
public net.minecraft.world.level.chunk.status.ChunkStatus getPersistedStatus() {
|
||||
return net.minecraft.world.level.chunk.status.ChunkStatus.FULL;
|
||||
}
|
||||
@Override
|
||||
public net.minecraft.server.level.FullChunkStatus getFullStatus() {
|
||||
return net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING;
|
||||
}
|
||||
}
|
||||
|
||||
static final class World extends net.minecraft.server.level.ServerLevel {
|
||||
Map<BlockPos, BlockState> cells = new HashMap<>();
|
||||
RandomSource rng;
|
||||
long time;
|
||||
long subTick;
|
||||
Chunk chunk;
|
||||
LevelTicks<Block> blockTicks;
|
||||
LevelTicks<Fluid> fluidTicks;
|
||||
StringBuilder log = new StringBuilder();
|
||||
int skyLight = 15;
|
||||
int blockLight = 0;
|
||||
GameRules rules;
|
||||
Server server;
|
||||
boolean raining, thundering;
|
||||
World() {
|
||||
super(null, null, null, null, null, null, false, 0L, List.of(), false);
|
||||
}
|
||||
void emit(String line) {
|
||||
log.append(line).append('\n');
|
||||
}
|
||||
@Override public BlockState getBlockState(BlockPos p) { return cells.getOrDefault(p, AIR); }
|
||||
@Override public FluidState getFluidState(BlockPos p) { return getBlockState(p).getFluidState(); }
|
||||
@Override public BlockEntity getBlockEntity(BlockPos p) { return null; }
|
||||
@Override public LevelChunk getChunkAt(BlockPos p) { return chunk; }
|
||||
@Override public LevelChunk getChunk(int x, int z) { return chunk; }
|
||||
@Override public boolean isLoaded(BlockPos p) { return true; }
|
||||
@Override public boolean hasChunkAt(BlockPos p) { return true; }
|
||||
@Override public boolean hasChunksAt(int x0, int z0, int x1, int z1) { return true; }
|
||||
@Override public boolean hasChunksAt(int x0, int y0, int z0, int x1, int y1, int z1) { return true; }
|
||||
@Override public boolean hasChunk(int x, int z) { return true; }
|
||||
@Override public boolean isClientSide() { return false; }
|
||||
@Override public RandomSource getRandom() { return rng; }
|
||||
@Override public int getMinY() { return -64; }
|
||||
@Override public int getHeight() { return 384; }
|
||||
@Override public long getGameTime() { return time; }
|
||||
@Override public long nextSubTickCount() { return subTick++; }
|
||||
@Override public LevelTicks<Block> getBlockTicks() { return blockTicks; }
|
||||
@Override public LevelTicks<Fluid> getFluidTicks() { return fluidTicks; }
|
||||
@Override public void sendBlockUpdated(BlockPos p, BlockState a, BlockState b, int f) { }
|
||||
@Override public void setBlocksDirty(BlockPos p, BlockState a, BlockState b) { }
|
||||
@Override public net.minecraft.core.RegistryAccess registryAccess() { return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY); }
|
||||
@Override public net.minecraft.world.flag.FeatureFlagSet enabledFeatures() { return FeatureFlags.DEFAULT_FLAGS; }
|
||||
@Override public GameRules getGameRules() { return rules; }
|
||||
@Override public net.minecraft.server.MinecraftServer getServer() { return server; }
|
||||
@Override public net.minecraft.world.level.border.WorldBorder getWorldBorder() { return new net.minecraft.world.level.border.WorldBorder(); }
|
||||
@Override public int getSeaLevel() { return 63; }
|
||||
@Override public int getBrightness(net.minecraft.world.level.LightLayer layer, BlockPos p) { return layer == net.minecraft.world.level.LightLayer.SKY ? skyLight : blockLight; }
|
||||
@Override public int getRawBrightness(BlockPos p, int darkening) { return Math.max(skyLight - darkening, blockLight); }
|
||||
@Override public int getSkyDarken() { return 0; }
|
||||
@Override public boolean isRaining() { return raining; }
|
||||
@Override public boolean isThundering() { return thundering; }
|
||||
@Override public boolean isRainingAt(BlockPos p) { return raining; }
|
||||
@Override public boolean isUnobstructed(BlockState state, BlockPos pos, net.minecraft.world.phys.shapes.CollisionContext c) { return true; }
|
||||
@Override public boolean isUnobstructed(Entity entity) { return true; }
|
||||
@Override public List<Entity> getEntities(Entity except, AABB box, java.util.function.Predicate<? super Entity> filter) { return List.of(); }
|
||||
@Override public <T extends Entity> List<T> getEntitiesOfClass(Class<T> type, AABB box, java.util.function.Predicate<? super T> filter) { return List.of(); }
|
||||
@Override public List<net.minecraft.server.level.ServerPlayer> players() { return List.of(); }
|
||||
@Override public net.minecraft.world.entity.player.Player getNearestPlayer(double x, double y, double z, double d, boolean b) { return null; }
|
||||
@Override public boolean addFreshEntity(Entity entity) {
|
||||
emit("E " + BuiltInRegistries.ENTITY_TYPE.getKey(entity.getType()));
|
||||
return true;
|
||||
}
|
||||
@Override public void levelEvent(Entity source, int type, BlockPos p, int data) { emit("L " + type + " " + pos(p) + " " + data); }
|
||||
@Override public void playSeededSound(Entity except, double x, double y, double z, net.minecraft.core.Holder<SoundEvent> sound, SoundSource source, float volume, float pitch, long seed) {
|
||||
emit("S " + pos(BlockPos.containing(x, y, z)) + " " + sound.value().location());
|
||||
}
|
||||
@Override public void playSeededSound(Entity except, Entity src, net.minecraft.core.Holder<SoundEvent> sound, SoundSource source, float volume, float pitch, long seed) {
|
||||
emit("S entity " + sound.value().location());
|
||||
}
|
||||
@Override public void gameEvent(net.minecraft.core.Holder<net.minecraft.world.level.gameevent.GameEvent> e, Vec3 p, net.minecraft.world.level.gameevent.GameEvent.Context c) {
|
||||
emit("G " + pos(BlockPos.containing(p)) + " " + e.getRegisteredName());
|
||||
}
|
||||
@Override public <T extends net.minecraft.core.particles.ParticleOptions> int sendParticles(T p, double x, double y, double z, int n, double a, double b, double c, double s) { return 0; }
|
||||
@Override public void blockEvent(BlockPos p, Block block, int a, int b) { emit("B " + pos(p) + " " + BuiltInRegistries.BLOCK.getKey(block) + " " + a + " " + b); }
|
||||
@Override public void updatePOIOnBlockStateChange(BlockPos p, BlockState a, BlockState b) { }
|
||||
@Override public boolean isPositionEntityTicking(BlockPos p) { return true; }
|
||||
@Override public boolean shouldTickBlocksAt(BlockPos p) { return true; }
|
||||
@Override public boolean shouldTickBlocksAt(long p) { return true; }
|
||||
}
|
||||
|
||||
static final class Loot extends net.minecraft.server.ReloadableServerRegistries.Holder {
|
||||
World world;
|
||||
Loot() {
|
||||
super(null);
|
||||
}
|
||||
@Override
|
||||
public net.minecraft.world.level.storage.loot.LootTable getLootTable(net.minecraft.resources.ResourceKey<net.minecraft.world.level.storage.loot.LootTable> id) {
|
||||
world.emit("D " + id.identifier());
|
||||
return net.minecraft.world.level.storage.loot.LootTable.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
static final class Server extends net.minecraft.server.MinecraftServer {
|
||||
Loot loot;
|
||||
Server() {
|
||||
super(null, null, null, null, null, null, null, null, null, false, null);
|
||||
}
|
||||
@Override public net.minecraft.server.ReloadableServerRegistries.Holder reloadableRegistries() { return loot; }
|
||||
@Override protected boolean initServer() { return false; }
|
||||
@Override public int getMaxPlayers() { return 0; }
|
||||
@Override public net.minecraft.server.permissions.LevelBasedPermissionSet operatorUserPermissions() { return null; }
|
||||
@Override public net.minecraft.server.permissions.PermissionSet getFunctionCompilationPermissions() { return null; }
|
||||
@Override public boolean shouldRconBroadcast() { return false; }
|
||||
@Override protected net.minecraft.util.debugchart.SampleLogger getTickTimeLogger() { return null; }
|
||||
@Override public boolean isTickTimeLoggingEnabled() { return false; }
|
||||
@Override public net.minecraft.SystemReport fillServerSystemReport(net.minecraft.SystemReport r) { return r; }
|
||||
@Override public boolean isDedicatedServer() { return true; }
|
||||
@Override public int getRateLimitPacketsPerSecond() { return 0; }
|
||||
@Override public int getCommandSpamThresholdSeconds() { return 0; }
|
||||
@Override public int getChatSpamThresholdSeconds() { return 0; }
|
||||
@Override public boolean useNativeTransport() { return false; }
|
||||
@Override public boolean isPublished() { return false; }
|
||||
@Override public boolean shouldInformAdmins() { return false; }
|
||||
@Override public boolean isSingleplayerOwner(net.minecraft.server.players.NameAndId id) { return false; }
|
||||
}
|
||||
|
||||
static List<String> summary(World w, Map<BlockPos, BlockState> before) {
|
||||
List<String> out = new ArrayList<>();
|
||||
Set<BlockPos> all = new HashSet<>(before.keySet());
|
||||
all.addAll(w.cells.keySet());
|
||||
List<BlockPos> sorted = new ArrayList<>(all);
|
||||
sorted.sort(Comparator.<BlockPos>comparingInt(q -> q.getX()).thenComparingInt(q -> q.getY()).thenComparingInt(q -> q.getZ()));
|
||||
for (BlockPos p : sorted) {
|
||||
BlockState a = before.getOrDefault(p, AIR), b = w.cells.getOrDefault(p, AIR);
|
||||
if (a != b) {
|
||||
out.add("set " + pos(p) + " " + Block.getId(b));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static void pending(World w, StringBuilder out) throws Exception {
|
||||
for (var pair : List.of(new Object[]{"bt", w.blockTicks}, new Object[]{"ft", w.fluidTicks})) {
|
||||
Field f = LevelTicks.class.getDeclaredField("allContainers");
|
||||
f.setAccessible(true);
|
||||
var map = (it.unimi.dsi.fastutil.longs.Long2ObjectMap<net.minecraft.world.ticks.LevelChunkTicks<Object>>) f.get(pair[1]);
|
||||
List<String> rows = new ArrayList<>();
|
||||
for (var container : map.values()) {
|
||||
container.getAll().forEach(t -> {
|
||||
String type = t.type() instanceof Block b ? BuiltInRegistries.BLOCK.getKey(b).toString() : BuiltInRegistries.FLUID.getKey((Fluid) t.type()).toString();
|
||||
rows.add(pair[0] + " " + pos(t.pos()) + " " + type + " " + (t.triggerTick() - w.time) + " " + t.priority().getValue());
|
||||
});
|
||||
}
|
||||
Collections.sort(rows);
|
||||
for (String r : rows) out.append(r).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
net.minecraft.SharedConstants.tryDetectVersion();
|
||||
net.minecraft.server.Bootstrap.bootStrap();
|
||||
AIR = Blocks.AIR.defaultBlockState();
|
||||
Path resources = Path.of(args[0]);
|
||||
VanillaHeightmapOracle.bindTags(JsonParser.parseString(Files.readString(resources.resolve("block-tags.json"))).getAsJsonObject());
|
||||
VanillaLevelShapeOracle.bindFluidTags(resources.resolve("data/minecraft/tags/fluid"));
|
||||
var allocator = Class.forName("sun.misc.Unsafe");
|
||||
var field = allocator.getDeclaredField("theUnsafe");
|
||||
field.setAccessible(true);
|
||||
unsafe = field.get(null);
|
||||
allocate = allocator.getMethod("allocateInstance", Class.class);
|
||||
try (var out = new PrintWriter(Files.newBufferedWriter(Path.of(args[2]))); var lines = Files.lines(Path.of(args[1]))) {
|
||||
int index = 0;
|
||||
for (var line : (Iterable<String>) lines::iterator) {
|
||||
var f = JsonParser.parseString(line).getAsJsonObject();
|
||||
StringBuilder result = new StringBuilder("case " + index++ + "\n");
|
||||
try {
|
||||
run(f, result);
|
||||
} catch (Throwable e) {
|
||||
Throwable c = e;
|
||||
while (c.getCause() != null) c = c.getCause();
|
||||
StackTraceElement[] st = c.getStackTrace();
|
||||
result.append("error ").append(c.getClass().getSimpleName()).append(' ').append(st.length > 0 ? st[0].toString() : "").append('\n');
|
||||
}
|
||||
out.print(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static BlockPos readPos(JsonArray a) {
|
||||
return new BlockPos(a.get(0).getAsInt(), a.get(1).getAsInt(), a.get(2).getAsInt());
|
||||
}
|
||||
|
||||
static World build(JsonObject f) throws Exception {
|
||||
World w = make(World.class);
|
||||
w.cells = new HashMap<>();
|
||||
w.log = new StringBuilder();
|
||||
for (var cell : f.getAsJsonArray("cells")) {
|
||||
var c = cell.getAsJsonArray();
|
||||
BlockState s = Block.stateById(c.get(3).getAsInt());
|
||||
if (!s.isAir()) w.cells.put(new BlockPos(c.get(0).getAsInt(), c.get(1).getAsInt(), c.get(2).getAsInt()), s);
|
||||
}
|
||||
w.rng = new LegacyRandomSource(f.get("seed").getAsLong());
|
||||
w.time = f.has("time") ? f.get("time").getAsLong() : 0;
|
||||
w.rules = new GameRules(FeatureFlags.DEFAULT_FLAGS);
|
||||
if (f.has("light")) {
|
||||
w.skyLight = f.getAsJsonArray("light").get(0).getAsInt();
|
||||
w.blockLight = f.getAsJsonArray("light").get(1).getAsInt();
|
||||
}
|
||||
w.raining = f.has("raining") && f.get("raining").getAsBoolean();
|
||||
w.thundering = f.has("thundering") && f.get("thundering").getAsBoolean();
|
||||
set(Level.class, w, "random", w.rng);
|
||||
set(Level.class, w, "neighborUpdater", new net.minecraft.world.level.redstone.CollectingNeighborUpdater(w, 512));
|
||||
w.server = make(Server.class);
|
||||
w.server.loot = make(Loot.class);
|
||||
w.server.loot.world = w;
|
||||
w.chunk = make(Chunk.class);
|
||||
w.chunk.world = w;
|
||||
w.blockTicks = new LevelTicks<>(l -> true);
|
||||
w.fluidTicks = new LevelTicks<>(l -> true);
|
||||
for (int x = -3; x <= 3; x++) {
|
||||
for (int z = -3; z <= 3; z++) {
|
||||
w.blockTicks.addContainer(new net.minecraft.world.level.ChunkPos(x, z), new net.minecraft.world.ticks.LevelChunkTicks<>());
|
||||
w.fluidTicks.addContainer(new net.minecraft.world.level.ChunkPos(x, z), new net.minecraft.world.ticks.LevelChunkTicks<>());
|
||||
}
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
static void run(JsonObject f, StringBuilder result) throws Exception {
|
||||
World w = build(f);
|
||||
Map<BlockPos, BlockState> before = new HashMap<>(w.cells);
|
||||
String mode = f.get("mode").getAsString();
|
||||
BlockPos p = readPos(f.getAsJsonArray("pos"));
|
||||
BlockState state = w.getBlockState(p);
|
||||
switch (mode) {
|
||||
case "shape" -> {
|
||||
Direction d = Direction.from3DDataValue(f.get("dir").getAsInt());
|
||||
BlockPos np = p.relative(d);
|
||||
BlockState n = Block.stateById(f.get("neighbor").getAsInt());
|
||||
BlockState s = state.updateShape(w, w, p, d, np, n, w.rng);
|
||||
result.append("shape ").append(Block.getId(s)).append('\n');
|
||||
}
|
||||
case "random" -> {
|
||||
if (state.isRandomlyTicking()) state.randomTick(w, p, w.rng);
|
||||
FluidState fs = state.getFluidState();
|
||||
if (fs.isRandomlyTicking()) fs.randomTick(w, p, w.rng);
|
||||
}
|
||||
case "tick" -> state.tick(w, p, w.rng);
|
||||
case "ftick" -> state.getFluidState().tick(w, p, state);
|
||||
case "neighbor" -> {
|
||||
BlockPos from = readPos(f.getAsJsonArray("from"));
|
||||
w.getBlockState(from);
|
||||
state.handleNeighborChanged(w, p, w.getBlockState(from).getBlock(), null, false);
|
||||
}
|
||||
case "run" -> {
|
||||
for (var op : f.getAsJsonArray("ops")) {
|
||||
var o = op.getAsJsonObject();
|
||||
switch (o.get("op").getAsString()) {
|
||||
case "set" -> w.setBlock(readPos(o.getAsJsonArray("pos")), Block.stateById(o.get("state").getAsInt()), o.get("flags").getAsInt());
|
||||
case "ticks" -> {
|
||||
for (int i = 0; i < o.get("n").getAsInt(); i++) {
|
||||
w.time++;
|
||||
w.blockTicks.tick(w.time, 65536, (bp, b) -> { if (w.getBlockState(bp).is(b)) w.getBlockState(bp).tick(w, bp, w.rng); });
|
||||
w.fluidTicks.tick(w.time, 65536, (bp, fl) -> {
|
||||
if (w.getFluidState(bp).getType() == fl) w.getFluidState(bp).tick(w, bp, w.getBlockState(bp));
|
||||
});
|
||||
if (o.has("snap") && o.get("snap").getAsBoolean()) {
|
||||
result.append("tick ").append(w.time).append('\n');
|
||||
for (String s : summary(w, before)) result.append(s).append('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalStateException(o.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> throw new IllegalStateException(mode);
|
||||
}
|
||||
for (String s : summary(w, before)) result.append(s).append('\n');
|
||||
pending(w, result);
|
||||
result.append(w.log);
|
||||
result.append("rng ").append(w.rng.nextLong()).append('\n');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared driver of the block update / random tick / redstone differential harnesses.
|
||||
|
||||
Cases are JSON lines understood by `VanillaBlockUpdateOracle` (vanilla block code
|
||||
over a map-backed headless ServerLevel) and `block_update_fixture` (the Rust
|
||||
`Behaviours` over the same kind of level). Both print one result block per case
|
||||
(state changes, pending scheduled ticks, side-effect log, RNG fingerprint); this
|
||||
module generates cases, runs both sides and reports differing cases grouped by
|
||||
the block under test.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
try:
|
||||
from . import lab, test_rust_parity as base
|
||||
except ImportError:
|
||||
import lab
|
||||
import test_rust_parity as base
|
||||
|
||||
NEIGHBOUR_PALETTE = [
|
||||
'stone', 'dirt', 'grass_block', 'water', 'lava', 'sand', 'oak_log', 'oak_planks', 'farmland', 'snow', 'snow_block',
|
||||
'ice', 'packed_ice', 'soul_sand', 'netherrack', 'mycelium', 'podzol', 'gravel', 'glass', 'oak_leaves', 'fire',
|
||||
'redstone_block', 'redstone_wire', 'redstone_torch', 'torch', 'lever', 'iron_block', 'magma_block', 'bedrock',
|
||||
'cobblestone', 'obsidian', 'tnt', 'oak_slab', 'oak_stairs', 'hopper', 'bamboo', 'cactus', 'sugar_cane', 'kelp',
|
||||
'seagrass', 'vine', 'moss_block', 'dripstone_block', 'amethyst_block', 'calcite', 'clay', 'coarse_dirt', 'rooted_dirt',
|
||||
]
|
||||
|
||||
DIRS = [(0, -1, 0), (0, 1, 0), (0, 0, -1), (0, 0, 1), (-1, 0, 0), (1, 0, 0)]
|
||||
|
||||
|
||||
class Facts:
|
||||
def __init__(self, resources):
|
||||
facts = json.loads((resources / 'facts.json').read_text())
|
||||
self.states = facts['states']
|
||||
self.defaults = facts['defaults']
|
||||
self.by_block = {}
|
||||
for index, state in enumerate(self.states):
|
||||
self.by_block.setdefault(state[0], []).append(index)
|
||||
self.palette = [name for name in ('minecraft:' + n for n in NEIGHBOUR_PALETTE) if name in self.by_block]
|
||||
self.names = list(self.by_block)
|
||||
|
||||
def describe(self, index):
|
||||
return self.states[index][3].replace('Block{', '').replace('}', '', 1)
|
||||
|
||||
def neighbour(self, rng, own):
|
||||
roll = rng.random()
|
||||
if roll < 0.30:
|
||||
return rng.choice(self.by_block[rng.choice(self.palette)])
|
||||
if roll < 0.45:
|
||||
return rng.choice(self.by_block[self.states[own][0]])
|
||||
if roll < 0.60:
|
||||
return rng.choice(self.by_block[rng.choice(self.names)])
|
||||
return None
|
||||
|
||||
def cells(self, rng, origin_state, density=0.55, face_density=0.8, extent=2):
|
||||
cells = [[0, 0, 0, origin_state]]
|
||||
for x in range(-extent, extent + 1):
|
||||
for y in range(-extent, extent + 1):
|
||||
for z in range(-extent, extent + 1):
|
||||
if (x, y, z) == (0, 0, 0):
|
||||
continue
|
||||
face = abs(x) + abs(y) + abs(z) == 1
|
||||
if rng.random() > (face_density if face else density):
|
||||
continue
|
||||
state = self.neighbour(rng, origin_state)
|
||||
if state is not None:
|
||||
cells.append([x, y, z, state])
|
||||
return cells
|
||||
|
||||
|
||||
def environment(rng):
|
||||
case = {'seed': rng.randrange(1 << 40), 'time': rng.randrange(0, 100000)}
|
||||
roll = rng.random()
|
||||
if roll < 0.5:
|
||||
case['light'] = [15, 0]
|
||||
elif roll < 0.7:
|
||||
case['light'] = [0, 0]
|
||||
elif roll < 0.85:
|
||||
case['light'] = [rng.randrange(16), rng.randrange(16)]
|
||||
else:
|
||||
case['light'] = [15, 15]
|
||||
if rng.random() < 0.1:
|
||||
case['raining'] = True
|
||||
case['thundering'] = rng.random() < 0.3
|
||||
return case
|
||||
|
||||
|
||||
def put(cells, pos, state):
|
||||
for cell in cells:
|
||||
if cell[:3] == list(pos):
|
||||
cell[3] = state
|
||||
return
|
||||
cells.append([*pos, state])
|
||||
|
||||
|
||||
def argument_parser(description):
|
||||
parser = argparse.ArgumentParser(description=description)
|
||||
parser.add_argument('--count', type=int, default=40000, help='cases to generate')
|
||||
parser.add_argument('--seed', type=int, default=1)
|
||||
parser.add_argument('--only', help='substring of the block name under test')
|
||||
parser.add_argument('--show', type=int, default=1, help='examples printed per block')
|
||||
parser.add_argument('--events', action='store_true', help='compare side-effect logs too (default: states, ticks, rng and events)')
|
||||
parser.add_argument('--ignore', default='', help='comma separated line prefixes to ignore when comparing (e.g. S,G)')
|
||||
return parser
|
||||
|
||||
|
||||
def blocks_of(facts):
|
||||
return facts.names
|
||||
|
||||
|
||||
def run(command, **kwargs):
|
||||
done = subprocess.run(command, capture_output=True, text=True, **kwargs)
|
||||
if done.returncode:
|
||||
print((done.stdout + done.stderr)[-3000:])
|
||||
raise SystemExit(f'{command[0]} failed')
|
||||
return done
|
||||
|
||||
|
||||
def execute(name, cases, args, facts, origin_of=None):
|
||||
"""Run `cases` on both sides; print a report and return the number of differing cases."""
|
||||
lock, jars, libraries = base.verify_artifacts()
|
||||
resources = lab.LOCAL / 'horizon-client' / lock['minecraft']['version']
|
||||
work = lab.LOCAL / name
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
(work / 'cases.jsonl').write_text(''.join(json.dumps(case) + '\n' for case in cases))
|
||||
classes = work / 'classes'
|
||||
classes.mkdir(exist_ok=True)
|
||||
dependencies = os.pathsep.join(map(str, [jars['server'], *libraries]))
|
||||
java_root = lab.ROOT / 'mods/horizon/src/test/java'
|
||||
run(['javac', '--release', str(lock['minecraft']['java_major']), '-proc:none', '-sourcepath', str(java_root), '-nowarn',
|
||||
'-cp', dependencies, '-d', str(classes), str(java_root / 'dev/horizonlab/parity/VanillaBlockUpdateOracle.java')])
|
||||
run(['java', '-Xmx2G', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaBlockUpdateOracle',
|
||||
str(resources), str(work / 'cases.jsonl'), str(work / 'native.txt')])
|
||||
run(['cargo', 'run', '-q', '--release', '-p', 'horizon-world', '--example', 'block_update_fixture', '--',
|
||||
str(resources), str(work / 'cases.jsonl'), str(work / 'rust.txt')], cwd=lab.ROOT, env={**os.environ, 'CARGO_BUILD_JOBS': '3'})
|
||||
ignore = tuple(prefix + ' ' for prefix in args.ignore.split(',') if prefix)
|
||||
native = split_cases((work / 'native.txt').read_text(), ignore)
|
||||
rust = split_cases((work / 'rust.txt').read_text(), ignore)
|
||||
assert len(native) == len(rust) == len(cases), (len(native), len(rust), len(cases))
|
||||
groups = {}
|
||||
errors = {}
|
||||
for case, a, b in zip(cases, native, rust):
|
||||
if a and a[0].startswith('error'):
|
||||
errors.setdefault(a[0], []).append(case)
|
||||
continue
|
||||
if a != b:
|
||||
groups.setdefault(origin_name(case, facts), []).append((case, a, b))
|
||||
total = sum(len(v) for v in groups.values())
|
||||
for kind, entries in sorted(errors.items(), key=lambda entry: -len(entry[1])):
|
||||
print(f'oracle error x{len(entries)}: {kind} (e.g. {facts.describe(entries[0]["cells"][0][3])})')
|
||||
for block, entries in sorted(groups.items(), key=lambda entry: -len(entry[1])):
|
||||
print(f'{block}: {len(entries)} differing')
|
||||
for case, a, b in entries[:args.show]:
|
||||
print(' case ' + describe_case(case, facts))
|
||||
print(' native ' + ' | '.join(a))
|
||||
print(' rust ' + ' | '.join(b))
|
||||
skipped = sum(len(v) for v in errors.values())
|
||||
print(f'{len(cases) - total - skipped}/{len(cases) - skipped} cases match ({skipped} oracle errors)')
|
||||
(work / 'differences.json').write_text(json.dumps({k: [[c, a, b] for c, a, b in v[:5]] for k, v in groups.items()}))
|
||||
return total
|
||||
|
||||
|
||||
def split_cases(text, ignore):
|
||||
out = []
|
||||
for block in text.split('case ')[1:]:
|
||||
lines = block.splitlines()[1:]
|
||||
out.append([line for line in lines if not line.startswith(ignore)] if ignore else lines)
|
||||
return out
|
||||
|
||||
|
||||
def origin_name(case, facts):
|
||||
pos = case.get('pos', [0, 0, 0])
|
||||
for cell in case['cells']:
|
||||
if cell[:3] == pos:
|
||||
return facts.states[cell[3]][0]
|
||||
return 'minecraft:air'
|
||||
|
||||
|
||||
def describe_case(case, facts):
|
||||
pos = case.get('pos', [0, 0, 0])
|
||||
parts = [case['mode'], facts.describe(next(c[3] for c in case['cells'] if c[:3] == pos))]
|
||||
if case['mode'] == 'shape':
|
||||
parts.append(f'dir={case["dir"]} neighbor={facts.describe(case["neighbor"])}')
|
||||
if case['mode'] == 'neighbor':
|
||||
parts.append(f'from={case["from"]}')
|
||||
if case.get('light') not in (None, [15, 0]):
|
||||
parts.append(f'light={case["light"]}')
|
||||
if case.get('raining'):
|
||||
parts.append('raining')
|
||||
near = []
|
||||
for dx, dy, dz in DIRS:
|
||||
at = [pos[0] + dx, pos[1] + dy, pos[2] + dz]
|
||||
state = next((c[3] for c in case['cells'] if c[:3] == at), None)
|
||||
near.append('air' if state is None else facts.describe(state))
|
||||
parts.append('faces=' + ';'.join(near))
|
||||
return ' '.join(parts) + f' seed={case["seed"]} time={case.get("time")}'
|
||||
|
||||
|
||||
def load_facts():
|
||||
lock, _, _ = base.verify_artifacts()
|
||||
return Facts(lab.LOCAL / 'horizon-client' / lock['minecraft']['version'])
|
||||
|
||||
|
||||
def state_pool(facts, only):
|
||||
return [i for i, s in enumerate(facts.states) if not only or only in s[0]]
|
||||
|
||||
|
||||
def finish(differing):
|
||||
raise SystemExit(1 if differing else 0)
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare vanilla updateShape and neighborChanged with horizon-world, state by state.
|
||||
|
||||
Every block state is put at the origin of a random neighbourhood; `shape` cases
|
||||
call `BlockState.updateShape` for one direction with a (consistent) neighbour
|
||||
state and compare the resulting state, requested scheduled ticks, side effects and
|
||||
RNG use; `neighbor` cases call `handleNeighborChanged` from one direction.
|
||||
"""
|
||||
import random
|
||||
try:
|
||||
from . import block_update_harness as h
|
||||
except ImportError:
|
||||
import block_update_harness as h
|
||||
|
||||
|
||||
def cases(facts, count, seed, only):
|
||||
rng = random.Random(seed)
|
||||
pool = h.state_pool(facts, only)
|
||||
air = facts.defaults['minecraft:air']
|
||||
for n in range(count):
|
||||
state = pool[n % len(pool)]
|
||||
direction = rng.randrange(6)
|
||||
cells = facts.cells(rng, state)
|
||||
case = {**h.environment(rng), 'pos': [0, 0, 0], 'dir': direction}
|
||||
dx, dy, dz = h.DIRS[direction]
|
||||
if n % 3 == 2:
|
||||
case['mode'] = 'neighbor'
|
||||
case['from'] = [dx, dy, dz]
|
||||
else:
|
||||
case['mode'] = 'shape'
|
||||
neighbour = facts.neighbour(rng, state)
|
||||
neighbour = air if neighbour is None else neighbour
|
||||
h.put(cells, (dx, dy, dz), neighbour)
|
||||
case['neighbor'] = neighbour
|
||||
case['cells'] = cells
|
||||
yield case
|
||||
|
||||
|
||||
def main():
|
||||
args = h.argument_parser(__doc__).parse_args()
|
||||
facts = h.load_facts()
|
||||
differing = h.execute('block-update-parity', list(cases(facts, args.count, args.seed, args.only)), args, facts)
|
||||
h.finish(differing)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user