Implement proto-chunk ticks and world-generation tick routing

This commit is contained in:
Ludovic Marie committed 2026-10-06 13:39:19 +02:00
1 parent fd3416da36
commit 1cf1322ae2
8 files changed
+604

No files matched your search

+4
View File
@@ -167,6 +167,10 @@ jobs:
env:
RUSTUP_TOOLCHAIN: stable
run: python tools/test_snow_freeze_parity.py
- name: Compare generation-stage tick storage and routing
env:
RUSTUP_TOOLCHAIN: stable
run: python tools/test_generation_tick_parity.py
- name: Compare ordered single-block replacement features
env:
RUSTUP_TOOLCHAIN: stable
@@ -0,0 +1,110 @@
{
"schema": 1,
"minecraft": "26.3",
"manifest_sha1": "4fe1aa1ef8da1cb95c5bad1fb98890ca56dd8ca3",
"platform": "Linux-7.0.0-31-generic-x86_64-with-glibc2.43",
"java": "openjdk 25.0.4.1 2026-08-18",
"rustc": "rustc 1.99.0 (b940084d7 2026-09-28)",
"commands": {
"reset": 32,
"load": 38,
"query": 8610,
"snapshot": 115,
"schedule": 8577,
"dump": 536,
"snapshot_dump": 122,
"promote": 115,
"fail": 244,
"will": 199,
"count": 122,
"unpack": 2,
"level": 31,
"remove": 84
},
"sources_sha256": {
"mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGenerationTickOracle.java": "c0b95e75e007c2ff53fde575104bf2e25b6fb65e46d11ad2a2dcae760004fe38",
"tools/test_generation_tick_parity.py": "e200240c24d12ed546e2254e0dea947ea3cf8db8a703e4908a90567250d2fdf1",
"tools/test_rust_parity.py": "29d3db08005873528894b45ca10dcd6278753650473b3eb574e53e06ede40ec6",
"tools/lab.py": "d00bf107fa01b6c579734a082c3ca4ade3018f0d6851d46844acf17574f66e64",
"Cargo.toml": "dc370d5fdde37238c3e4820046f11c3201020406a4edfa1308257c13edbdcda4",
"Cargo.lock": "7307d0c887364db36d327685ecf0671b75305e31e5ba5fbdcdbb15eaf156d20e",
"crates/horizon-core/Cargo.toml": "e21b502c949da93cf67826592459b687a8cbf737ef92fee514e3ed2cc7cd27ad",
"crates/horizon-core/src/bin/density_parity.rs": "e5b1dd33b7d01306c73cf072629b4fb186e9ab87b672a00108999d1a72263fd0",
"crates/horizon-core/src/bin/float_log_parity.rs": "4a655fcf8822ee7668c4bd126bc84d0b118864a6fc7f5a6aeee667d8bf2590b1",
"crates/horizon-core/src/bin/geometry_parity.rs": "27f6c09fe5018ac3de8e70e0f0bbf78f9abb14f5e4783bcaf46e8ca87b9c6f8a",
"crates/horizon-core/src/bin/noise_parity.rs": "c135bfc708d853e7061e81487d2cfe10de64b2f65677d3b4837cbfe0b8341010",
"crates/horizon-core/src/bin/parity.rs": "f35d928c66f958de39fac5e7e836a657eed95f83599e84899f7ea7a5802ba481",
"crates/horizon-core/src/bin/tick_parity.rs": "a54c164fe4814034d369947a887f5273966f5a878ba849204b80d1c729526a8f",
"crates/horizon-core/src/biome/seed_hash.rs": "5afbb4921c750826b395a08bf06f382fc7f01b5759a93de02a2749c523e2a317",
"crates/horizon-core/src/biome.rs": "908efa8cd4aa0a7adf1cb5e1a222e3dfb640eed5e9172e0fe7f0d1287afae7e8",
"crates/horizon-core/src/carving/ellipsoid.rs": "09232a7588610e8a9176fd9c62d40afb1fb89f2ecab6d555536a1d847b142d86",
"crates/horizon-core/src/carving.rs": "0aa3093caa0437868a8c0ed52a6c6e1993bd51c03583c670a2837e749eae5426",
"crates/horizon-core/src/density/arithmetic.rs": "372faec96c923e2cf4aa6c66c1ac398e676853274557823850dbc8933dd15ab1",
"crates/horizon-core/src/density/beardifier.rs": "bb1052e27dae4a01954ca78d7d5babafef85f3b2c704ccaf1920cb85a098cb50",
"crates/horizon-core/src/density/cache.rs": "38d0b6c9dd04ffef979fdad3f29cfebf98d331c8c9d430be5f39aa34f6c30975",
"crates/horizon-core/src/density/end_islands.rs": "168b9384237bd2e41d0ae5a74e944da4dc74e493bdec31fb73184c9a49e3de55",
"crates/horizon-core/src/density/graph/compiler.rs": "ac67deeb16f16d0859aa20e5a165811a29aca0b910037ee6bf5098211f3a6ede",
"crates/horizon-core/src/density/graph/mod.rs": "b776e7da934cb03051a3053d56d461c71e9978e084922fe4194147aa580c07bf",
"crates/horizon-core/src/density/graph/seeded.rs": "6bcdcaa3dffcfa9df673bbbea35120057a16ba3d2b506c52709c36cf8976b74a",
"crates/horizon-core/src/density/graph/tests.rs": "6bfb8cbff462587dd147b0775ea01d370abbd97544b9bd596c792df1b5f7e634",
"crates/horizon-core/src/density/interpolation.rs": "8c890f52b6467f38a94c654986a6a484b8e0b296ab98c398e60a6ce465b5b010",
"crates/horizon-core/src/density/interval.rs": "b42629163baa6b599a233afe8596d1c370cd81d015cc6fc36a9663aa47b98424",
"crates/horizon-core/src/density/mod.rs": "ca0d77d3282e3746ddcbc92bee97dbf640b9a56214e6ac1537f51f9a5b63d6e7",
"crates/horizon-core/src/density/power.rs": "d5b7270a128d4264268cd772109ad16d71a8ec3ff2d3b63b84cb985296d54a91",
"crates/horizon-core/src/density/rounding.rs": "71840c5206ff8d218ac29bf6f66915d5212764ba270c675711a866ba5b55a7a6",
"crates/horizon-core/src/density/selection.rs": "0dd26af2e85004ec5e053039ad11c384c17c56ace9ab45eb3f5cb3503c0b0330",
"crates/horizon-core/src/density/slice.rs": "5d77605573c0deda47778f1f78f7fa3fbf1944d8f5efbcc65cf589181cadf80b",
"crates/horizon-core/src/density/spline.rs": "6bb2e40f940fc6544d6fb09cb0b8cac722be03bc663dbdfb0f61faae207400bc",
"crates/horizon-core/src/density/surface.rs": "159e0e69cda74f870e80ea64fe523c4397e64af4ccb861bb9fd07b2c3cb91b00",
"crates/horizon-core/src/density/terrain_noise.rs": "8d0059d25eb5d248cb97a4c5a2e670d61528928ea151495c004f25adb723fe3e",
"crates/horizon-core/src/density/tests.rs": "7817796ee40d0ae241bfbebec6bca190e0a8e7d5a1caa8ddf858c3eaa58b8858",
"crates/horizon-core/src/density/world_fields.rs": "c1ce305cb8046b384d90c564cafb24af7365d580389637910f3d8b0cc18eb583",
"crates/horizon-core/src/geometry/mod.rs": "dc92f73885dd0a7760743bf74f1ab8afa839d61cef2dfef890f8a23b67b4742e",
"crates/horizon-core/src/geometry/movement.rs": "930e34abef6f6eeec1ee7a6c920b5ba01bfe37b90678884490f4be53241f6b96",
"crates/horizon-core/src/geometry/shape/create.rs": "189812b91d5e0e3ad963afcd1a6e2831b31b8188df91a904ef27061e644806ad",
"crates/horizon-core/src/geometry/shape/face.rs": "f243bce81384040a95ce7984b02b5699206c596c537b2713097e86005b4d3a5c",
"crates/horizon-core/src/geometry/shape/join.rs": "f1b51887b7bf9482557e3ffa99e136f2e5db07ceeb05fdb116adf45e87129075",
"crates/horizon-core/src/geometry/shape.rs": "e8d88a763de1525f77a30b99591641e8a8b80946263acbde035a9bcc356df960",
"crates/horizon-core/src/geometry/sweep.rs": "abd6009c61555161ef2f18b431b2aa384b126c0871cae8b96a62358498a56a1d",
"crates/horizon-core/src/geometry/traversal.rs": "77a3da570a0b85582b669cf4d841cba4b06327006dda26d4a26df4e8ce05bae4",
"crates/horizon-core/src/geometry/trig.rs": "05cef9341a298a474221a0556a11ddf47650393ea7a97b4b2022a4da6628fe37",
"crates/horizon-core/src/lib.rs": "47350edab5791f5074a54563ba98108818c5c6b1c5818a591388eab2ac9f5490",
"crates/horizon-core/src/noise/mod.rs": "1dab5a713abf702402e69ace1bf9e476bba544b0ede5a61c3c3afc60af0a03f3",
"crates/horizon-core/src/noise/normal.rs": "e9e7a46472a54a02c98c46da819842090567153b32fe0c84249009ef19af7aad",
"crates/horizon-core/src/noise/perlin.rs": "e73815447432eab7542f67366cb3f1b368d164e33f6c9cdbd045ac8e4db0932a",
"crates/horizon-core/src/noise/simplex.rs": "6c0dbc15b2b7510195dd28fceca01dcaac1847ccc4a84d299c351589c642fe7a",
"crates/horizon-core/src/noise/stack.rs": "7f6ef0e7d920139ef1a6ad3ec5640f92765715f86dcc5ec29f1a149984f45fa3",
"crates/horizon-core/src/noise/volume.rs": "3f05d2b1780acfdfee60037677d309937a5033b45ef428fc2d540942d4b56b43",
"crates/horizon-core/src/position/iter.rs": "32d2d8e74821cc4e49e3354202d35d3120e4415bfdda9375a73f2137ef651129",
"crates/horizon-core/src/position.rs": "e10101088e6902386b9b6d27773d44312f1ff72bebb19e57967a54945dde9dcd",
"crates/horizon-core/src/random/hash.rs": "0f88a127a1ba2705d3d6c0ebd009324d6ba86ffa2d27edf2d2a84765e690e4a3",
"crates/horizon-core/src/random/positional.rs": "35494745e5d4cf89e282c3ae0ee245e9f2640ddbbc03bc0055df742006a9c862",
"crates/horizon-core/src/random/worldgen.rs": "9f07ff33bcc038d34e27aa5465753bb21df18f2975e43cae201f709bc186945a",
"crates/horizon-core/src/random.rs": "ddb95d3a83e28ad605ff1f49f5df270cc543ca5ef1130e37d989dc74c8d861e6",
"crates/horizon-core/src/storage.rs": "f6c32201c64aefde06e57a2c767be46918df613548a322801546b133799cc33d",
"crates/horizon-core/src/ticks/generation.rs": "be1237e790693f633a61714ddae68c6506a777588a8a3ab5efbe53a35774dbd2",
"crates/horizon-core/src/ticks/heap.rs": "bf28afab8af77c3d243aa96cd8a1fb8d934140a4dbd07dbfdbf4e7908bb4b845",
"crates/horizon-core/src/ticks/index.rs": "1f1a6a568c2a30eafecb020ca4d5f5fc414860de85b8b381fc419a2e3e65c24a",
"crates/horizon-core/src/ticks/mod.rs": "01634630668172516b8b79d0907bad47f6e2c543a6d499c2fa4b201823b375cb",
"crates/horizon-core/examples/generation_tick_fixture.rs": "67a6aca02386a9bddd0198c559d66bbb54b1a8f44bad0e916028cfbc4d3cde97"
},
"fixtures_sha256": {
".local/rust-generation-tick-parity/inputs.txt": "bc9332ec147bfde2fe6d5a866deb4f985079c77c206fbbece33d4128ff01146e"
},
"rust_output_sha256": "9ae4ecba3b9b417ee46f8c152c5c2c23d464064e5cdd537da8892c6728860fd7",
"references": [
{
"side": "client",
"jar_sha256": "4508d006323f24fa02876310c192d739af56516eb259000ac50f0909a68c9a2d",
"output_sha256": "9ae4ecba3b9b417ee46f8c152c5c2c23d464064e5cdd537da8892c6728860fd7",
"matched_output_lines": 101784
},
{
"side": "server",
"jar_sha256": "a362163eec5d1612d520772bc16e5b39c09e3b234fdc045f56bf544284ee8ae6",
"output_sha256": "9ae4ecba3b9b417ee46f8c152c5c2c23d464064e5cdd537da8892c6728860fd7",
"matched_output_lines": 101784
}
],
"scope": "Native ProtoChunkTicks load/schedule deduplication, insertion order, zero delay for scheduled requests, preserved saved delays/priorities, time-independent packing and independent scheduledTicks snapshots. Native WorldGenTickAccess lookup ordering, routing to mixed proto/level containers, lookup failures, false willTickThisTick and zero count without lookups. Proto-to-level promotion and unpacked save behavior, position/type identities, priorities, duplicates, unloaded containers and integer/time extremes are compared. Rust exposes pack as a borrowed immutable slice; mutation of the native returned list is outside the API. Actual block/fluid execution and feature-world scheduling integration remain pending."
}
@@ -0,0 +1,188 @@
//! Private native differential transport for generation-stage tick containers.
use horizon_core::{
position::BlockPos,
ticks::{
ChunkTicks, ProtoChunkTicks, SavedTick, ScheduledTick, TickContainerAccess, TickPriority,
TickTypeId, WorldGenTickAccess, WorldGenTickContainers,
},
};
use std::{
collections::BTreeMap,
io::{self, BufRead, Write},
str::{FromStr, SplitWhitespace},
};
enum Container {
Proto(ProtoChunkTicks),
Level(ChunkTicks),
}
impl TickContainerAccess for Container {
fn schedule(&mut self, t: ScheduledTick) -> bool {
match self {
Self::Proto(c) => c.schedule(t),
Self::Level(c) => c.schedule(t),
}
}
fn has_scheduled(&self, p: BlockPos, k: TickTypeId) -> bool {
match self {
Self::Proto(c) => c.has_scheduled(p, k),
Self::Level(c) => c.has_scheduled(p, k),
}
}
}
#[derive(Default)]
struct World {
chunks: BTreeMap<(i32, i32), Container>,
events: Vec<String>,
fail: bool,
}
impl WorldGenTickContainers for World {
type Container = Container;
type Error = ();
fn container_at(&mut self, p: BlockPos) -> Result<&mut Container, ()> {
self.events.push(format!("lookup {} {} {}", p.x, p.y, p.z));
if self.fail {
return Err(());
}
Ok(self
.chunks
.entry((p.x >> 4, p.z >> 4))
.or_insert_with(|| Container::Proto(ProtoChunkTicks::default())))
}
}
fn number<T: FromStr>(input: &mut SplitWhitespace<'_>) -> T
where
T::Err: std::fmt::Debug,
{
input.next().unwrap().parse().unwrap()
}
fn pos(input: &mut SplitWhitespace<'_>) -> BlockPos {
BlockPos::new(number(input), number(input), number(input))
}
fn saved(input: &mut SplitWhitespace<'_>) -> SavedTick {
SavedTick {
kind: TickTypeId(number(input)),
pos: pos(input),
delay: number(input),
priority: TickPriority::by_value(number(input)),
}
}
fn emit(out: &mut impl Write, ticks: &[SavedTick]) -> io::Result<()> {
writeln!(out, "saved {}", ticks.len())?;
for t in ticks {
writeln!(
out,
"value {} {} {} {} {} {}",
t.kind.0, t.pos.x, t.pos.y, t.pos.z, t.delay, t.priority as i8
)?;
}
Ok(())
}
fn flush(access: &mut WorldGenTickAccess<World>, out: &mut impl Write) -> io::Result<()> {
for e in access.containers_mut().events.drain(..) {
writeln!(out, "{e}")?;
}
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut access = WorldGenTickAccess::new(World::default());
let mut snapshot = Vec::new();
let mut out = io::BufWriter::new(io::stdout().lock());
for (index, line) in io::stdin().lock().lines().enumerate() {
let line = line?;
let mut input = line.split_whitespace();
let op = input.next().unwrap();
writeln!(out, "command {index} {op}")?;
match op {
"reset" => {
access = WorldGenTickAccess::new(World::default());
snapshot.clear();
}
"fail" => access.containers_mut().fail = number::<i32>(&mut input) != 0,
"load" | "level" => {
let key = (number(&mut input), number(&mut input));
let count: usize = number(&mut input);
let ticks = (0..count).map(|_| saved(&mut input)).collect::<Vec<_>>();
let c = if op == "load" {
Container::Proto(ProtoChunkTicks::from_saved(ticks))
} else {
Container::Level(ChunkTicks::from_saved(ticks))
};
access.containers_mut().chunks.insert(key, c);
}
"schedule" => {
let t = ScheduledTick {
kind: TickTypeId(number(&mut input)),
pos: pos(&mut input),
trigger_tick: number(&mut input),
priority: TickPriority::by_value(number(&mut input)),
sub_tick_order: number(&mut input),
};
let result = access.schedule(t);
flush(&mut access, &mut out)?;
writeln!(
out,
"{}",
if result.is_ok() { "scheduled" } else { "error" }
)?;
}
"query" => {
let k = TickTypeId(number(&mut input));
let p = pos(&mut input);
let result = access.has_scheduled(p, k);
flush(&mut access, &mut out)?;
match result {
Ok(v) => writeln!(out, "query {v}")?,
Err(()) => writeln!(out, "error")?,
}
}
"will" => {
let k = TickTypeId(number(&mut input));
let p = pos(&mut input);
writeln!(out, "will {}", access.will_tick_this_tick(p, k))?;
}
"dump" => {
let key = (number(&mut input), number(&mut input));
let time = number(&mut input);
match access.containers().chunks.get(&key) {
Some(Container::Proto(c)) => emit(&mut out, c.pack(time))?,
Some(Container::Level(c)) => emit(&mut out, &c.pack(time))?,
None => writeln!(out, "absent")?,
}
}
"snapshot" => {
let key = (number(&mut input), number(&mut input));
if let Some(Container::Proto(c)) = access.containers().chunks.get(&key) {
snapshot = c.scheduled_ticks();
} else {
snapshot.clear();
}
}
"snapshot_dump" => emit(&mut out, &snapshot)?,
"unpack" | "promote" => {
let key = (number(&mut input), number(&mut input));
let time = number(&mut input);
if let Some(c) = access.containers_mut().chunks.get_mut(&key) {
if op == "promote" {
if let Container::Proto(proto) = c {
*c = Container::Level(ChunkTicks::from_saved(proto.scheduled_ticks()));
}
}
if let Container::Level(c) = c {
c.unpack(time);
}
}
}
"remove" => {
access
.containers_mut()
.chunks
.remove(&(number(&mut input), number(&mut input)));
}
"count" => {}
_ => panic!("unknown operation {op}"),
}
assert!(input.next().is_none());
writeln!(out, "count {}", access.count())?;
}
Ok(())
}
+110
View File
@@ -0,0 +1,110 @@
//! Generation-stage tick storage and routing, before level tick execution.
use super::{ChunkTicks, SavedTick, ScheduledTick, TickTypeId};
use crate::position::BlockPos;
use std::collections::HashSet;
/// Proto chunks retain the first request for a type/position in insertion order.
/// Newly scheduled delays are zero; loaded delays and priorities are preserved.
#[derive(Clone, Debug, Default)]
pub struct ProtoChunkTicks {
ticks: Vec<SavedTick>,
positions: HashSet<(TickTypeId, BlockPos)>,
}
impl ProtoChunkTicks {
pub fn from_saved(ticks: impl IntoIterator<Item = SavedTick>) -> Self {
let mut result = Self::default();
for tick in ticks {
result.schedule_saved(tick);
}
result
}
fn schedule_saved(&mut self, tick: SavedTick) -> bool {
if !self.positions.insert((tick.kind, tick.pos)) {
return false;
}
self.ticks.push(tick);
true
}
pub fn schedule(&mut self, tick: ScheduledTick) -> bool {
self.schedule_saved(SavedTick {
kind: tick.kind,
pos: tick.pos,
delay: 0,
priority: tick.priority,
})
}
pub fn has_scheduled(&self, pos: BlockPos, kind: TickTypeId) -> bool {
self.positions.contains(&(kind, pos))
}
pub fn count(&self) -> usize {
self.ticks.len()
}
/// Borrow the current saved list. Game time does not affect proto delays.
pub fn pack(&self, _game_time: i64) -> &[SavedTick] {
&self.ticks
}
/// An independent snapshot, matching scheduledTicks rather than pack's view.
pub fn scheduled_ticks(&self) -> Vec<SavedTick> {
self.ticks.clone()
}
}
pub trait TickContainerAccess {
fn schedule(&mut self, tick: ScheduledTick) -> bool;
fn has_scheduled(&self, pos: BlockPos, kind: TickTypeId) -> bool;
}
impl TickContainerAccess for ProtoChunkTicks {
fn schedule(&mut self, tick: ScheduledTick) -> bool {
Self::schedule(self, tick)
}
fn has_scheduled(&self, pos: BlockPos, kind: TickTypeId) -> bool {
Self::has_scheduled(self, pos, kind)
}
}
impl TickContainerAccess for ChunkTicks {
fn schedule(&mut self, tick: ScheduledTick) -> bool {
Self::schedule(self, tick)
}
fn has_scheduled(&self, pos: BlockPos, kind: TickTypeId) -> bool {
Self::has_scheduled(self, pos, kind)
}
}
/// World-owned selection of the receiving chunk/container. This receives the
/// original block position, so lookup policy and its errors remain observable.
pub trait WorldGenTickContainers {
type Container: TickContainerAccess;
type Error;
fn container_at(&mut self, pos: BlockPos) -> Result<&mut Self::Container, Self::Error>;
}
pub struct WorldGenTickAccess<C> {
containers: C,
}
impl<C: WorldGenTickContainers> WorldGenTickAccess<C> {
pub fn new(containers: C) -> Self {
Self { containers }
}
pub fn containers(&self) -> &C {
&self.containers
}
pub fn containers_mut(&mut self) -> &mut C {
&mut self.containers
}
pub fn into_containers(self) -> C {
self.containers
}
pub fn schedule(&mut self, tick: ScheduledTick) -> Result<bool, C::Error> {
Ok(self.containers.container_at(tick.pos)?.schedule(tick))
}
pub fn has_scheduled(&mut self, pos: BlockPos, kind: TickTypeId) -> Result<bool, C::Error> {
Ok(self.containers.container_at(pos)?.has_scheduled(pos, kind))
}
/// Generation access never reports a currently executing tick or a level count.
pub fn will_tick_this_tick(&self, _pos: BlockPos, _kind: TickTypeId) -> bool {
false
}
pub fn count(&self) -> usize {
0
}
}
+4
View File
@@ -1,5 +1,9 @@
//! Scheduled block/fluid tick machinery for Java Edition 26.3. Type IDs identify
//! registry-owned types; each block/fluid registry uses its own LevelTicks.
mod generation;
pub use generation::{
ProtoChunkTicks, TickContainerAccess, WorldGenTickAccess, WorldGenTickContainers,
};
mod heap;
mod index;
use crate::position::{BlockPos, ChunkPos};
+16
View File
@@ -661,3 +661,19 @@ mutations, rejected writes, partial failures, extreme heights/sea levels, wrappe
coordinates and composition. Workspace tests and all-target Clippy pass. The
fixture uses controlled heights and one biome; lighting propagation, dynamic
biome selection, world updates and full generation remain pending.
## Generation-stage tick containers
Proto-chunk tick containers now keep the first type/position request in insertion
order. Newly scheduled ticks retain priority with zero delay; loaded entries
retain their saved delay. Generation tick access routes requests to the selected
container and reports no currently executing ticks or aggregate count. Snapshots
can be promoted into the existing level tick queue.
The [generation-tick comparison](../benchmarks/rust-generation-ticks-2026-10-06/scoped-parity.json)
matches **101,784 lines per reference JAR**, covering duplicates, saved entries,
snapshot independence, mixed proto/level routing, lookup failures, promotion,
priority boundaries and integer/time extremes. Workspace tests, all-target Clippy
and all 149 Python tests pass. Rust exposes packed entries as an immutable
borrowed slice; mutating Java's returned list is outside this API. Feature-world
scheduling integration and block/fluid tick execution remain pending.
@@ -0,0 +1,48 @@
package dev.horizonlab.parity;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.ticks.*;
/** Original transport for native proto tick storage and world-generation routing. */
public final class VanillaGenerationTickOracle {
private static final Object[] TYPES=new Object[32];
static{for(int i=0;i<TYPES.length;i++)TYPES[i]=new Object();}
private static final class Stop extends RuntimeException{}
private static final class Input {
final String[] words;int cursor=1;Input(String line){words=line.split(" ");}
int integer(){return Integer.parseInt(words[cursor++]);}long number(){return Long.parseLong(words[cursor++]);}
BlockPos pos(){return new BlockPos(integer(),integer(),integer());}long chunk(){return new ChunkPos(integer(),integer()).pack();}
SavedTick<Object> saved(){return new SavedTick<>(TYPES[integer()],pos(),integer(),TickPriority.byValue(integer()));}
}
private final Map<Long,TickContainerAccess<Object>> chunks=new HashMap<>();
private final PrintWriter out;private boolean fail;private List<SavedTick<Object>> snapshot=List.of();
private final WorldGenTickAccess<Object> access;
private VanillaGenerationTickOracle(PrintWriter out){this.out=out;access=new WorldGenTickAccess<>(p->{out.println("lookup "+p.getX()+" "+p.getY()+" "+p.getZ());if(fail)throw new Stop();return chunks.computeIfAbsent(ChunkPos.pack(p),key->new ProtoChunkTicks<>());});}
private static int type(Object value){for(int i=0;i<TYPES.length;i++)if(TYPES[i]==value)return i;throw new AssertionError();}
private void emit(List<SavedTick<Object>> ticks){out.println("saved "+ticks.size());for(var t:ticks)out.println("value "+type(t.type())+" "+t.pos().getX()+" "+t.pos().getY()+" "+t.pos().getZ()+" "+t.delay()+" "+t.priority().getValue());}
@SuppressWarnings("unchecked") private void command(String line,int index){var in=new Input(line);String op=in.words[0];out.println("command "+index+" "+op);
try{switch(op){
case "reset"->{chunks.clear();snapshot=List.of();fail=false;}
case "fail"->fail=in.integer()!=0;
case "load","level"->{long key=in.chunk();int count=in.integer();var ticks=new ArrayList<SavedTick<Object>>();for(int i=0;i<count;i++)ticks.add(in.saved());chunks.put(key,op.equals("load")?ProtoChunkTicks.load(ticks):new LevelChunkTicks<>(ticks));}
case "schedule"->{var t=new ScheduledTick<>(TYPES[in.integer()],in.pos(),in.number(),TickPriority.byValue(in.integer()),in.number());access.schedule(t);out.println("scheduled");}
case "query"->{var type=TYPES[in.integer()];var p=in.pos();out.println("query "+access.hasScheduledTick(p,type));}
case "will"->{var type=TYPES[in.integer()];var p=in.pos();out.println("will "+access.willTickThisTick(p,type));}
case "dump"->{var c=chunks.get(in.chunk());long time=in.number();if(c==null)out.println("absent");else emit(((SerializableTickContainer<Object>)c).pack(time));}
case "snapshot"->{var c=chunks.get(in.chunk());snapshot=c instanceof ProtoChunkTicks<?>?((ProtoChunkTicks<Object>)c).scheduledTicks():List.of();}
case "snapshot_dump"->emit(snapshot);
case "unpack","promote"->{long key=in.chunk(),time=in.number();var c=chunks.get(key);if(op.equals("promote")&&c instanceof ProtoChunkTicks<?>){c=new LevelChunkTicks<>(((ProtoChunkTicks<Object>)c).scheduledTicks());chunks.put(key,c);}if(c instanceof LevelChunkTicks<?>)((LevelChunkTicks<Object>)c).unpack(time);}
case "remove"->chunks.remove(in.chunk());
case "count"->{}
default->throw new AssertionError(op);
}}catch(Stop ignored){out.println("error");}
if(in.cursor!=in.words.length)throw new AssertionError("extra input");out.println("count "+access.count());
}
public static void main(String[] args)throws Exception{
net.minecraft.SharedConstants.tryDetectVersion();
try(var input=Files.newBufferedReader(Path.of(args[0]));var out=new PrintWriter(Files.newBufferedWriter(Path.of(args[1])))){var oracle=new VanillaGenerationTickOracle(out);String line;int index=0;while((line=input.readLine())!=null)oracle.command(line,index++);}
}
}
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""Compare proto-chunk ticks and world-generation tick routing with vanilla."""
import argparse
from collections import Counter
import json
import os
from pathlib import Path
import platform
import random
import subprocess
try:
from . import lab, test_rust_parity as base
except ImportError:
import lab, test_rust_parity as base
def corpus():
extremes = [-9223372036854775808, -2147483648, -1, 0, 1, 2147483647, 9223372036854775807]
for time in extremes:
yield 'reset'
yield 'load -1 -1 4 0 -1 64 -1 -2147483648 -100 0 -1 64 -1 2147483647 100 1 -1 64 -1 -1 0 2 -2 65 -2 12 3'
yield 'query 0 -1 64 -1'
yield 'snapshot -1 -1'
yield f'schedule 0 -1 64 -1 {time} -3 {time}'
yield f'schedule 3 -3 66 -3 {time} 2 {time}'
yield f'dump -1 -1 {time}'
yield 'snapshot_dump'
yield f'promote -1 -1 {time}'
yield f'dump -1 -1 {time}'
yield f'schedule 4 -4 67 -4 {time} -2 {time}'
yield f'dump -1 -1 {time}'
yield 'snapshot_dump'
for kind in ['load', 'level']:
yield 'reset'
# Deliberately load a position outside the owning container; routing is
# determined by each request's position, not by entries in saved lists.
yield f'{kind} 0 0 2 0 16 64 0 7 0 0 16 64 0 8 -3'
yield 'query 0 16 64 0'
yield 'dump 0 0 0'
yield 'dump 1 0 0'
yield 'fail 1'
yield 'query 0 0 64 0'
yield 'schedule 1 0 64 0 0 0 0'
yield 'will 1 0 64 0'
yield 'count'
yield 'fail 0'
yield 'query 1 0 64 0'
yield 'unpack 0 0 9223372036854775807'
yield 'dump 0 0 -9223372036854775808'
positions = [[-2147483648, -2147483648, 2147483647], [2147483647, 2147483647, -2147483648],
[0, 0, 0], [67108864, 4096, -67108864], [-1, -1, -1], [15, 319, 15], [16, -64, 16]]
for priority in [-2147483648, -4, -3, -2, -1, 0, 1, 2, 3, 4, 2147483647]:
yield 'reset'
for i, p in enumerate(positions):
xyz = ' '.join(map(str, p))
yield f'schedule {i} {xyz} -9223372036854775808 {priority} 9223372036854775807'
yield f'schedule {i} {xyz} 9223372036854775807 {-priority if priority != -2147483648 else 0} -9223372036854775808'
yield f'query {i} {xyz}'
yield f'will {i} {xyz}'
yield f'dump {p[0] >> 4} {p[2] >> 4} 12345'
rng = random.Random(0x484f52495a4f4e)
for trial in range(12):
yield 'reset'
for cx in range(-2, 3):
command = 'load' if (cx + trial) % 2 == 0 else 'level'
rows = []
for i in range(24):
row = [rng.randrange(8), cx*16+rng.randrange(4), rng.randrange(-3, 4), rng.randrange(4), rng.choice([-2147483648, -1, 0, 1, 2147483647]), rng.randrange(-8, 9)]
rows.append(' '.join(map(str, row)))
if i % 3 == 0: rows.append(rows[-1])
yield f'{command} {cx} 0 {len(rows)} ' + ' '.join(rows)
for step in range(700):
cx = rng.randrange(-2, 3); p = [cx*16+rng.randrange(4), rng.randrange(-3, 4), rng.randrange(4)]
xyz = ' '.join(map(str, p)); typ = rng.randrange(8)
if step % 71 == 0:
yield 'fail 1'; yield f'query {typ} {xyz}'; yield f'will {typ} {xyz}'; yield 'count'; yield 'fail 0'
if step % 79 == 0: yield f'promote {cx} 0 {rng.choice(extremes)}'
if step % 83 == 0: yield f'snapshot {cx} 0'
if step % 89 == 0: yield 'snapshot_dump'
if step % 101 == 0: yield f'remove {cx} 0'
yield f'schedule {typ} {xyz} {rng.choice(extremes)} {rng.randrange(-8, 9)} {rng.choice(extremes)}'
yield f'query {typ} {xyz}'
if step % 23 == 0: yield f'dump {cx} 0 {rng.choice(extremes)}'
for cx in range(-2, 3): yield f'dump {cx} 0 {rng.choice(extremes)}'
yield 'snapshot_dump'
def main():
parser = argparse.ArgumentParser(description=__doc__); parser.add_argument('--prepare', action='store_true'); args = parser.parse_args()
build = lab.LOCAL / 'rust-generation-tick-parity'; build.mkdir(parents=True, exist_ok=True)
report_path = build / 'report.json'; report_path.unlink(missing_ok=True)
if args.prepare: lab.prepare()
lock, jars, libraries = base.verify_artifacts()
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGenerationTickOracle.java'
sources = [oracle, Path(__file__), lab.ROOT / 'tools/test_rust_parity.py', lab.ROOT / 'tools/lab.py', lab.ROOT / 'Cargo.toml', lab.ROOT / 'Cargo.lock', lab.ROOT / 'crates/horizon-core/Cargo.toml',
*sorted((lab.ROOT / 'crates/horizon-core/src').rglob('*.rs')), lab.ROOT / 'crates/horizon-core/examples/generation_tick_fixture.rs']
tested_sources = base.source_hashes(sources)
commands = list(corpus()); inputs = build / 'inputs.txt'; inputs.write_text('\n'.join(commands) + '\n')
tested_fixtures = base.source_hashes([inputs]); classes = build / 'classes'; classes.mkdir(exist_ok=True)
dependencies = os.pathsep.join(map(str, libraries))
def run(command, name, **kwargs):
with (build / (name + '.log')).open('w') as log:
subprocess.run(list(map(str, command)), cwd=lab.ROOT, check=True, timeout=3600, stderr=log, **({'stdout': log} | kwargs))
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-cp', str(jars['server']) + os.pathsep + dependencies, '-d', classes, oracle], 'compile-java')
run(['cargo', 'build', '--release', '-p', 'horizon-core', '--example', 'generation_tick_fixture'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--no-deps', '--format-version', '1'], cwd=lab.ROOT, text=True))
binary = Path(metadata['target_directory']) / 'release/examples' / ('generation_tick_fixture.exe' if os.name == 'nt' else 'generation_tick_fixture')
with inputs.open('rb') as source, (build / 'rust.txt').open('wb') as output: run([binary], 'rust', stdin=source, stdout=output)
references = []
for side, jar in jars.items():
run(['java', '-Xmx512M', '-cp', os.pathsep.join([str(classes), str(jar), dependencies]), 'dev.horizonlab.parity.VanillaGenerationTickOracle', inputs, build / (side + '.txt')], side)
matched = base.compare(build / (side + '.txt'), build / 'rust.txt')
references.append({'side': side, 'jar_sha256': lab.digest(jar, 'sha256'), 'output_sha256': lab.digest(build / (side + '.txt'), 'sha256'), 'matched_output_lines': matched})
print(f'{side}: {matched:,} generation-tick lines match Rust', flush=True)
base.ensure_sources_unchanged(sources, tested_sources); base.ensure_sources_unchanged([inputs], tested_fixtures)
report = {'schema': 1, 'minecraft': lock['minecraft']['version'], 'manifest_sha1': lock['minecraft']['manifest_sha1'], 'platform': platform.platform(),
'java': subprocess.check_output(['java', '--version'], text=True).splitlines()[0], 'rustc': subprocess.check_output(['rustc', '--version'], text=True).strip(),
'commands': dict(Counter(command.split()[0] for command in commands)), 'sources_sha256': tested_sources, 'fixtures_sha256': tested_fixtures,
'rust_output_sha256': lab.digest(build / 'rust.txt', 'sha256'), 'references': references,
'scope': 'Native ProtoChunkTicks load/schedule deduplication, insertion order, zero delay for scheduled requests, preserved saved delays/priorities, time-independent packing and independent scheduledTicks snapshots. Native WorldGenTickAccess lookup ordering, routing to mixed proto/level containers, lookup failures, false willTickThisTick and zero count without lookups. Proto-to-level promotion and unpacked save behavior, position/type identities, priorities, duplicates, unloaded containers and integer/time extremes are compared. Rust exposes pack as a borrowed immutable slice; mutation of the native returned list is outside the API. Actual block/fluid execution and feature-world scheduling integration remain pending.'}
lab.write_json(report_path, report); print(f'Scoped generation-tick parity passed: {report_path}')
if __name__ == '__main__': main()