mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 02:56:17 -10:00
Implement proto-chunk ticks and world-generation tick routing
This commit is contained in:
1 parent
fd3416da36
commit
1cf1322ae2
8 files changed
+604
No files matched your search
@@ -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(())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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++);}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user