mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Add random tick and redstone harnesses, wire detector rails and plates
Adds differential harnesses for random/scheduled ticks and redstone circuits on top of the block update oracle, and makes the oracle report pending block events as the deduplicated set vanilla keeps. The harness found that detector rails never sensed anything, so they now check for minecarts on place, tick and entity contact, and the client reports the player and minecarts to pressure plates, tripwires and detector rails each tick. Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
40802dd9af
commit
bd9b873fcd
8 files changed
+227
-5
No files matched your search
@@ -120,13 +120,16 @@ use horizon_client::{
|
||||
vanillagen::VanillaTerrain,
|
||||
world::World,
|
||||
};
|
||||
use horizon_core::{geometry::Direction, position::BlockPos};
|
||||
use horizon_core::{geometry::Vec3, random::RandomSource};
|
||||
use horizon_core::{
|
||||
geometry::{Aabb, Direction},
|
||||
position::BlockPos,
|
||||
};
|
||||
use horizon_world::{
|
||||
block_collision::BlockCollisionRegistry,
|
||||
level::{
|
||||
Behaviours, Dimension as LevelDimension, Landing, LevelAccess, LevelEvent, PlaceContext,
|
||||
PlaceOutcome, PlayerContext, PlayerEffect, Ticks, flags as level_flags,
|
||||
Behaviours, Dimension as LevelDimension, EntityQuery, Landing, LevelAccess, LevelEvent,
|
||||
PlaceContext, PlaceOutcome, PlayerContext, PlayerEffect, Ticks, flags as level_flags,
|
||||
},
|
||||
registry::BlockStateId,
|
||||
};
|
||||
@@ -312,6 +315,8 @@ struct GameLevel<'w> {
|
||||
raining: bool,
|
||||
thundering: bool,
|
||||
player_box: Option<([f64; 3], [f64; 3])>,
|
||||
/// Minecart boxes (detector rails).
|
||||
cart_boxes: Vec<([f64; 3], [f64; 3])>,
|
||||
biomes: &'w Biomes,
|
||||
seed: i64,
|
||||
behaviours: &'w Behaviours,
|
||||
@@ -444,6 +449,22 @@ impl LevelAccess for GameLevel<'_> {
|
||||
&& z < hi[2] - 1e-7
|
||||
&& z + 1.0 > lo[2] + 1e-7)
|
||||
}
|
||||
/// Entities that trigger pressure plates, tripwires and detector rails:
|
||||
/// the player (a mob) and the minecarts.
|
||||
fn count_entities(&self, area: Aabb, query: EntityQuery) -> usize {
|
||||
let hit = |(lo, hi): &([f64; 3], [f64; 3])| {
|
||||
area.intersects(Aabb::new(
|
||||
Vec3::new(lo[0], lo[1], lo[2]),
|
||||
Vec3::new(hi[0], hi[1], hi[2]),
|
||||
))
|
||||
};
|
||||
let carts = || self.cart_boxes.iter().filter(|b| hit(b)).count();
|
||||
match query {
|
||||
EntityQuery::Minecart => carts(),
|
||||
EntityQuery::Mobs => self.player_box.iter().filter(|b| hit(b)).count(),
|
||||
EntityQuery::Everything => self.player_box.iter().filter(|b| hit(b)).count() + carts(),
|
||||
}
|
||||
}
|
||||
fn emit(&mut self, event: LevelEvent) {
|
||||
self.events.push(event);
|
||||
}
|
||||
@@ -1191,6 +1212,17 @@ impl Game<'_> {
|
||||
],
|
||||
)
|
||||
});
|
||||
let cart_boxes = self
|
||||
.carts
|
||||
.iter()
|
||||
.map(|c| {
|
||||
let p = c.position;
|
||||
(
|
||||
[p.x - 0.49, p.y, p.z - 0.49],
|
||||
[p.x + 0.49, p.y + 0.7, p.z + 0.49],
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let dimension = match self.dim {
|
||||
Dim::Overworld => LevelDimension::Overworld,
|
||||
Dim::Nether => LevelDimension::Nether,
|
||||
@@ -1198,6 +1230,7 @@ impl Game<'_> {
|
||||
};
|
||||
let rules = self.level_rules();
|
||||
let mut level = GameLevel {
|
||||
cart_boxes,
|
||||
world: &mut self.world,
|
||||
facts: &self.res.facts,
|
||||
events: &mut self.level_events,
|
||||
@@ -1266,7 +1299,41 @@ impl Game<'_> {
|
||||
.collect();
|
||||
ticking.push((x, z, sections));
|
||||
}
|
||||
// Entity.checkInsideBlocks for the plates, tripwires and detector rails
|
||||
// under the player and the minecarts.
|
||||
let mut inside = Vec::new();
|
||||
let boxes = self
|
||||
.player
|
||||
.iter()
|
||||
.filter(|_| !self.spectator())
|
||||
.map(|p| {
|
||||
let w = f64::from(p.dimensions.width) / 2.0;
|
||||
let h = f64::from(p.dimensions.height);
|
||||
(
|
||||
[p.position.x - w, p.position.y, p.position.z - w],
|
||||
[p.position.x + w, p.position.y + h, p.position.z + w],
|
||||
)
|
||||
})
|
||||
.chain(self.carts.iter().map(|c| {
|
||||
let p = c.position;
|
||||
(
|
||||
[p.x - 0.49, p.y, p.z - 0.49],
|
||||
[p.x + 0.49, p.y + 0.7, p.z + 0.49],
|
||||
)
|
||||
}));
|
||||
for (lo, hi) in boxes {
|
||||
for x in (lo[0] + 1e-7).floor() as i32..=(hi[0] - 1e-7).floor() as i32 {
|
||||
for y in (lo[1] + 1e-7).floor() as i32..=(hi[1] - 1e-7).floor() as i32 {
|
||||
for z in (lo[2] + 1e-7).floor() as i32..=(hi[2] - 1e-7).floor() as i32 {
|
||||
inside.push(BlockPos::new(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.with_level(|b, level, ticks| {
|
||||
for pos in inside {
|
||||
b.entity_inside(level, ticks, pos);
|
||||
}
|
||||
b.tick_scheduled(level, ticks);
|
||||
for (x, z, sections) in ticking {
|
||||
b.tick_chunk_random(
|
||||
|
||||
@@ -90,7 +90,10 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
Class::BaseFire => self.base_fire_on_place(pos, state, same),
|
||||
Class::DetectorRail | Class::BaseRail => {
|
||||
if !same {
|
||||
self.rail_update_state(pos, state, moved);
|
||||
let updated = self.rail_update_state(pos, state, moved);
|
||||
if class == Class::DetectorRail {
|
||||
self.detector_rail_check_pressed(pos, updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
Class::Brushable => {
|
||||
@@ -416,6 +419,10 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
if signal == 0 {
|
||||
self.plate_check_pressed(pos, state, signal);
|
||||
}
|
||||
} else if data.is(Class::DetectorRail) {
|
||||
if !b.table.bool(state, b.props.powered) {
|
||||
self.detector_rail_check_pressed(pos, state);
|
||||
}
|
||||
} else if data.is(Class::TripWire)
|
||||
&& !b.table.bool(state, b.props.powered)
|
||||
&& !self.ticks.has_block_tick(pos, b.block(state))
|
||||
|
||||
@@ -196,6 +196,8 @@ pub enum EntityQuery {
|
||||
Everything,
|
||||
/// Living entities only (stone plates).
|
||||
Mobs,
|
||||
/// Minecarts only (detector rails).
|
||||
Minecart,
|
||||
}
|
||||
|
||||
/// Side effects reported to the caller, in vanilla call order.
|
||||
|
||||
@@ -515,6 +515,40 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
}
|
||||
}
|
||||
}
|
||||
/// `DetectorRailBlock.checkPressed`.
|
||||
pub(crate) fn detector_rail_check_pressed(&mut self, pos: BlockPos, state: BlockStateId) {
|
||||
let b = self.b;
|
||||
if !self.can_survive(state, pos) || !b.is_block(self.state(pos), b.block(state)) {
|
||||
return;
|
||||
}
|
||||
let was = b.table.bool(state, b.props.powered);
|
||||
// getInteractingMinecartOfType: the 0.2-inset search box.
|
||||
let (x, y, z) = (f64::from(pos.x), f64::from(pos.y), f64::from(pos.z));
|
||||
let area = Aabb::new(
|
||||
Vec3::new(x + 0.2, y, z + 0.2),
|
||||
Vec3::new(x + 0.8, y + 0.8, z + 0.8),
|
||||
);
|
||||
let pressed = self
|
||||
.level
|
||||
.count_entities(area, super::EntityQuery::Minecart)
|
||||
> 0;
|
||||
let block = b.block(state);
|
||||
if pressed != was {
|
||||
let next = b.table.set_bool(state, b.props.powered, pressed);
|
||||
self.set_block_and_update(pos, next);
|
||||
let rail = self.rail_new(pos, next);
|
||||
for connection in rail.connections {
|
||||
let at = self.state(connection);
|
||||
self.neighbor_changed_full(at, connection, b.block(at), false);
|
||||
}
|
||||
self.update_neighbors_at(pos, block);
|
||||
self.update_neighbors_at(below(pos), block);
|
||||
}
|
||||
if pressed {
|
||||
self.schedule_tick(pos, block, 20);
|
||||
}
|
||||
self.update_neighbour_for_output_signal(pos, block);
|
||||
}
|
||||
pub(crate) fn tripwire_tick(&mut self, pos: BlockPos) {
|
||||
let b = self.b;
|
||||
let state = self.state(pos);
|
||||
|
||||
@@ -52,6 +52,11 @@ impl<L: LevelAccess + ?Sized> Sim<'_, L> {
|
||||
self.plate_check_pressed(pos, state, signal);
|
||||
}
|
||||
}
|
||||
Class::DetectorRail => {
|
||||
if b.table.bool(state, b.props.powered) {
|
||||
self.detector_rail_check_pressed(pos, state);
|
||||
}
|
||||
}
|
||||
Class::BigDripleaf => self.big_dripleaf_tick(pos, state),
|
||||
Class::AbstractCauldron => self.cauldron_tick(state, pos),
|
||||
Class::BubbleColumn => {
|
||||
|
||||
@@ -117,6 +117,7 @@ public final class VanillaBlockUpdateOracle {
|
||||
World() {
|
||||
super(null, null, null, null, null, null, false, 0L, List.of(), false);
|
||||
}
|
||||
java.util.LinkedHashSet<String> blockEvents;
|
||||
void emit(String line) {
|
||||
log.append(line).append('\n');
|
||||
}
|
||||
@@ -173,7 +174,7 @@ public final class VanillaBlockUpdateOracle {
|
||||
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 blockEvent(BlockPos p, Block block, int a, int b) { blockEvents.add("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; }
|
||||
@@ -287,6 +288,7 @@ public final class VanillaBlockUpdateOracle {
|
||||
World w = make(World.class);
|
||||
w.cells = new HashMap<>();
|
||||
w.log = new StringBuilder();
|
||||
w.blockEvents = new java.util.LinkedHashSet<>();
|
||||
for (var cell : f.getAsJsonArray("cells")) {
|
||||
var c = cell.getAsJsonArray();
|
||||
BlockState s = Block.stateById(c.get(3).getAsInt());
|
||||
@@ -372,6 +374,7 @@ public final class VanillaBlockUpdateOracle {
|
||||
for (String s : summary(w, before)) result.append(s).append('\n');
|
||||
pending(w, result);
|
||||
result.append(w.log);
|
||||
for (String s : w.blockEvents) result.append(s).append('\n');
|
||||
result.append("rng ").append(w.rng.nextLong()).append('\n');
|
||||
}
|
||||
}
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare vanilla random ticks (and scheduled ticks) with horizon-world, state by state.
|
||||
|
||||
Every block state is put at the origin of a random neighbourhood and `randomTick`
|
||||
(or `tick`) is run with a seeded RNG on both sides; the resulting block changes,
|
||||
scheduled ticks, side effects and the RNG fingerprint after the call must match.
|
||||
"""
|
||||
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)
|
||||
for n in range(count):
|
||||
state = pool[n % len(pool)]
|
||||
case = {**h.environment(rng), 'pos': [0, 0, 0], 'cells': facts.cells(rng, state),
|
||||
'mode': 'random' if n % 4 else 'tick'}
|
||||
yield case
|
||||
|
||||
|
||||
def main():
|
||||
args = h.argument_parser(__doc__).parse_args()
|
||||
facts = h.load_facts()
|
||||
differing = h.execute('random-tick-parity', list(cases(facts, args.count, args.seed, args.only)), args, facts)
|
||||
h.finish(differing)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compare vanilla redstone circuits with horizon-world, tick by tick.
|
||||
|
||||
A small floor carries randomly placed redstone components (wire, repeaters,
|
||||
comparators, torches, observers, pistons, lamps, rails, plates, buttons...). A power
|
||||
source is placed and later removed through `Level.setBlock` and the scheduled-tick
|
||||
queue is run for N game ticks; the block changes and pending ticks after every
|
||||
tick (and the RNG fingerprint at the end) must match.
|
||||
"""
|
||||
import random
|
||||
try:
|
||||
from . import block_update_harness as h
|
||||
except ImportError:
|
||||
import block_update_harness as h
|
||||
|
||||
COMPONENTS = [
|
||||
'redstone_wire', 'redstone_wire', 'redstone_wire', 'redstone_wire', 'repeater', 'repeater', 'comparator', 'redstone_torch',
|
||||
'redstone_wall_torch', 'observer', 'piston', 'sticky_piston', 'redstone_lamp', 'lever', 'stone_button', 'oak_button',
|
||||
'stone_pressure_plate', 'note_block', 'target', 'dropper', 'dispenser', 'hopper', 'rail', 'powered_rail', 'activator_rail',
|
||||
'detector_rail', 'oak_door', 'iron_door', 'oak_trapdoor', 'oak_fence_gate', 'tnt', 'slime_block', 'honey_block', 'stone',
|
||||
'redstone_block', 'daylight_detector', 'trapped_chest', 'copper_bulb', 'sculk_sensor', 'tripwire_hook', 'lectern',
|
||||
'oak_slab', 'iron_block', 'glass', 'crafter',
|
||||
]
|
||||
|
||||
|
||||
def build(facts, rng):
|
||||
floor = facts.defaults['minecraft:stone']
|
||||
names = [n for n in ('minecraft:' + c for c in COMPONENTS) if n in facts.by_block]
|
||||
cells = []
|
||||
for x in range(-4, 5):
|
||||
for z in range(-4, 5):
|
||||
cells.append([x, -1, z, floor])
|
||||
if rng.random() < 0.5:
|
||||
name = rng.choice(names)
|
||||
states = facts.by_block[name]
|
||||
state = rng.choice(states)
|
||||
cells.append([x, 0, z, state])
|
||||
if rng.random() < 0.15:
|
||||
cells.append([x, 1, z, rng.choice(facts.by_block[rng.choice(names)])])
|
||||
return cells
|
||||
|
||||
|
||||
def cases(facts, count, seed, only):
|
||||
rng = random.Random(seed)
|
||||
source = facts.defaults['minecraft:redstone_block']
|
||||
air = facts.defaults['minecraft:air']
|
||||
for n in range(count):
|
||||
cells = build(facts, rng)
|
||||
at = [rng.randrange(-4, 5), 0, rng.randrange(-4, 5)]
|
||||
cells = [c for c in cells if c[:3] != at]
|
||||
cells.append([*at, air])
|
||||
case = {**h.environment(rng), 'mode': 'run', 'pos': [0, -1, 0], 'cells': cells, 'ops': [
|
||||
{'op': 'set', 'pos': at, 'state': source, 'flags': 3},
|
||||
{'op': 'ticks', 'n': 12, 'snap': True},
|
||||
{'op': 'set', 'pos': at, 'state': air, 'flags': 3},
|
||||
{'op': 'ticks', 'n': 12, 'snap': True},
|
||||
]}
|
||||
case.pop('raining', None)
|
||||
yield case
|
||||
|
||||
|
||||
def main():
|
||||
args = h.argument_parser(__doc__).parse_args()
|
||||
args.count = min(args.count, 4000) if args.count == 40000 else args.count
|
||||
facts = h.load_facts()
|
||||
differing = h.execute('redstone-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