diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f3ffc8327..e85a0f1483 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/benchmarks/rust-generation-ticks-2026-10-06/scoped-parity.json b/benchmarks/rust-generation-ticks-2026-10-06/scoped-parity.json new file mode 100644 index 0000000000..7cce5eb4ec --- /dev/null +++ b/benchmarks/rust-generation-ticks-2026-10-06/scoped-parity.json @@ -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." +} diff --git a/crates/horizon-core/examples/generation_tick_fixture.rs b/crates/horizon-core/examples/generation_tick_fixture.rs new file mode 100644 index 0000000000..3d154e82ae --- /dev/null +++ b/crates/horizon-core/examples/generation_tick_fixture.rs @@ -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, + 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(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, out: &mut impl Write) -> io::Result<()> { + for e in access.containers_mut().events.drain(..) { + writeln!(out, "{e}")?; + } + Ok(()) +} +fn main() -> Result<(), Box> { + 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::(&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::>(); + 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(()) +} diff --git a/crates/horizon-core/src/ticks/generation.rs b/crates/horizon-core/src/ticks/generation.rs new file mode 100644 index 0000000000..4e1fdce6f0 --- /dev/null +++ b/crates/horizon-core/src/ticks/generation.rs @@ -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, + positions: HashSet<(TickTypeId, BlockPos)>, +} +impl ProtoChunkTicks { + pub fn from_saved(ticks: impl IntoIterator) -> 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 { + 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 { + containers: C, +} +impl WorldGenTickAccess { + 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 { + Ok(self.containers.container_at(tick.pos)?.schedule(tick)) + } + pub fn has_scheduled(&mut self, pos: BlockPos, kind: TickTypeId) -> Result { + 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 + } +} diff --git a/crates/horizon-core/src/ticks/mod.rs b/crates/horizon-core/src/ticks/mod.rs index 38fcef4db0..f3cc839999 100644 --- a/crates/horizon-core/src/ticks/mod.rs +++ b/crates/horizon-core/src/ticks/mod.rs @@ -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}; diff --git a/docs/rust-parity.md b/docs/rust-parity.md index 3deb7baccd..806104da9f 100644 --- a/docs/rust-parity.md +++ b/docs/rust-parity.md @@ -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. diff --git a/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGenerationTickOracle.java b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGenerationTickOracle.java new file mode 100644 index 0000000000..4d956f5f06 --- /dev/null +++ b/mods/horizon/src/test/java/dev/horizonlab/parity/VanillaGenerationTickOracle.java @@ -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 saved(){return new SavedTick<>(TYPES[integer()],pos(),integer(),TickPriority.byValue(integer()));} + } + private final Map> chunks=new HashMap<>(); + private final PrintWriter out;private boolean fail;private List> snapshot=List.of(); + private final WorldGenTickAccess 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> 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>();for(int i=0;i(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)c).pack(time));} + case "snapshot"->{var c=chunks.get(in.chunk());snapshot=c instanceof ProtoChunkTicks?((ProtoChunkTicks)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)c).scheduledTicks());chunks.put(key,c);}if(c instanceof LevelChunkTicks)((LevelChunkTicks)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++);} + } +} diff --git a/tools/test_generation_tick_parity.py b/tools/test_generation_tick_parity.py new file mode 100644 index 0000000000..c0d906fb76 --- /dev/null +++ b/tools/test_generation_tick_parity.py @@ -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()